Hystrix如何工作

流程图

下图展示了当你请求一个外部依赖的接口时Hystrix内部发生了什么

下面几节将更详细地解释这个流程:

  1. 构造一个HystrixCommand或HystrixObservableCommand对象
  2. 执行Command命令
  3. 是否缓存响应?
  4. 是否打开回路?
  5. 是否线程池、队列、信号量满了?
  6. HystrixObservableCommand.construct() or HystrixCommand.run()
  7. 计算回路健康状况
  8. 获取Fallback
  9. 返回成功的响应

1.构造一个HystrixCommandHystrixObservableCommand对象

首先第一步构造一个HystrixCommandHystrixObservableCommand对象来代表你的请求去请求依赖。传递构造函数在发出请求时需要的任何参数。
如果期望依赖返回一个单独的响应则像下面这样构造一个HystrixCommand对象

HystrixCommand command = new HystrixCommand(arg1, arg2);

如果期望依赖返回一个发送响应的Observable对象则像下面这样构造一个HystrixObservableCommand对象

HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);

2. 执行Command命令

有四种方法执行命令,使用Hystrix命令对象的4个方法中的一个执行(第一二种仅适用于HystrixCommand 类型的命令,都不能在HystrixObservableCommand类型的命令中使用):

  • execute() — 阻塞, 接收依赖返回的一个响应 (或者在发生错误时抛出一个异常)
  • queue() — 返回一个可以从依赖中获取一个单独的响应的Future对象
  • observe() — 订阅一个代表依赖响应的Observable 对象并且返回一个原Observable 订阅对象的复制的Observable 对象
  • toObservable() — 返回一个Observable对象当你订阅它时将会立即执行Hystrix命令并且发出它的响应
K             value   = command.execute();
Future<K>     fValue  = command.queue();
Observable<K> ohValue = command.observe();         //热启动类型的观察者对象
Observable<K> ocValue = command.toObservable();    //冷启动类型的观察者对象,在用户订阅它时才会执行对应的命令并返回响应

同步调用execute调用queue().get().queue()依次调用toObservable().toBlocking().toFuture()。也就是说最终每个HystrixCommand 命令是基于Observable对象实现的,即使那些命令想要返回一个单独的简单对象。

3. 是否缓存响应?

如果启用了请求缓存的命令,如果响应请求的缓存是有效的,那么这个缓存的响应将被立即返回一个Observable对象,后面会详细讲

4. 是否打开回路?

当执行命令时Hystrix的熔断器将会检查命令如果回路是打开的
如果回路是打开的(或者“跳闸”)那么Hystrix将不会执行命令而是路由这个请求流至Fallback兜底流程(第8步)
如果回路是关闭的那么流处理继续走到第5步的检查,如果有足够的空间则执行命令

5. 是否线程池、队列、信号量满了?

如果命令关联的线程池与队列(或者信号量,如果不是在线程中执行)满了那么Hystrix将不会执行命令而是路由这个请求流至Fallback兜底流程(第8步)

6. HystrixObservableCommand.construct() or HystrixCommand.run()

在这,Hystrix执行请求至依赖使用你已经写好的方法实现目的,实现下面方法中的一个

  • HystrixCommand.run() — 返回一个单独的响应或者抛出一个异常
  • HystrixObservableCommand.construct() — 返回一个Observable对象发出响应或者发出一个onError通知

如果run()或者construct()方法超过命令的超时配置,执行命令的线程将抛出一个TimeoutException 异常(或者分出一个定时线程,如果命令他自己不是运行在自己的线程)。这种情况Hystrix路由这个响应至Fallback兜底流程(第8步),它将抛弃最终的run()或者construct()返回值如果这两个方法没有取消cancel或中断interrupt

请注意没有方法可以强制终止潜在的线程工作 - 最好的Hystrix可以在jvm上抛出一个InterruptedException异常。如果工作线程被Hystrix包装没有遵守InterruptedExceptions,这个线程在Hystrix线程池将继续它的工作,即使客户端已经收到了超时异常TimeoutException。这个行为可能会使Hystrix线程池饱和,即使负载是‘正确的减载’。大多数Java HTTP客户端类库不能中断InterruptedExceptions异常。那么确定正确的配置连接以及读写超时在HTTP clients

