1、流程图
下图显示了当你通过 Hystrix 向服务依赖请求时会发生什么:
下面几节将更详细地解释这个流程:
- 构造一个
HystrixCommand
或HystrixObservableCcommand
对象 - 执行这个 Command
- 是否缓存了响应?
- 短路是否打开?
- 线程池/队列/信号量是否已满?
HystrixObservableCommand.construct()
或HystrixCommand.run ()
- 计算短路器健康状态
- 获取 Fallback
- 返回成功的响应
1.1 构造一个 HystrixCommand
或 HystrixObservableCcommand
对象
第一步是构造一个 HystrixCommand
或 HystrixObservableCommand
对象来表示对依赖项的请求。向构造函数传递发出请求时所需的任何参数。
如果依赖关系预期返回单个响应,则构造一个 HystrixCommand
对象。例如:
HystrixCommand command = new HystrixCommand(arg1, arg2);
如果依赖预期会返回一个 Observable
的可观察对象,那么就构造一个HystrixObservableCommand
对象。例如:
HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);
1.2 执行这个 Command
有四种方法可以执行这个命令,通过使用 Hystrix
命令对象的以下四种方法之一(前两种只适用于简单的HystrixCommand
对象,而 HystrixObservableCommand
不能使用):
execute()
– 阻塞,然后返回从依赖项接收到的单个响应(或者在出现错误时抛出异常)queue()
– 返回一个Future,你可以用它从依赖项中获取单个响应observe()
– 订阅表示来自依赖项的响应的Observable,并返回一个复制源Observable的ObservabletoObservable()
– 返回一个Observable
对象,当你订阅它时,它会执行Hystrix
命令并发出响应
K value = command.execute();
Future<K> fValue = command.queue();
Observable<K> ohValue = command.observe(); //hot observable
Observable<K> ocValue = command.toObservable(); //cold observable
同步调用 execute()
调用 queue().get()
。queue()
反过来调用 toObservable(). toblocking (). tofuture()
。也就是说,最终每个 HystrixCommand
都有一个 Observable
的实现来支持,即使是那些打算返回单个简单值的命令。
1.3 是否缓存了响应?
如果这个命令启用了请求缓存,并且该请求的响应在缓存中可用,那么这个缓存的响应将立即以一个可观察对象的形式返回。(请参阅下面的“请求缓存”。)
1.4 短路是否打开?
当你执行这个命令时,Hystrix 检查断路器,看看电路是否打开。
如果电路断开(或“触发”),Hystrix 将不会执行命令,但会将流程路由到(8)获得回退。
如果电路是闭合的,则流向(5)以检查是否有可用的容量来运行命令。
1.5 线程池/队列/信号量是否已满?
如果与该命令相关的线程池和队列(或者信号量,如果不是在线程中运行)已满,Hystrix 将不会执行该命令,而是立即将流路由到(8)获取回退。
1.6 HystrixObservableCommand.construct()
或 HystrixCommand.run ()
在这里,Hystrix通过你为此目的编写的方法调用对依赖的请求,如下之一:
HystrixCommand.run()
– 返回单个响应或抛出异常HystrixObservableCommand .construct()
– 返回一个Observable
,它会发出响应或者发送一个onError
通知
如果 run()
或 construct()
方法超过了命令的超时值,线程将抛出一个 TimeoutException
(如果命令本身没有在自己的线程中运行,则另一个计时器线程将抛出一个 TimeoutException
)。在这种情况下,Hystrix
将响应路由到(8)。获取回退,如果该方法没有取消/中断,它将丢弃最终的返回值 run()
或 construct()
方法。
请注意,没有办法强制潜在线程停止工作 —— Hystrix在JVM上能做的最好的事情是向它抛出InterruptedException
。如果Hystrix包装的工作不尊重 interruptedexception
, Hystrix
线程池中的线程将继续它的工作,尽管客户端已经收到了一个 TimeoutException
。这种行为会使 Hystrix
线程池饱和,尽管负载会“正确地释放”。大多数Java HTTP客户端库不会解释 interruptedexception
。因此,请确保正确配置HTTP客户机上的连接和读写超时。
如果命令没有抛出任何异常并返回响应,Hystrix将在执行一些日志记录和指标报告后返回此响应。在run()的例子中,Hystrix返回一个Observable,它会发出单个响应,然后发出一个onCompleted通知;在construct()的情况下,
Hystrix返回的是construct()返回的相同的可观察对象。
1.7 计算短路器健康状态
Hystrix向断路器报告成功、失败、拒绝和超时,断路器维护一组滚动的计数器来计算统计数据。
它使用这些统计数据来确定电路何时应该“跳闸”,在这一点上它会短路任何后续请求,直到恢复周期结束,在恢复周期结束后,它会在首先检查某些健康检查后再次闭合电路。
1.8 获取 Fallback
Hystrix试图恢复你的回滚命令执行失败时:当一个异常的 construct()
或 run
(6),当命令电路短路,因为打开(4),当命令的线程池和队列或信号能力(5),或者当命令已超过其超时长度。
编写 fallback()
方法,从内存缓存或通过其他静态逻辑提供无任何网络依赖的通用响应。如果必须在回退中使用网络调用,则应该通过另一个 HystrixCommand
或 HystrixObservableCommand
来实现。
在 HystrixCommand
的情况下,为了提供 fallback 逻辑,您实现了 HystrixCommand. getfallback()
,它返回一个 fallback 值。
在 HystrixObservableCommand
的例子中,为了提供 fallback 逻辑,你实现了HystrixObservableCommand.resumeWithFallback()
,它返回一个 Observable
对象,这个对象可能会返回出一个或多个 fallback 值。
如果 fallback 方法返回一个响应,那么 Hystrix 将把这个响应返回给调用者。在HystrixCommand.getFallback()
的例子中,它将返回一个 Observable
对象,该对象会发出从该方法返回的值。在HystrixObservableCommand.resumeWithFallback()
的例子中,它会返回从该方法返回的相同的 Observable
对象。
如果你还没有为 Hystrix
命令实现一个 fallback 方法,或者 fallback
方法本身抛出一个异常,Hystrix
仍然返回一个 Observable 对象,但这个对象不发出任何东西,并立即以一个 onError
通知终止。通过这个onError
通知,导致命令失败的异常被传输回调用者。(实现可能失败的回退实现是一个糟糕的实践。你应该实现你的 fallback 方法,这样它就不会执行任何可能失败的逻辑。)
失败或不存在的 fallback 方法的结果取决于你如何调用Hystrix命令:
execute()
– 抛出异常queue()
– 成功返回一个Future
,但是如果调用它的get()
方法,这个Future
将抛出异常observe()
– 返回一个Observable
对象,当你订阅它时,它会通过调用订阅者的onError
方法立即终止订阅toObservable()
– 返回一个Observable
对象,当你订阅它时,它会通过调用订阅者的onError
方法终止订阅
1.9 返回成功的响应
如果 Hystrix 命令成功,它将以一个 Observable
对象的形式返回一个或多个响应给调用者。根据你在步骤 2中调用命令的方式,这个 Observable
对象在返回给你之前可能会被转换:
- execute() :以与
.queue()
相同的方式获取一个Future
,然后在这个Future
上调用get()
来获取Observable
发出的单个值 queue()
:将Observable
对象转换为BlockingObservable
,这样它就可以被转换为Future
,然后返回这个Future
observe()
:立即订阅这个Observable
对象,并开始执行该命令的流;返回一个可观察对象,当你订阅它时,它会回放发射和通知toObservable()
:返回Observable
对象不变;为了真正开始导致命令执行的流,您必须订阅它
2、序列图
@adrianb11好心地提供了一个 序列图 来演示上述流程
3、断路器
下图展示了HystrixCommand
或 HystrixObservableCommand
如何与HystrixCircuitBreaker`` 交互,以及它的逻辑和决策流程,包括计数器在断路器中的行为。
电路开合时的精确方式如下:
- 假设断路器中的音量满足某个阈值(
HystrixCommandProperties.circuitBreakerRequestVolumeThreshold
())… - 并且假设错误百分比超过了阈值错误百分比(
HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
)… - 然后断路器从
CLOSED
转换到OPEN
。 - 当它打开时,它会短路所有针对断路器的请求。
- 在一段时间之后(
HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()
),下一个请求被允许通过(这是HALF-OPEN
状态)。如果请求失败,断路器在睡眠窗口期间返回OPEN
状态。如果请求成功,断路器转换为CLOSED
,并且执行逻辑 1 步骤。一遍又一遍。
4、隔离
Hystrix使用 bulkhead 模式来隔离彼此的依赖关系,并限制对其中任何一个的并发访问。
5、Threads & Thread Pools
客户端(库、网络调用等)在单独的线程上执行。这将它们与调用线程(Tomcat线程池)隔离开来,以便调用者可以“离开”花费太长时间的依赖调用。
Hystrix 使用独立的、每个依赖项的线程池来约束任何给定的依赖项,因此底层执行的延迟只会使池中的可用线程饱和。
您可以在不使用线程池的情况下防止失败,但这需要信任的客户机非常快地失败(网络连接/读取超时和重试配置),并始终保持良好的行为。
Netflix在Hystrix的设计中,选择使用线程和线程池来实现隔离,原因有很多,包括:
- 许多应用程序对许多不同团队开发的几十种不同服务执行几十种(有时超过100种)不同的后端服务调用。
- 每个服务都提供自己的客户端库。
- 客户端库一直在变化。
- 客户端库逻辑可以更改以添加新的网络调用。
- 客户端库可以包含重试、数据解析、缓存(内存中或跨网络)等逻辑,以及其他此类行为。
- 客户端库往往是“黑盒子”——对用户不透明的实现细节、网络访问模式、默认配置等。
- 在一些实际的生产中断中,判断结果是“噢,有些东西发生了变化,应该调整属性”或“客户机库改变了其行为”。
- 即使客户端本身没有改变,服务本身也会改变,这会影响性能特征,从而导致客户端配置无效。
- 可传递依赖项可以拉入其他未预期的、可能没有正确配置的客户端库。
- 大多数网络访问是同步执行的。
- 失败和延迟也可能发生在客户端代码中,而不仅仅是在网络调用中。
5.1线程池的好处
通过线程在它们自己的线程池中进行隔离的好处是:
- 应用程序完全受到保护,不受失控客户端库的影响。给定依赖库的池可以填满,而不会影响应用程序的其余部分。
- 应用程序可以以低得多的风险接受新的客户端库。如果发生了问题,它会被隔离到库中,不会影响到其他任何事情。
- 当一个失败的客户端重新恢复健康时,线程池将被清除,应用程序将立即恢复健康的性能,而不是在整个Tomcat容器不堪重负时进行长时间的恢复。
- 如果客户端库配置错误,线程池的健康状况将迅速证明这一点(通过增加错误、延迟、超时、拒绝等),而且您可以处理它(通常通过动态属性实时处理),而不会影响应用程序的功能。
- 如果客户服务更改性能特征(经常发生足以成为问题)进而导致需要调整属性(增加/减少超时,改变重试,等等)又可以通过线程池指标(错误、延迟、超时、拒绝),可以处理而不影响其他客户,请求,或用户。
- 除了隔离的好处之外,拥有专用的线程池还提供了内置的并发性,可以利用它在同步客户端库之上构建异步facade(类似于Netflix API在Hystrix命令之上构建响应式、完全异步的Java API)。
简而言之,线程池提供的隔离允许优雅地处理客户端库和子系统性能特征的不断变化和动态组合,而不会导致停机。
**注意:**尽管单独的线程提供了隔离,但您的底层客户端代码也应该有超时和/或响应线程中断,这样它就不会无限期地阻塞和饱和Hystrix线程池。
5.2 线程池的缺点
线程池的主要缺点是它们增加了计算开销。每个命令执行都涉及到在单独的线程上运行命令所涉及的排队、调度和上下文切换。
Netflix 在设计这一系统时,决定接受这种管理费用的成本,以换取它所提供的利益,并认为它足够小,不会对成本或性能产生重大影响。
5.3 线程的成本
Hystrix 在子线程上执行 construct()
或 run()
方法时测量延迟,以及在父线程上的总端到端时间。这样你就可以看到Hystrix开销的成本(线程,度量,日志,断路器等)。
Netflix API 使用线程隔离,每天处理 100 多亿次 Hystrix 命令执行。每个 API 实例有 40 多个线程池,每个线程有 5-20 个线程(大多数设置为10个)。
下图显示了一个 HystrixCommand
在一个 API 实例上以每秒 60 个请求的速度执行(每台服务器每秒大约 350个线程执行):
在中值(或更低)处,使用单独的线程没有成本。
在第90个百分位数,拥有单独线程的成本是 3 毫秒。
在第 99 百分位数,拥有一个单独线程的成本是 9 毫秒。但是请注意,成本的增加远远小于单独线程(网络请求)执行时间的增加,后者从 2 跳到了 28,而成本从 0 跳到了 9。
对于像这样的调用来说,这种高达 90 个百分点甚至更高的开销对于大多数 Netflix 用例来说都是可以接受的,因为它获得了弹性带来的好处。
调用,包装非常低延迟请求(比如那些主要是内存中的缓存)的开销可以过高,在这些情况下你可以使用另一种方法,比如 tryable 信号量,虽然他们不允许超时,提供大部分的弹性福利没有开销。然而,总的来说,开销很小,Netflix 在实践中通常更喜欢单独线程的隔离好处,而不是这样的技术。
5.4 Semaphores
您可以使用信号量(或计数器)来限制任何给定依赖项的并发调用数量,而不是使用线程池/队列大小。这允许Hystrix 在不使用线程池的情况下摆脱负载,但它不允许超时和退出。如果您信任客户机,并且只希望减少负载,那么您可以使用这种方法。
HystrixCommand
和 HystrixObservableCommand
在两个地方支持信号量:
fallback:当Hystrix 获取 fallback 时,它总是在调用 Tomcat 线程上这样做。
Execution : 如果设置了属性 execution.isolation.strategy
为 SEMAPHORE
。然后 Hystrix 将使用信号量而不是线程来限制调用命令的父线程并发的数量。
您可以通过定义可以执行多少并发线程的动态属性来配置这两种信号量的使用。您应该使用与调整线程池大小类似的计算来调整它们的大小(一个以亚毫秒时间返回的内存调用可以在信号量为1或2的情况下执行超过5000rps……但默认值是10)。
注意:如果依赖关系被信号量隔离,然后变得潜在,父线程将保持阻塞状态,直到底层网络调用超时。
一旦达到限制,信号量拒绝就会开始,但是填充信号量的线程不能离开。
6、Request Collapsing
您可以在 HystrixCommand
前面添加一个请求折叠器(HystrixCollapser
是抽象父类),使用它可以将多个请求折叠成一个后端依赖调用。
下图显示了两种情况下的线程和网络连接数量:首先是没有请求合并的情况,然后是请求合并的情况(假设所有连接都在很短的时间窗口内“并发”,在本例中是10ms)。
6.1 时序图
@adrianb11好心地提供了一个请求折叠的序列图。
6.2 为什么使用请求合并?
使用请求合并来减少执行并发 HystrixCommand 所需的线程和网络连接数量。请求合并以一种自动化的方式完成这一工作,它不会强迫代码库的所有开发人员协调请求的手动批处理。
6.3 全局上下文(跨所有Tomcat线程)
理想的合并类型是在全局应用程序级别完成的,这样来自任何Tomcat线程上的任何用户的请求都可以一起合并。
例如,如果您为任何用户在配置 HystrixCommand
支持批处理请求检索电影评级的依赖,当任何用户线程在同一JVM发出这样的请求,Hystrix 将增加它的请求以及任何其他人到相同的网络调用倒塌。
请注意,折叠器将向折叠的网络调用传递单个 HystrixRequestContext
对象,因此下游系统必须处理这种情况,使其成为一个有效的选项。
6.4 对象建模和代码复杂性
有时,当您创建对对象的消费者有逻辑意义的对象模型时,这与对象的生产者的有效资源利用并不匹配。
例如,给定一个包含 300 个视频对象的列表,迭代它们并在每个对象上调用 getSomeAttribute()
是一个明显的对象模型,但如果天真地实现,就会导致 300
个网络调用,每个调用都在毫秒内完成(很可能会使资源饱和)。
有一些手动的方法可以处理这个问题,比如在允许用户调用 getSomeAttribute()
之前,要求他们声明他们想要为哪些视频对象获取属性,以便它们都可以被预先获取。
或者,您可以划分对象模型,以便用户必须从一个地方获得视频列表,然后从其他地方请求该视频列表的属性。
这些方法可能导致不符合心智模型和使用模式的笨拙的 api
和对象模型。当多个开发人员在一个代码库上工作时,它们还会导致简单的错误和低效率,因为对一个用例所做的优化可能会被实现另一个用例和通过代码的新路径所破坏。
通过将合并逻辑向下推到 Hystrix
层,无论您如何创建对象模型、调用的顺序,或者不同的开发人员是否知道正在执行或甚至需要执行优化,都不再重要。
getSomeAttribute()
方法可以放在最适合它的地方,可以以适合使用模式的任何方式调用,折叠器将自动将调用批处理到时间窗口中。
6.5 请求合并的代价是什么?
启用请求合并的代价是在实际执行命令之前增加延迟。最大成本是批处理窗口的大小。
如果一个命令的平均执行时间为 5ms,而批处理窗口为 10ms,那么在最坏的情况下,执行时间可能变为15ms。通常,请求不会在窗口打开时恰好提交给窗口,因此惩罚的中值是窗口时间的一半,在本例中是5ms。
是否值得这样做取决于所执行的命令。高延迟命令不会因为少量额外的平均延迟而受到太大的影响。另外,给定命令上的并发量是关键:如果很少有超过1个或2个请求被批处理在一起,那么付出代价是没有意义的。事实上,在单线程顺序迭代中,合并将成为主要的性能瓶颈,因为每个迭代将等待 10ms 批处理窗口时间。
但是,如果一个特定的命令被大量地并发使用,并且可以批处理几十个甚至数百个调用,那么,由于 Hystrix减少了所需的线程数量和到依赖项的网络连接数量,所增加的吞吐量通常远远超过了成本。
请求合并流
7、Request Caching
HystrixCommand
和 HystrixObservableCommand
实现可以定义一个缓存键,然后用并发感知的方式在请求上下文中去重复调用。
下面是一个包含 HTTP 请求生命周期和两个在该请求中工作的线程的流程示例:
请求缓存的好处是:
- 不同的代码路径可以执行 Hystrix 命令,而不必担心重复的工作。
这在许多开发人员正在实现不同功能的大型代码库中尤其有益。
例如,代码中的多个路径都需要获取用户的Account对象,每个路径都可以这样请求它:
Account Account = new UserGetAccount(accountId).execute();
/ /或
Observable<Account> accountObservable = new UserGetAccount(accountId).observe();
Hystrix RequestCache
将执行底层的 run()
方法一次且仅一次,两个执行 HystrixCommand
的线程将接收相同的数据,尽管它们实例化了不同的实例。
- 数据检索在整个请求中保持一致。
在每次执行命令时,不是潜在地返回不同的值(或 fallback),而是缓存第一个响应,并为同一请求中的所有后续调用返回。
- 消除重复的线程执行。
由于请求缓存位于 construct()
或 run()
方法调用之前,Hystrix 可以在它们导致线程执行之前消除重复调用。
如果 Hystrix 没有实现请求缓存功能,那么每个命令都需要在 construct
或 run
方法中自己实现它,这将把它放在线程排队和执行之后。
原文地址: