写在前面
分布式系统环境下,服务间依赖非常常见。单个应用通常会有多个不同类型的外部依赖服务,内部通常依赖于各种RPC服务,外部则依赖于各种HTTP服务。这些依赖服务不可避免的会出现调用失败,比如超时、异常等情况。因此,为了构建稳定、可靠的分布式系统,我们的服务应当具有自我保护能力,当依赖服务不可用时,当前服务启动自我保护功能,如何在外部依赖出问题的情况下仍然保证自身应用的稳定,避免出现服务资源耗尽的情况呢?可以阅读一下本文
硬件故障:如服务器宕机,机房断电,光纤被挖断等。
流量激增:如异常流量,重试加大流量等。
程序BUG:如程序逻辑导致内存泄漏,JVM长时间FullGC等。
同步等待:服务间采用同步调用模式,同步等待造成的资源耗尽。
缓存穿透:一般发生在应用重启,所有缓存失效或短时间内大量缓存失效时导致的服务提供者超负荷运行,引起服务不可用
一般情况对于服务依赖的保护主要有三种解决方案:
熔断模式:这种模式主要是参考电路熔断。如果一条线路电压过高,保险丝会熔断,防止火灾。类比到我们的系统中,如果某个目标服务调用慢或者有大量超时便熔断该服务的调用。对于后续调用请求,不在继续调用目标服务而是直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
隔离模式:对不同类型的请求使用线程池来资源隔离,每种类型的请求互不影响。如果一种类型的请求线程资源耗尽,则对后续的该类型请求直接返回。不再调用后续资源。
限流模式:上述的熔断模式和隔离模式都属于出错后的容错处理机制,而限流模式则可以称为预防模式。限流模式主要是提前对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源。这种模式不能解决服务依赖的问题,只能解决系统整体资源分配问题,因为没有被限流的请求依然有可能造成雪崩效应。
Hystrix入门
Hystrix设计目标:
对来自依赖的延迟和故障进行防护和控制
阻止故障的连锁反应
快速失败并迅速恢复
回退并优雅降级
提供接近实时的监控与告警
Hystrix遵循的设计原则:
防止任何单独的依赖耗尽资源(线程)
过载立即切断并快速失败,防止排队
尽可能提供回退以保护用户免受故障
使用隔离技术(例如隔板,泳道和断路器模式)来限制任何一个依赖的影响
通过近实时的指标,监控和告警,确保故障被及时发现
通过动态修改配置属性,确保故障及时恢复
防止整个依赖客户端执行失败,而不仅仅是网络通信
Hystrix如何实现这些设计目标?
使用命令模式将所有对外部服务(或依赖关系)的调用包装在
HystrixCommand或HystrixObservableCommand对象中,并将该对象放在单独的线程中执行
每个依赖都维护着一个线程池(或信号量),线程池被耗尽则拒绝请求(而不是让请求排队)
记录请求成功、失败、超时和线程拒绝。
服务错误百分比超过了阈值,熔断器开关自动打开,一段时间内停止对该服务的所有请求。
请求失败、被拒绝、超时或熔断时执行降级逻辑。
近实时地监控指标和配置的修改。
Hystrix容错
资源隔离
熔断
降级
保护应用程序以免受来自依赖故障的影响,指定依赖线程池饱和不会影响应用程序的其余部分。
当依赖从故障恢复正常时,应用程序会立即恢复正常的性能。
当应用程序一些配置参数错误时,线程池的运行状况会很快检测到这一点(通过增加错误、延迟、超时、拒绝等),同时可以通过动态属性进行实时纠正错误的参数配置。
如果服务的性能有变化,需要实时调整,比如增加或者减少超时时间,更改重试次数,可以通过线程池指标动态属性修改,而且不会影响到其他调用请求。
注意:尽管线程池提供了线程隔离,我们的客户端底层代码也必须要有超时设置或响应线程中断,不能无限制的阻塞以致线程池一直饱和。
缺点:

使用线程池时,发送请求的线程和执行依赖服务的线程不是同一个,而使用信号量时,发送请求的线程和执行依赖服务的线程是同一个,都是发起请求的线程。
客户端需向依赖服务发起请求时,首先要获取一个信号量才能真正发起调用,由于信号量的数量有限,当并发请求量超过信号量个数时,后续的请求都会直接拒绝,进入fallback流程。
| 线程切换 | 支持异步 | 支持超时 | 支持熔断 | 限流 | 开销 | |
| 信号量 | N | N | N | Y | Y | 小 |
| 线程池 | Y | Y | Y | Y | Y | 大 |
服务降级
服务降级触发条件:
程序运行异常
调用执行超时
服务熔断触发服务降级
线程池/信号量打满也会导致服务降级
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency>


熔断机制是应对雪崩的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
熔断打开:请求不在进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设休眠时长则进入熔断状态
熔断关闭:不会对服务进行熔断
熔断半开:部分请求根据规则调用当前服务,如果请求成功且服务符合规则则任务当前服务恢复正常,关闭熔断
三个重要参数
快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒
请求总数阈值:在快照时间窗内,必须满足请求总数阈值才有资格熔断,默认20次。意味着10秒内,如果Hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开
错误百分比阈值:当请求总数在快照时间窗内超过了阈值,比如发生了30次调用,如果在这30次调用中,有15次发生了异常,也就是超过了50%,在默认设定50%的阈值下断路器会打开
@EnableHystrix/@HystrixCommand/@HystrixProperty

刚才我们提到的@HystrixCommand、@HystrixProperties注解都是针对方法级别的,也就是说,针对每一个微服务接口我们都要提供对应的服务熔断策略和服务降级方法,这样不但代码非常的冗余,而且也会是Controller文件变得非常的庞大。针对这种情况,SpringCloud OpenFeign给我们提供了很好的支持。
针对每一个微服务接口提供对应的fallbackFactory实现,当某一个微服务发生异常触发降级时,会找到对应接口的实现方法进行降级操作

实现类要实现FallbackFactory接口,并指定泛型为微服务接口类型,并返回此类型

#开启feign对hystrix的支持feign.hystrix.enabled=true############# 降级&线程隔离相关配置 ############### 是否给方法执行设置超时时间,默认为truehystrix.command.default.execution.timeout.enabled=true## 配置请求隔离的方式,默认线程池方式。hystrix.command.default.execution.isolation.strategy=threadPool## 配置超时触发降级时间,默认为1shystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000## 发生超时时是否中断方法的执行,默认值为truehystrix.command.default.execution.isolation.thread.interruptOnTimeout=true## 是否在方法执行被取消时中断方法,默认值为falsehystrix.command.default.execution.isolation.thread.interruptOnCancel=false############# 熔断相关配置 ############### 是否启动熔断器,默认为truehystrix.command.default.circuitBreaker.enabled=true## 启用熔断器功能窗口时间内的最小请求数,默认20hystrix.command.default.circuitBreaker.requestVolumeThreshold=20## 熔断闭合后再次进入半开状态的休眠时间hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5000## 在滚动时间窗口内,在请求数量超过circuitBreaker.requestVoLumeThreshoLd的情况下,如果错误请求数的百分比超过此值,就把断路器设置为"闭合”状态,否则就设置为"关闭"状态.hystrix.command.default.circuitBreaker.errorThresholdPercentage=50## 是否强制启用熔断器,默认falsehystrix.command.default.circuitBreaker.forceOpen=false## 是否强制关闭熔断器,默认falsehystrix.command.default.circuitBreaker.forceClosed=false############# 统计器相关设置 ############### 设置滚动时间窗口时长,默认设置为10000毫秒hystrix.command.default.metrics.rollingStats.timeInMilliseconds=10000
当然Hystrix还有很多其他的配置,具体可以查看官方文档。通过这样的配置,我们就可以实现配置文件+针对接口的实现来全局配置Hystrix。
如果不启用Hystrix则Ribbon的超时时间就是Feign的超时时间,Feign自身的配置会被覆盖。
如果开启了Hystrix,那么Ribbon的超时时间与Hystrix的超时时间则存在依赖关系,因为涉及到Ribbon的重试机制,所以一般情况下都是Ribbon的超时时间小于Hystrix的超时时间,否则会出现以下错误:
2019-07-12 11:10:20,238 397194 [http-nio-8084-exec-2] WARN o.s.c.n.z.f.r.s.AbstractRibbonCommand - The Hystrix timeout of 40000ms for the command operation is set lower than the combination of the Ribbon read and connect timeout, 80000ms.
Ribbon重试次数(包含首次) =
1 + ribbon.MaxAutoRetries +
ribbon.MaxAutoRetriesNextServer +
(ribbon.MaxAutoRetries *
ribbon.MaxAutoRetriesNextServer)
所以,在Ribbon超时但Hystrix没有超时的情况下,Ribbon便会采取重试机制;而重试期间如果时间超过了Hystrix的超时配置则会立即被熔断.

创建HystrixCommand(用在依赖的服务返回单个操作结果的时候)或
HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候)对象.
命令执行。其中HystrixComand实现下面前两种执行方式:
而HystrixObservableCommand实现了后两种方式
execute():同步执行。从依赖的服务返回一个单一的结果对象,或是在发生错误的时候抛出异常。
queue():异步执行,直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。
observe():返回Observable对象,它代表了操作的多个结果,它是一个Hot Obserable(不论"事件源"是否有"订阅者",都会在创建后对事件进行发布,所以对于Hot Observable的每一个"订阅者"都有可能是从"事件源"的中途开始的,并可能只是看到了整个操作的局部过程).
toObservable():同样会返回Observable对象,也代表了操作的多个结果,但它返回的是一个Cold Observable(没有"订阅者"的时候并不会发布事件,而是进行等待,直到有"订阅者"之后才发布事件,所以对于Cold Observable的订阅者,它可以保证从一开始看到整个操作的全部过程
若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以Observable对象的形式返回。
检查断路器是否为打开状态
如果断路器是打开的,那么Hystrix不会执行命令,而是转接到fallback处理逻辑(第8步)
如果断路器是关闭的,检查是否有可用资源来执行命令(第5步)
线程池、请求队列、信号量是否占满。如果命令依赖服务的专有线程池和请求队列,或者信号量已经被占满,那么Hystrix也不会执行命令,而是转接到fallback处理逻辑(第8步).
Hystrix会根据我们编写的方法来决定采取什么样的方式去请求依赖服务
HystrixCommand.run():返回一个单一结果,或者抛出异常
HystrixObservableCommand.construct(): 返回一个Observable对象来发射多个结果或通过onError发送错误通知
Hystrix会将成功、失败、拒绝、超时等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开来对某个依赖服务的请求进行熔断。
当命令执行失败的时候,Hystrix会进入fallback尝试回退处理(服务降级),而能够引起服务降级处理的情况有下面几步
第4步:当前命令处于熔断状态,断路器是打开的时候
第5步:当前命令的线程池、请求队列、信号量被占满的时候
第6步:
HystrixObservableCommand.construct()或
HystrixCommand.run()抛出异常的时候
当Hystrix命令执行成功之后,它会将处理结果直接返回或是以Observable的形式返回
以上就是今天Hystrix的内容,谢谢阅读