如果命令没有抛出任何异常并且它返回了一个响应,Hystrix在它执行一个日志与度量报告之后返回这个响应。在run()执行的情况下,Hystrix返回一个Observable对象发出一个单独的响应并且发出一个onCompleted 通知;在construct()执行情况下,Hystrix返回相同的Observable对象。

7. 计算回路健康值

Hystrix报告成功,失败,拒绝,以及超时给熔断器,熔断器维护一个起伏的counters的set集合来计算统计指标
熔断器使用这些状态统计决定什么时候熔断需要“跳闸熔断”在一个节点,熔断器短路任何后续的请求直到一个恢复周期过去,根据第一次检查确认健康之后熔断器将关闭再次关闭回路(即恢复服务不再熔断)

8. 兜底降级处理

Hystrix回复你的降级fallback逻辑无论何时命令执行失败:当一个异常被construct()或者run()抛出时,当命令因为回路打开被短路时,当命令线程池与队列或者信号量满的时候,或者当命令超时。

写你的fallback降级去提供一个响应,没有任何的网络依赖,从一个内存缓存或者使用静态逻辑。如果你必须使用一个网络调用再fallback降级逻辑中,你应该使用另外一个命令_HystrixCommand或HystrixObservableCommand。_
使用HystrixCommand情况下,提供一个fallback降级逻辑你实现HystrixCommand.getFallback() 返回一个单独的降级值

使用HystrixObservableCommand情况下,提供一个fallback降级逻辑你实现HystrixObservableCommand.resumeWithFallback()返回一个Observable对象可以发出一个降级值或者多个值。
如果降级方法fallback返回一个响应那么Hystrix将返回这个响应给调用者。

使用HystrixCommand.getFallback()的情况下,它将返回一个Observable对象发出的从降级方法中返回值响应。

使用HystrixObservableCommand.resumeWithFallback()的情况下,它将返回同样的Observable对象从方法返回值响应。

如果你没有为你的Hystrix命令你实现一个fallback降级方法,或者这个降级方法它自己抛出一个异常,Hystrix依然会返回一个Observable对象,但是不会发出任何值并且立即中断并发出onError通知。它通过onError发通知给调用者是异常导致命令失败。(fallback兜底实现可能会失败这是一个很糟糕的实践。你应该实现fallback兜底逻辑让它不执行任何可能会失败的逻辑)

失败的结果或者不存在fallback将会有一些不同依赖于你调用的Hystrix命令

  • execute() — 抛出一个异常
  • queue() — 成功返回一个Future,但是Future将抛出一个异常如果你调用它的get()方法
  • observe() — 返回一个Observable对象,当你订阅它时,将会通过调用订阅者的onError方法立即中断业务逻辑
  • toObservable() — 返回一个Observable对象,当你订阅它时,将通过订阅者的onError方法中断业务逻辑

9. 返回成功的响应

如果Hystrix命令成功了,它将从Observable对象中返回响应或者多个响应至调用者。依赖于你如果调用命令在步骤2里面,上面的,这个Observable对象可以在它返回给你之前被转换:

  • execute() — 以与.queue()方法相同的方式获取一个Future对象然后调用Future的get()方法由Observable对象来获得一个单独的值
  • queue() — 转换Observable对象为BlockingObservable 类型,所以它也可以被转换成一个Future对象然后返回这个Future对象
  • observe() — 立即订阅Observable对象并开始执行命令流;返回一个Observable对象,当你订阅它时,重新发出值与通知
  • toObservable() — 返回未被转换变更的Observable对象;为了实际真正开始导向执行命令流你必须订阅它

时序图Sequence Diagram

@ adrianb11友好地提供了一个演示上述流程的序列图

熔断器Circuit Breaker

下图展示了一个HystrixCommand 或者HystrixObservableCommandHystrixCircuitBreaker 之间的交互和它的逻辑流程以及决策的判定,包括counters在熔断器中怎样的行为表现。


回路打开与关闭发生的准确方式如下:

  1. 假定流量通过回路达到一个确定的阈值(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())…
  2. 并且假定错误的百分比超过了错误的百分比阈值(HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())…
  3. 那么熔断器将从关闭转换为打开
  4. 当熔断器打开时,它将短路所有针对熔断器的请求
  5. 在一段时间(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds())之后, 下一个单独的请求是允许通过的(这是HALF-OPEN半开状态). 如果请求失败,熔断器在休眠窗口期间返回OPEN状态。 如果请求成功,熔断器转换为关闭状态并回到上面的逻辑1处,由逻辑1接管

