雪崩效应
在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。服务雪崩效应是一种因“服务提供者”的不可用导致“服务消费者”的不可用,并将不可用逐渐放大的过程。
如果下图所示:A作为服务提供者,B为A的服务消费者,C和D是B的服务消费者。A不可用引起了B的不可用,并将不可用像滚雪球一样放大到C和D时,雪崩效应就形成了。
分布式系统面临的问题复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。 服务雪崩多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”. 对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。备注:一般情况对于服务依赖的保护主要有3中解决方案:
(1)熔断模式:这种模式主要是参考电路熔断,如果一条线路电压过高,保险丝会熔断,防止火灾。放到我们的系统中,如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
(2)隔离模式:这种模式就像对系统请求按类型划分成一个个小岛的一样,当某个小岛被火少光了,不会影响到其他的小岛。例如可以对不同类型的请求使用线程池来资源隔离,每种类型的请求互不影响,如果一种类型的请求线程资源耗尽,则对后续的该类型请求直接返回,不再调用后续资源。这种模式使用场景非常多,例如将一个服务拆开,对于重要的服务使用单独服务器来部署,再或者公司最近推广的多中心。
(3)限流模式:上述的熔断模式和隔离模式都属于出错后的容错处理机制,而限流模式则可以称为预防模式。限流模式主要是提前对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源。这种模式不能解决服务依赖的问题,只能解决系统整体资源分配问题,因为没有被限流的请求依然有可能造成雪崩效应。
什么是Hystrix?
在分布式系统中,服务与服务之间的依赖错综复杂,一种不可避免的情况就是某些服务会出现故障,导致依赖于它们的其他服务出现远程调度的线程阻塞。Hystrix提供了熔断器功能,能够阻止分布式系统中出现联动故障,通过隔离服务的访问点阻止联动故障的,并提供了故障的解决方案,从而提高 了整个分布式系统的弹性。
简单来说:微服务架构中存在很多的服务,例如A、B、C、D、E5个服务,并且对于一次调用来说,并不仅仅是访问一个服务,按照功能划分来说,每个服务做得事情是单一的,所以大部分的请求都是类似于A-C-E,跨过多个服务。
如果单个服务的正常运行几率为99.00%(机房的不可靠性、运营商的不可靠性等等),那么对于A-C-E调用流程来说,几率为99.00%*99.00%*99.00%=97.03%,也就意味着10000次请求有300多次会失败,问题还是比较严重的。
作用
服务降级
Hystrix服务降级,其实就是线程池中单个线程障处理,防止单个线程请求时间太长,导致资源长期被占有而得不到释放,从而导致线程池被快速占用完,导致服务崩溃。Hystrix能解决如下问题:1.请求超时降级,线程资源不足降级,降级之后可以返回自定义数据2.线程池隔离降级,分布式服务可以针对不同的服务使用不同的线程池,从而互不影响3.自动触发降级与恢复4.实现请求缓存和请求合并
服务熔断
熔断模式:这种模式主要是参考电路熔断,如果一条线路电压过高,保险丝会熔断,防止火灾。放到我们的系统中,如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
服务限流
限流模式主要是提前对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源。这种模式不能解决服务依赖的问题,只能解决系统整体资源分配问题,因为没有被限流的请求依然有可能造成雪崩效应。
原理
熔断器的原理很简单,如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。
熔断器模式就像是那些容易导致错误的操作的一种代理。这种代理能够记录最近调用发生错误的次数,然后决定使用允许操作继续,或者立即返回错误。
熔断器开关相互转换的逻辑如下图:
熔断器就是保护服务高可用的最后一道防线。
Hystrix设计原则
- 防止单个服务的故障耗尽整个服务的Servlet容器(例如Tomcat)的线程资源。
- 快速失败机制,如果某个服务出现了故障,则调用该服务的请求快速失败,而不是线程等待。
- 提供回退(fallback)方案,在请求发生故障时,提供设定好的回退方案。
- 使用熔断机制,防止故障扩散到其他服务。
- 提供熔断器的监控组件Hystrix Dashboard ,可以实时监控熔断器的状态。
Hystrix工作机制
1.断路器机制
断路器很好理解, 当Hystrix Command请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒), 自动切换到半开路状态(HALF-OPEN). 这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力.
2.Fallback
Fallback相当于是降级操作. 对于查询操作, 我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. fallback方法的返回值一般是设置的默认值或者来自缓存.
3.资源隔离
在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池. 例如调用产品服务的Command放入A线程池, 调用账户服务的Command放入B线程池. 这样做的主要优点是运行环境被隔离开了. 这样就算调用服务的代码存在bug或者由于其他原因导致自己所在线程池被耗尽时, 不会对系统的其他服务造成影响. 但是带来的代价就是维护多个线程池会对系统带来额外的性能开销. 如果是对性能有严格要求而且确信自己调用服务的客户端代码不会出问题的话, 可以使用Hystrix的信号模式(Semaphores)来隔离资源.
使用方法
1、在RestTemplate和Ribbon上使用熔断器
找到本人之前的eureka-consumer项目,gitbub地址:https://github.com/ali-mayun/eureka-consumer
新加依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> <version>2.1.1.RELEASE</version> </dependency>
运行主类:
package com.ty.eurekaconsumer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.hystrix.EnableHystrix; @EnableEurekaClient @SpringBootApplication //使用这注解开启hystrix的功能 @EnableHystrix public class EurekaConsumerApplication { public static void main(String[] args) { SpringApplication.run(EurekaConsumerApplication.class, args); } }
更改之前的service
package com.ty.eurekaconsumer.service; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.client.RestTemplate; @Service public class RibbonService { @Autowired private RestTemplate restTemplate; //使用@HystrixCommand注解启用熔断器的功能,当服务熔断后,将不调用远程服务,直接调用本地的handleError方法 @HystrixCommand(fallbackMethod = "handleError") public String hiService(String name) { return restTemplate.getForObject("http://eureka-provider/firstCall?name=" + name, String.class); } public String handleError(String name) { return name + "在访问远程服务时,被熔断了,调用本地的方法"; } }
现在启动eureka-server、eureka-consumer两个项目,不启动eureka-provider,制造服务不能用的假象,运行效果如下:
2、在feign上使用熔断器
上面一种方式相对来说还是比较麻烦的,使用feign则方便很多。
打开上篇博文中的eureka-feign-consumer项目,github地址:https://github.com/ali-mayun/eureka-feign-consumer
修改application.properties文件,开启Hystrix功能:
# 服务名称 spring.application.name=eureka-feign-consumer # 端口号 server.port=4001 # 服务注册中心地址 eureka.client.serviceUrl.defaultZone=http://localhost:1001/eureka/ # 增加Hystrix功能 feign.hystrix.enabled=true
更改EurekaClientFeign:
package com.ty.eurekafeignconsumer.service; import com.ty.eurekafeignconsumer.config.FeignConfig; import com.ty.eurekafeignconsumer.hystrix.BaseHystrix; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; //新增fallback属性指定出现熔断由哪个类来处理。 @FeignClient(value = "eureka-provider", configuration = FeignConfig.class, fallback = BaseHystrix.class) public interface EurekaClientFeign { //只需要在接口中定义方法即可。调用eureka-provider服务的firstCall方法,并且feign集成了Ribbon @GetMapping(value = "/firstCall") String sayHiFromClientEureka(@RequestParam("name") String name); }
FeignConfig:
package com.ty.eurekafeignconsumer.config; import feign.Retryer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import static java.util.concurrent.TimeUnit.SECONDS; @Configuration public class FeignConfig { @Bean public Retryer feignRetryer() { //调用远程服务失败后,会进行重试。重试间隔为1s,最大尝试次数为5 return new Retryer.Default(100, SECONDS.toMillis(1), 5); } }
BaseHystrix:
package com.ty.eurekafeignconsumer.hystrix; import com.ty.eurekafeignconsumer.service.EurekaClientFeign; import org.springframework.stereotype.Component; //熔断处理类需要实现调用远程服务的service,例如本案例在EurekaClientFeign类中调用远程服务,就需要实现此接口, //然后相同名字的方法就可以一一对应 @Componentpublic class BaseHystrix implements EurekaClientFeign { @Override public String sayHiFromClientEureka(String name) { return name + "访问的远程服务被熔断"; } }
package com.ty.eurekafeignconsumer.controller; import com.ty.eurekafeignconsumer.service.EurekaClientFeign; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class FeignController {
//本人idea会报一个问题:Could not autowire. There is more than one bean of 'EurekaClientFeign' type.但是不影响运行 @Autowired private EurekaClientFeign eurekaClientFeign; @GetMapping("/hi") public String sayHi(@RequestParam(value = "name", defaultValue = "马云", required = false) String name) { return eurekaClientFeign.sayHiFromClientEureka(name); } }
当eureka-provider正常开启,效果如下:
关闭eureka-provider,访问效果如下: