本文主要讲述Hystrix的工作流程、断路器原理以及线程池和信号量隔离。
Hystrix工作流程
下图是来自Netflix Hystrix官方的流程图,该图展示了当一个请求调用了相关服务依赖之后Hystrix是如何工作的。
1.构建HystrixCommand或是HystrixObservableCommand对象
构建一个HystrixCommand或是HystrixObservableCommand对象,用来表示对依赖服务的操作请求,同时传递所有需要的参数。
- HystrixCommand:用在依赖的服务返回单个操作结果的时候
- HystrixObservableCommand:用在依赖的服务返回多个操作结果的时候
2.命令执行
从图中我们可以看到4中命令的执行方式
- execute(): 同步执行,从依赖的服务返回一个单一的结果对象(或是在发生错误的时候抛出异常)。
- queue(): 异步执行,返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。
- observe(): 返回Observable对象,代表了操作的多个结果,是一个Hot Observable。
- toObservable(): 返回Observable对象,代表了操作的多个结果,是一个Code Observable。
其中execute()
是通过queue().get()
来实现同步执行的。queue()
是通过toObservable().toBlocking().toFuture()
来实现。
Hot Observable不论"事件源"是否有"订阅者",都会在创建后对事件进行发布;Cold Observable在没有“订阅者”的时候并不会发布事件,而是进行等待,直到有“订阅者”之后才发布事件。
3.结果是否被缓存
若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以Observable对象的形式返回。
4.断路器是否打开
Hystrix在执行命令前需要检查断路器是否为打开状态:
- 如果断路器是打开的,那么Hystrix不会执行命令,而是路由到fallback处理逻辑(对应下面的第8步)
- 如果断路器是关闭的,则执行第5步,检查是否有可用资源来执行命令。
5.线程池/请求队列/信号量是否占满
如果与命令相关的线程池和请求队列,或者信号量已经被占满,那么Hystrix也不会执行命令,而是路由到fallback处理逻辑(对应下面第8步)。
这里Hystrix所判断的线程池并非容器的线程池,而是每个依赖服务的专有线程池。
6.HystrixObservableCommand.construct()或者HystrixCommand.run()
Hystrix根据我们编写的方法来决定采取什么样的方式去请求依赖服务。
- HystrixCommand.run(): 返回一个单一的结果,或抛出异常
- HystrixObservableCommand.construct(): 返回一个Observable对象来发布多个结果,或通过onError发送错误通知。
如果run()
或construct()
方法的执行时间超过了命令设置的超时阈值,当前处理线程将会抛出一个TimeoutException(如果该命令不在其自身线程中执行,则会通过单独的计时线程来抛出)。在这种情况下,Hystrix会路由到fallback处理逻辑(对应下面第8步)。同时,如果当前命令没有被取消或中断,那么它最终会忽略run()
或者construct()
方法的返回。
如果命令没有抛出异常并返回了结果,那么Hystrix在记录一些日志并采集监控报告之后将该结果返回。在使用run()
的情况下,Hystrix会返回一个Observable,它发布单个结果并产生onCompleted的结束通知;而在使用construct()
的情况下,Hystrix会直接返回该方法产生的Observable对象。
7.计算断路器的健康度
Hystrix会将“成功”,“失败”,“拒绝”,“超时”等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。
Hystrix会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行“熔断/短路”,直到会恢复期结束。在恢复期结束后,如果第一次健康检查通过,则会关闭断路器,否则继续保持打开状态。
8.fallback处理
当命令执行失败的时候,Hystrix会进入fallback尝试回退处理,该操作也被称为“服务降级”。能够引起服务降级处理的情况如下:
- 第4步,当前命令处于“熔断/短路”状态,断路器是打开的时候。
- 第5步,当前命令的线程池、请求队列或者信号量被占满的时候。
- 第6步,HystrixObservableCommand.construct()或HystrixCommand.run()抛出异常的时候。
在服务降级逻辑中,我们需要实现一个通用的响应结果,并且该结果的处理逻辑不能依赖网络请求获取。如果一定要在降级逻辑中包含网络请求,那么该请求也必须被包装在HystrixCommand或是HystrixObservableCommand中。
- 当使用HystrixCommand的时候,通过实现
HystrixCommand.getFallback()
来实现服务降级逻辑,将返回单个降级结果。 - 当使用HystrixObservableCommand的时候,通过
HystrixObservableCommand.resumeWithFallback()
实现的时候,该方法会返回一个Observable对象来发射一个或多个降级结果。
当命令的降级逻辑返回结果之后,Hystrix就将该结果返回给调用者。如果降级执行失败,Hystrix会根据不同的执行方法做出不同的处理。
- execute(): 抛出异常
- queue(): 返回Futue对象,但是当调用get()来获取结果的时候会抛出异常。
- observe(): 返回Observable对象,当订阅它的时候,将立即通过调用订阅者的onError方法来通知中止请求。
- toObservable(): 返回Observable对象,当订阅它的时候,将通过调用订阅者的onError方法来通知中止请求。
9.返回成功的响应
当Hystrix命令执行成功后,它会将处理结果直接返回或是以Observable的形式返回。具体哪种方式取决于第二步中提到的4种命令执行方式,下图总结了这四种调用方式之间的依赖:
- execute(): 在
queue()
产生异步结果Future
对象之后,通过调用get()
方法阻塞并等待结果的返回。 - queue(): 将t
oObservable()
产生的Observable
转换成BlockingObservable
,然后转换成Futue
,之后返回Futue
。 - observe(): 在
toObservable()
产生Observable
后立即订阅它,让命令能够马上开始异步执行,并返回一个Observable
对象。当调用它的subscribe时,将重新产生结果和通知给订阅者。 - toObservable(): 返回
Observable
对象,必须通过订阅他才会真正触发命令的执行流程。
尽管Hystrix命令的响应总是以Observable的形式来返回,但是它可以被转换成你需要的使用方式来进行命令调用。
断路器原理
下图显示了HystrixCommand或HystrixObservableCommand如何与hystrixcircuit断路器交互,以及它的逻辑和决策流程,包括计数器在断路器中的行为。
断路器运作的方式如下:
- 假设通过一个回路的请求量达到某个阈值(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())…
- 假设错误百分比超过阈值错误百分比(hystrixcommandproperties .circuit breakererrorthreshold oldpercentage())…
- 上面两个条件只要满足一个,断路器的状态就会从close切换到open。
- 当断路器的状态是open,它会短路所有针对断路器的请求。
- 经过一段时间后(hystrixcommandproperties .circuit breakersleepwindowin毫秒()),下一个请求被允许通过(这是半打开状态)。如果请求失败,断路器在休眠窗口期间返回到打开状态。如果请求成功,断路器状态有open切换到close,然后又开始从第一步判断,循环往复。
隔离
线程池隔离
Hystrix使用“舱壁模式”来实现线程池的隔离,它会为每一个依赖服务创建一个独立的线程池,这样即使某个依赖服务出现问题,也只是对该依赖服务的调用产生影响,而不会拖慢其他的依赖服务。
通过实现对依赖服务的线程池隔离,可以带来如下优势:
- 应用程序自身得到完全保护,不会受不可控的依赖服务影响。即使给依赖服务分配的线程池被填满,也不会影响应用程序的其余部分。
- 有效降低新服务的接入风险。如果新接入的服务出现问题,则将其隔离起来而不会影响其他所有内容。
- 当依赖的服务从失败恢复正常后,线程池会被清空并且应用程序也能立即恢复健康的性能,而不是在整个Tomcat容器不堪重负时进行长时间的恢复。
- 如果应用服务本身配置错误,线程池会快速反映出该问题(通过新增加的错误、延迟、超时、拒绝等)。并且可以在不影响应用程序的情况下处理它(通常是通过动态属性实时处理)。
- 如果应用服务需要调整性能数据,而性能的调整需要调优属性(增加/减少超时,修改重试机制等),那么通过线程池的监控指标信息(错误、延迟、超时、拒绝)可以快速反映出此问题,并且可以在不影响其它服务、请求或者用户的情况下进行处理。
- 除了隔离的好处之外,每个专有的线程池都提供了内置的并发实现,可以利用并发性为同步的应用服务上构建异步访问。(类似于Netflix API如何在HystrixCommand上构建响应性的、完全异步的Java API)。
总之,通过对依赖服务使用线程池隔离,我们可以让应用变得更加灵活,可以在不停止服务的情况下,配合动态配置实现性能参数的调整。
线程池隔离的缺点
线程池的主要缺点是增加了计算开销。每个命令的执行都涉及到在单独的线程上运行命令所涉及的队列、调度和上下文切换。
Netflix在设计这个系统时,决定接受这种开销的成本,以换取它所提供的好处,并认为它足够小,不会对成本或性能产生重大影响。
信号量隔离
在Hystrix中除了可以使用线程池外,还可以使用信号量来控制单个依赖服务的并发量。信号量的额开销远比线程池开销小,但是它不能设置超时和实现异步访问,所以只有在依赖服务足够可靠的情况才使用信号量。
在HystrixCommand和HystrixObservableCommand中有两处支持信号量的使用:
- 命令执行: 如果将隔离策略参数
execution.isolation.strategy
设置为SEMAPHORE,Hystrix会使用信号量替代线程池来控制依赖服务的并发。 - 降级逻辑: 当Hystrix尝试降级逻辑时,它会在调用线程中使用信号量。
我们可以通过定义可以执行多少并发线程的动态属性来配置这两种信号量的使用。对于信号量大小的估算方法与线程池并发度的估算类似。以1毫秒为单位返回的内存调用,在信号量仅为1或2的情况下可以执行5000rps以上的性能(默认值是10)。我们可以按照此标准并根据实际请求耗时来设置信号量。下图是从官方截取的关于两种隔离方式的使用:
参考资料
- https://github.com/Netflix/Hystrix/wiki/How-it-Works
- Spring Cloud微服务实战 翟永超著