隔离Isolation

非翻译内容:HystrixObservableCommand命令默认使用的是信号量模式,HystrixCommand默认实现线程池+队列模式

private HystrixCommandProperties.Setter setDefaults(HystrixCommandProperties.Setter commandPropertiesDefaults) {
    if (commandPropertiesDefaults.getExecutionIsolationStrategy() == null) {
        // default to using SEMAPHORE for ObservableCommand if the user didn't set it
        commandPropertiesDefaults.withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE);
    }
    return commandPropertiesDefaults;
}

Hystrix使用舱壁隔离模式去隔离互相之间的依赖并限制并发访问他们中的任何一个。


线程与线程池

客户端(类库,网络请求,等等)执行在分离的线程。这样隔离他们的调用线程(Tomcat线程池)那么这些调用者可以从那些耗时长的依赖调用中“离开”
Hystrix使用分离,每个依赖线程池作为一个限制方法限制任何与它的依赖,因此底层执行的延迟将仅使该池中的可用线程饱和。

你可以在不使用线程池的情况下防止故障,但是这需要客户端被信任为快速失败类型(网络连接/读取 超时以及重试配置)并始终表现良好
Netflix,在设计Hystrix中,选择使用线程与线程池来实现隔离的原因包括:

  • 很多应用执行一堆(很多时候超过100)不同的后端服务,调用针对一堆不同的服务被开发出很多不同的分组.
  • 每个服务提供它自己的客户端类库.
  • 客户端类库一直在改变.
  • 客户端类库逻辑可以改为新的网络调用.
  • 客户端类库可能包含重试逻辑,数据解析,缓存(内存或通过网络),等等类似的行为.
  • 客户端类库往往是“黑盒子” — 对于用户他们的实现细节,网络访问模式,默认配置等等不透明.
  • 在真实产品生产过程中断开,断定是“哦,某些东西变了并且属性需要调整”或者“客户端类库改变了它的行为”
  • 甚至如果客户端他自己没有变更,服务他自身可能发生了变化,变化可能影响降低了性能特性,可能引起客户端配置失效
  • 传递依赖可能会拉取那些未预期的并且可能没有正确配置的其他客户端类库
  • 大多数网络访问是同步执行的
  • 失败与延迟也可能会发生在客户端侧代码中,不仅仅是网络调用


线程池的优点Benefits of Thread Pools

由线程在他们线程池中隔离的优点有:

  • 该应用程序完全受到失控客户端库的保护。给定依赖库的池可以填满,而不会影响应用程序的其余部分。
  • 应用可以以更低的风险接受新客户端类库。如果一个问题发生,它将从类库中被隔离开并不影响其他事情.
  • 当一个是失败的客户端再次恢复健康时,线程池将清空并且应用立即恢复健康的性能,而不是在整个Tomcat容器不堪重负时进行长时间恢复。
  • 如果一个客户端类库配置错误,线程池的健康状态将立即证明这一点(通过新增的错误,延迟,超时,拒绝等等)并且你可以处理它(通常实时通过动态的属性)而不影响应用的程序功能.
  • 如果客户端服务性能发生变化(通常为发生了一个问题)反而导致需要调整属性(增加/减小 超时,改变重试次数等等)这样通过线程池度量(错误,延迟,超时,拒绝)又变得可以可见,并且可以处理它而不影响其他客户端,请求,或者用户
  • 除了隔离的优势之外,有了专用的线程池提供内建的并发性,可以被用于在同步客户端之上构建异步的表现(类似于Netflix API如何在Hystrix命令之上构建一个活性的,完全异步的Java API)

简而言之,隔离由线程池提供允许一直在变更的和动态组合的客户端类库和子系统性能特征可优雅的处理而不需要中断服务
注意:尽管隔离由一个分离出的线程提供,在你的客户端代码里也依然应该有超时和或者响应处理线程中断,那么他便不能无限期的阻塞而导致Hystrix线程池打满

线程池的缺点Drawbacks of Thread Pools

线程池主要的缺点是他们增加了计算的成本。每一个命令执行运行在一个单独的线程上包含排队的队列,调度,以及上下文切换
Netflix在设计这个系统时,决定接受花费这个成本来换得它所提供的优点并认为它足够小,不会产生大的成本或性能影响

线程开销Cost of Threads

Hystrix测量执行一个construct()或者run()方法在一个子线程的延迟与父线程上所有的端对端的时间。这种方式你可以看到Hystrix的花费的成本(线程,度量,日志,熔断器,等等)
Netfix API处理10亿+Hystrix命令执行每天使用线程隔离。每个API实例有40+线程池与5-20线程(大多数是10)
紧接着的图表代表一个HystrixCommand 被执行在60qps请求量的一个API实例上(大约总共350个线程执行每秒每服务)

在中位数请求下(与更低)分出一个线程是没有消耗的,2(User Median) - 2(Median) = 0
在90%请求下分出一个线程有3毫秒的消耗,8(User 90th) - 5(90th) = 3
在99%请求下分出一个线程有9毫秒的消耗。注意无论怎样增加的消耗是远小于分出线程(网络请求)的执行时间的增长,请求的执行时间从2到28毫秒,增加的花费是0到9毫秒
在90%分位上的开销以及更高的对于回路像大多数的Netflix用例的弹性实现的益处被认为是可以接受的
对于回路包装非常低延迟的请求(像那些基本都是命中缓存的操作)这个成本可能是比较高,这种情况下,你可以使用另一种方法像可重试的信号量,尽管他们不允许超时,提供最大的弹性优势而不需要成本。开销在通常情况下,无论怎样,Netflix在实践中通常更喜欢使用分离线程的技术来隔离的优势,开销是足够小的

信号量Semaphores

你可以使用信号量(或者计数器)来限制并发数量的调用任何依赖,来替代使用线程池+队列的方式。这样允许Hystrix减小负载而不适用线程池,但是它不允许超时与离开。如果你信任客户端并且你仅仅想要负载减小,你可以使用这个方法
HystrixCommandHystrixObservableCommand 在两点支持信号量:

  • Fallback: 当Hystrix检索到fallback降级时它总是在Tomcat线程中执行这个操作.
  • Execution: 如果你设置了属性 execution.isolation.strategySEMAPHORE 那么Hystrix将使用信号量代理线程数去限制调用命令的并发父线程数

你可以配置上面两点都使用信号量使用动态的属性定义多少并发线程可以执行。你应该设置他们的大小使用类似于当你计算线程池大小时的方法(一个内存中的调用的返回在亚毫秒内可以执行超过5000rps使用1或者2个信号量…但默认的信号量是10)
注意:如果一个依赖于一个信号量被隔离开然后变成潜在的,这个父线程将保持阻塞知道一个底层的网络调用超时。
信号量拒绝启动当到达限制之后,但是填充信号量的线程不能离开。

请求折叠合并Request Collapsing

你可以前置一个HystrixCommand 命令与一个请求折叠器(HystrixCollapser 是一个抽象的父类),你可以使用折叠器折叠多个请求成一个单独的后端依赖调用
紧接着的图展示了线程数与网络连接的两种场景:第一种没有折叠器,第二种有请求折叠器(假定所有的链接是“并发”在一个短的时间窗口内,这个案例是在10毫秒内)

时序图Sequence Diagram

@adrianb11 友好地提供了一个演示上述流程的时序图sequence diagram.

为什么使用请求折叠?

使用请求折叠可以减少线程数与网络连接需要并发执行的HystrixCommand 命令。请求折叠完全自动的方式完成这个操作不需要强制代码库的开发者去协调手工批处理请求。

全局上下文(Across All Tomcat Threads)

理想型的折叠是在全应用级别完成的,那么任何用户的请求任何tomcat线程都是可以合并在一起的。
例如,如果你配置一个HystrixCommand 命令来支持任何用户批量的请求一个检索电影评级的依赖,那么当任何用户线程在同一个JVM中发出一个这样的请求时,Hystrix将添加这个请求一起与其他请求到同一个折叠器中折叠后进行网络调用。
请注意,collapser会将单个HystrixRequestContext对象传递给折叠的网络调用,那么下游系统必须去处理这个情况才能使其成为有效的选项

用户请求上下文(Single Tomcat Thread)

如果你配置一个HystrixCommand 命令仅处理批量请求为一个单独的用户,那么Hystrix可以从一个单独的Tomcat线程(请求)中折叠请求
例如,如果一个用户想要加载300个电影对象的书签,代替执行300个网络请求,Hystrix可以合并他们至一个

对象模型与代码复杂度

有时当你创建一个对象模型并且对象的消费者具有逻辑意义时,这与对象的生产者的有效资源使用率并不能匹配。
例如,给一个300个电影对象列表,遍历他们并调用getSomeAttribute()方法每一个电影对象是一个明显的对象模型,但是如果简单的实现可能在300个网络调用里彼此全都在毫秒内返回结果(并且很可能使资源饱和,例如瞬间打满带宽或者IO等等)
有一个手工的方式可以处理这个问题,像在允许这个用户调用getSomeAttribute()之前,要求他们声明哪些对象他们想要获取属性,那么他们可以全都声明一个预提取pre-fetched。
或者,你可以切分这个对象模型,那么一个用户不得不从一个地方获取一个电影列表,然后从其他地方为这个电影列表查找属性
这些方式可能导致智力模型与使用模式不匹配的笨拙APIs和对象模型。他们也可能导致多个代码库开发者出现简单的错误和无用功,因为完成一个用例的优化可以通过另一个用例的实现与新路径(分支想法)的代码打破。
通过将折叠逻辑推送至Hystrix下一层,它不需要关心你怎样创建对象模型,什么样的调用顺序,或者是否不同的开发者是否了解正在进行的优化或者甚至需要去完成的优化。
该getSomeAttribute()方法可以被放在任何最合适的地方,任何适合方式的使用模式调用,折叠器将自动批量调用到时间窗口。(用户配置的时间窗,到达时间窗就必须要触发一个折叠请求的批量调用)

请求折叠的成本消耗?

启用请求折叠的成本消耗是在实际命令被执行之前的一个自增的延迟。最大的消耗是批处理窗口的大小(比如定义批处理的窗口为1秒,那么最大的消耗就是1秒,等到1秒延迟后便会触发折叠的请求执行)。
如果你有一个命令执行花费中位数是5毫秒,一个10毫秒的批处理窗口,在最差的情况下这个执行时间可能变成15毫秒。通常情况下请求在窗口打开时没有被提交至窗口,那么这个中值惩罚是窗口时间的一半,这个情况下是5毫秒
确定此成本是否值得依赖于正在被执行的命令。一个高延迟的命令不会受到少量额外的平均延迟的影响。此外,给定命令的并发数量是关键:如果很少有超过1或2个请求一起批处理,则无需支付罚金。事实上,在单线程顺序迭代中,折叠器将会是一个主要的性能瓶颈因为每次迭代将会等待一个10毫秒批处理窗口时间。
如果,无论怎样,一个特别的命令被重量的使用并发并可以被批处理一堆或者甚至上百个一起调用,那么由于Hystrix减少了请求到依赖所需的线程数量和网络连接数量,因此成本通常远远胜过优于通过自增吞吐量达到需求的实现

请求折叠流程图Collapser Flow


缓存请求Request Caching

HystrixCommand and HystrixObservableCommand 实现可以定义一个缓存key,然后使用这个缓存key以一个并发感知的方式在一个请求上下文内进行重复数据删除
这是一个样例流程包含一个HTTP请求的生命周期和两个线程工作在一个请求内:

缓存请求的优点是:

  • 不同的code路径可以执行Hystrix命令而不需要关心重复工作

这在许多开发人员正在实现不同功能的大型代码库中特别有用。
例如,多个代码路径都需要获取用户的Account对象可能每个请求像下面这样:

Account account = new UserGetAccount(accountId).execute();

//or

Observable<Account> accountObservable = new UserGetAccount(accountId).observe();

Hystrix请求缓存将执行底层的run()方法一次并且是仅一次,并且线程都执行HystrixCommand 命令并将会收到相同的返回数据尽管命令是被实例化为两个不同的实例

  • 在同一个请求中数据检索是始终一致的

每次执行命令不可能返回一个不同的值(或者fallback兜底),第一次响应被缓存并且作为返回值返回给其他后续的调用在同一个请求内

  • 消除重复的线程执行

当一个请求缓存在construct() or run()方法调用之前,Hystrix可以在导致线程执行之前删除重复的调用
如果Hystrix没有实现请求缓存功能,那么每个命令都需要在他们自己内部的construct or run方法中实现它,这将在线程排队并执行之后将其放入缓存

发布了81 篇原创文章 · 获赞 85 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/u010597819/article/details/95797126