hystrix怎么做的
流程图
下图显示了当您通过 Hystrix 向服务依赖项发出请求时会发生什么:
以下部分将更详细地解释此流程:
- 构造一个
HystrixCommand
或HystrixObservableCommand
对象 - 执行命令
- 响应是否缓存?
- 电路是否打开?
- 线程池/队列/信号量是否已满?
HystrixObservableCommand.construct()
或者HystrixCommand.run()
- 计算电路健康
- 获取后备
- 返回成功的响应
1.构造一个HystrixCommand
或HystrixObservableCommand
对象
第一步是构造一个HystrixCommand
或HystrixObservableCommand
对象来表示您对依赖项发出的请求。向构造函数传递发出请求时需要的任何参数。
HystrixCommand
如果期望依赖项返回单个响应,则构造一个对象。例如:
1 |
|
HystrixObservableCommand
如果期望依赖项返回一个发出响应的 Observable,则构造一个对象。例如:
1 |
|
2.执行命令
有四种方法可以执行命令,通过使用 Hystrix 命令对象的以下四种方法之一(前两种仅适用于简单HystrixCommand
对象,不适用于HystrixObservableCommand
):
execute()
— 阻塞,然后返回从依赖项接收到的单个响应(或在发生错误时抛出异常)queue()
— 返回 aFuture
,您可以使用它从依赖项中获取单个响应observe()
— 订阅Observable
表示来自依赖项的响应的 ,并返回一个Observable
复制该源的Observable
toObservable()
— 返回一个Observable
,当您订阅它时,将执行 Hystrix 命令并发出它的响应
1 |
|
同步调用execute()
调用queue().get()
. queue()
反过来调用toObservable().toBlocking().toFuture()
. 也就是说,最终每个HystrixCommand
都由一个Observable
实现支持,即使是那些旨在返回单个简单值的命令。
3. 响应是否被缓存?
如果该命令启用了请求缓存,并且缓存中有对请求的响应,则缓存的响应将立即以Observable
. (请参阅下面的“请求缓存”。)
4. 电路是否开路?
当您执行命令时,Hystrix 会检查断路器以查看电路是否打开。
如果电路打开(或“跳闸”),则 Hystrix 不会执行命令,但会将流程路由到 (8) Get the Fallback。
如果电路闭合,则流程进行到 (5) 以检查是否有容量可用于运行命令。
5. 线程池/队列/信号量是否已满?
如果与命令关联的线程池和队列(或信号量,如果未在线程中运行)已满,则 Hystrix 将不执行命令,但会立即将流路由到 (8) Get the Fallback。
6.HystrixObservableCommand.construct()
或HystrixCommand.run()
在这里,Hystrix 通过您为此目的编写的方法调用对依赖项的请求,以下方法之一:
HystrixCommand.run()
— 返回单个响应或抛出异常HystrixObservableCommand.construct()
— 返回一个发出响应或发送onError
通知的 Observable
如果run()
orconstruct()
方法超过了命令的超时值,线程将抛出一个TimeoutException
(或者一个单独的计时器线程,如果命令本身没有在它自己的线程中运行)。在这种情况下,Hystrix 通过 8 路由响应。获取 Fallback,如果该方法没有取消/中断,它会丢弃最终的返回值run()
或方法。construct()
请注意,没有办法强制潜在线程停止工作——Hystrix 在 JVM 上可以做的最好的事情就是抛出一个 InterruptedException。如果 Hystrix 包裹的工作不考虑 InterruptedExceptions,Hystrix 线程池中的线程将继续工作,尽管客户端已经收到了 TimeoutException。这种行为可能会使 Hystrix 线程池饱和,尽管负载是“正确卸载”的。大多数 Java HTTP 客户端库不解释 InterruptedExceptions。因此,请确保在 HTTP 客户端上正确配置连接和读/写超时。
如果该命令没有抛出任何异常并且它返回了响应,Hystrix 会在执行一些日志记录和指标报告后返回此响应。在 的情况下run()
,Hystrix 返回一个Observable
发出单个响应然后发出onCompleted
通知的 在construct()
Hystrix的情况下,Observable
返回由construct()
.
7.计算电路健康
Hystrix 向断路器报告成功、失败、拒绝和超时,断路器维护一组滚动的计数器来计算统计信息。
它使用这些统计数据来确定电路何时应该“跳闸”,此时它会短路任何后续请求,直到经过恢复期,然后在首先检查某些健康检查后再次关闭电路。
8. 获得后备
construct()
每当命令执行失败时,Hystrix 都会尝试恢复到您的回退:当or (6.)抛出异常run()
时,当由于电路打开 (4.) 而导致命令短路时,当命令的线程池和队列或信号量达到容量 (5.),或者当命令超过其超时长度时。
编写您的回退以从内存缓存或其他静态逻辑提供通用响应,而无需任何网络依赖。如果您必须在回退中使用网络调用,您应该通过另一个HystrixCommand
或HystrixObservableCommand
.
在 a 的情况下HystrixCommand
,提供您实现的回退逻辑,HystrixCommand.getFallback()
该逻辑返回单个回退值。
在 a 的情况下HystrixObservableCommand
,提供您实现的回退逻辑,HystrixObservableCommand.resumeWithFallback()
该逻辑返回一个可能发出一个或多个回退值的 Observable。
如果 fallback 方法返回一个响应,那么 Hystrix 将这个响应返回给调用者。对于 a HystrixCommand.getFallback()
,它将返回一个 Observable ,该 Observable 发出从该方法返回的值。在这种情况下,HystrixObservableCommand.resumeWithFallback()
它将返回从方法返回的相同 Observable。
如果你没有为你的 Hystrix 命令实现一个回退方法,或者如果回退本身抛出一个异常,Hystrix 仍然返回一个 Observable,但它什么都不发出并立即终止并发出onError
通知。正是通过这个onError
通知,导致命令失败的异常被传回给调用者。(实施可能失败的回退实现是一种糟糕的做法。您应该实施回退,使其不执行任何可能失败的逻辑。)
失败或不存在的回退的结果将根据您调用 Hystrix 命令的方式而有所不同:
execute()
— 抛出异常queue()
— 成功返回 aFuture
,但是如果调用它的方法Future
会抛出异常get()
observe()
— 返回一个Observable
,当您订阅它时,将通过调用订阅者的onError
方法立即终止toObservable()
— 返回一个Observable
,当您订阅它时,将通过调用订阅者的onError
方法终止
9. 返回成功响应
如果 Hystrix 命令成功,它将以Observable
. 根据您在上述步骤 2 中调用命令的方式,Observable
在返回给您之前可能会对其进行转换:
execute()``Future
— 以与 dos相同的方式获得 a ,.queue()
然后调用get()
它Future
以获取由 the 发出的单个值Observable
queue()
— 将 theObservable
转换为 aBlockingObservable
以便可以将其转换为 aFuture
,然后返回 thisFuture
observe()
— 立即订阅Observable
并开始执行命令的流程;返回一个Observable
,当您subscribe
访问它时,会重放排放和通知toObservable()
— 返回未Observable
更改的;您必须subscribe
这样做才能真正开始导致执行命令的流程
断路器
下图显示了 a HystrixCommand
orHystrixObservableCommand
与 a 的交互方式HystrixCircuitBreaker
及其逻辑和决策流程,包括计数器在断路器中的行为方式。
电路开闭发生的具体方式如下:
- 假设整个电路的音量满足某个阈值 (
HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
)… - 并假设错误百分比超过阈值错误百分比(
HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
)…… - 然后断路器从 转变
CLOSED
为OPEN
。 - 当它打开时,它会将针对该断路器的所有请求短路。
- 一段时间后(
HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()
),下一个请求被允许通过(这是HALF-OPEN
状态)。如果请求失败,断路器将OPEN
在睡眠窗口期间返回状态。如果请求成功,断路器转换到1.CLOSED
中的逻辑再次接管。
隔离
Hystrix 使用隔板模式来隔离彼此的依赖关系并限制对其中任何一个的并发访问。
线程和线程池
客户端(库、网络调用等)在不同的线程上执行。这将它们与调用线程(Tomcat 线程池)隔离开来,以便调用者可以“离开”耗时过长的依赖调用。
Hystrix 使用单独的、每个依赖项的线程池作为约束任何给定依赖项的一种方式,因此底层执行的延迟将仅使该池中的可用线程饱和。
您可以在不使用线程池的情况下防止失败,但这需要受信任的客户端非常快速地失败(网络连接/读取超时和重试配置)并始终表现良好。
Netflix 在其 Hystrix 的设计中,出于多种原因选择使用线程和线程池来实现隔离,包括:
- 许多应用程序针对由许多不同团队开发的数十种不同服务执行数十种(有时甚至超过 100 次)不同的后端服务调用。
- 每个服务都提供自己的客户端库。
- 客户端库一直在变化。
- 客户端库逻辑可以更改以添加新的网络调用。
- 客户端库可以包含诸如重试、数据解析、缓存(内存中或跨网络)和其他此类行为之类的逻辑。
- 客户端库往往是“黑匣子”——对于用户来说,实现细节、网络访问模式、配置默认值等都是不透明的。
- 在几次实际的生产中断中,决定是“哦,有些东西发生了变化,应该调整属性”或“客户端库改变了它的行为”。
- 即使客户端本身没有变化,服务本身也会发生变化,这会影响性能特征,进而导致客户端配置无效。
- 传递依赖可能会引入其他未预期且可能未正确配置的客户端库。
- 大多数网络访问是同步执行的。
- 失败和延迟也可能发生在客户端代码中,而不仅仅是网络调用。
线程池的好处
通过自己的线程池中的线程进行隔离的好处是:
- 该应用程序完全不受失控客户端库的影响。给定依赖库的池可以填满,而不会影响应用程序的其余部分。
- 该应用程序可以接受风险低得多的新客户端库。如果出现问题,它会与库隔离,不会影响其他所有内容。
- 当失败的客户端再次恢复健康时,线程池将清理干净,应用程序立即恢复健康的性能,而不是整个 Tomcat 容器不堪重负时的长时间恢复。
- 如果客户端库配置错误,线程池的健康状况将很快证明这一点(通过增加的错误、延迟、超时、拒绝等),您可以在不影响应用程序功能的情况下处理它(通常通过动态属性实时处理) .
- 如果客户端服务更改了性能特征(这种情况经常发生,足以成为一个问题),进而导致需要调整属性(增加/减少超时、更改重试等),这再次通过线程池指标(错误、延迟)变得可见、超时、拒绝),并且可以在不影响其他客户端、请求或用户的情况下进行处理。
- 除了隔离优势之外,拥有专用线程池还提供了内置的并发性,可用于在同步客户端库之上构建异步外观(类似于 Netflix API 如何在 Hystrix 命令之上构建反应式、完全异步的 Java API) .
简而言之,线程池提供的隔离允许客户端库和子系统性能特征的不断变化和动态组合得到妥善处理,而不会导致中断。
注意:尽管单独的线程提供了隔离,但您的底层客户端代码也应该有超时和/或响应线程中断,因此它不会无限期地阻塞并使 Hystrix 线程池饱和。
线程池的缺点
线程池的主要缺点是它们增加了计算开销。每个命令执行都涉及在单独线程上运行命令所涉及的排队、调度和上下文切换。
Netflix 在设计这个系统时,决定接受这种开销的成本以换取它提供的好处,并认为它足够小,不会对成本或性能产生重大影响。
线程成本
Hystrix 测量在子线程上执行construct()
orrun()
方法时的延迟以及在父线程上的总端到端时间。通过这种方式,您可以看到 Hystrix 开销(线程、指标、日志记录、断路器等)的成本。
Netflix API 每天使用线程隔离处理 10+ 亿次 Hystrix 命令执行。每个 API 实例有 40 多个线程池,每个线程池有 5-20 个线程(大多数设置为 10)。
下图表示HystrixCommand
在单个 API 实例上以每秒 60 个请求的速度执行(每台服务器每秒大约 350 个总线程执行):
在中位数(或更低),拥有一个单独的线程是没有成本的。
在第 90个百分位,有一个单独的线程需要 3 毫秒的成本。
在第 99个百分位,有一个单独的线程需要 9 毫秒。但是请注意,成本的增加远小于单独线程(网络请求)的执行时间增加,后者从 2 跳到 28,而成本从 0 跳到 9。
对于大多数Netflix用例而言,此类电路的 90% 或更高的开销已被认为是可以接受的,因为可以实现弹性优势。
对于包装非常低延迟请求的电路(例如那些主要命中内存缓存的请求),开销可能太高,在这些情况下,您可以使用另一种方法,例如可尝试的信号量,虽然它们不允许超时,无需开销即可提供大部分弹性优势。然而,一般来说,开销足够小,以至于 Netflix 在实践中通常更喜欢单独线程的隔离优势而不是此类技术。
信号量
您可以使用信号量(或计数器)来限制对任何给定依赖项的并发调用数,而不是使用线程池/队列大小。这允许 Hystrix 在不使用线程池的情况下减轻负载,但它不允许超时和走开。如果您信任客户端并且只想要减载,则可以使用这种方法。
HystrixCommand
并HystrixObservableCommand
在两个地方支持信号量:
- 后备:当 Hystrix 检索后备时,它总是在调用 Tomcat 线程上这样做。
- 执行:如果你设置这个属性
execution.isolation.strategy
,SEMAPHORE
那么 Hystrix 将使用信号量而不是线程来限制调用命令的并发父线程的数量。
您可以通过定义可以执行多少并发线程的动态属性来配置信号量的这两种用途。您应该使用与调整线程池大小时使用的类似计算来调整它们的大小(在亚毫秒时间内返回的内存调用可以在信号量仅为 1 或 2 的情况下执行超过 5000rps ……但默认值为 10)。
注意:如果一个依赖被信号量隔离然后变成潜在的,父线程将保持阻塞,直到底层网络调用超时。
一旦达到限制,信号量拒绝将开始,但填充信号量的线程不能走开。
请求折叠
您可以在 a 前面HystrixCommand
加上一个请求折叠器(HystrixCollapser
是抽象父级),您可以使用它将多个请求折叠到一个后端依赖调用中。
下图显示了两种情况下的线程数和网络连接数:首先没有请求折叠,然后请求折叠(假设所有连接在很短的时间窗口内“并发”,在本例中为 10 毫秒)。
为什么使用请求折叠?
使用请求折叠来减少执行并发HystrixCommand
执行所需的线程数和网络连接数。请求折叠以自动方式执行此操作,不会强制代码库的所有开发人员协调手动批处理请求。
全局上下文(跨所有 Tomcat 线程)
理想的折叠类型是在全局应用程序级别完成的,因此来自任何 Tomcat 线程上的任何用户的请求都可以折叠在一起。
例如,如果您将 a 配置HystrixCommand
为支持任何用户对检索电影评级的依赖项的请求进行批处理,那么当同一 JVM 中的任何用户线程发出此类请求时,Hystrix 会将其请求与任何其他请求一起添加到同一个折叠的网络通话。
请注意,折叠器会将单个 HystrixRequestContext 对象传递给折叠的网络调用,因此下游系统必须处理这种情况才能成为有效的选项。
用户请求上下文(单个 Tomcat 线程)
如果您将 a 配置HystrixCommand
为仅处理单个用户的批处理请求,则 Hystrix 可以折叠来自单个 Tomcat 线程(请求)的请求。
例如,如果用户想要为 300 个视频对象加载书签,而不是执行 300 个网络调用,Hystrix 可以将它们全部合并为一个。
对象建模和代码复杂性
有时,当您创建对对象的消费者具有逻辑意义的对象模型时,这与对象生产者的有效资源利用不匹配。
例如,给定一个包含 300 个视频对象的列表,遍历它们并调用getSomeAttribute()
每个对象是一个明显的对象模型,但如果实施得天真,可能会导致 300 个网络调用都在几毫秒内进行(并且很可能会导致资源饱和)。
您可以通过手动方式处理此问题,例如在允许用户调用之前getSomeAttribute()
,要求他们声明他们想要获取哪些视频对象的属性,以便它们都可以被预取。
或者,您可以划分对象模型,以便用户必须从一个地方获取视频列表,然后从其他地方请求该视频列表的属性。
这些方法可能会导致笨拙的 API 和对象模型与心智模型和使用模式不匹配。当多个开发人员在代码库上工作时,它们还可能导致简单的错误和效率低下,因为为一个用例完成的优化可能会被另一个用例的实现和代码中的新路径破坏。
通过将折叠逻辑下推到 Hystrix 层,您如何创建对象模型、以什么顺序进行调用、或者不同的开发人员是否知道正在完成甚至需要完成的优化都无关紧要。
该getSomeAttribute()
方法可以放在最适合的地方,并以适合使用模式的任何方式调用,并且折叠器将自动将调用批处理到时间窗口中。
请求崩溃的成本是多少?
启用请求折叠的代价是在执行实际命令之前增加了延迟。最大成本是批处理窗口的大小。
如果您有一个执行中位数为 5 毫秒的命令,以及一个 10 毫秒的批处理窗口,那么在最坏的情况下执行时间可能会变为 15 毫秒。通常,请求不会恰好在窗口打开时被提交到窗口,因此中值惩罚是窗口时间的一半,在本例中为 5 毫秒。
该成本是否值得的确定取决于正在执行的命令。高延迟命令不会因少量额外的平均延迟而受到太大影响。此外,给定命令的并发量是关键:如果要批处理的请求很少超过 1 或 2 个,那么付出代价是没有意义的。事实上,在单线程顺序迭代中,崩溃将是一个主要的性能瓶颈,因为每次迭代都会等待 10 毫秒的批处理窗口时间。
然而,如果一个特定的命令被大量并发使用并且可以同时批处理数十甚至数百个调用,那么成本通常远远超过所获得的吞吐量增加,因为 Hystrix 减少了它所需的线程数和网络连接数依赖关系。
折叠器流
请求缓存
HystrixCommand
并且HystrixObservableCommand
实现可以定义一个缓存键,然后用于以并发感知的方式在请求上下文中对调用进行重复数据删除。
这是一个示例流程,涉及 HTTP 请求生命周期和在该请求中工作的两个线程:
请求缓存的好处是:
- 不同的代码路径可以执行 Hystrix 命令,而不用担心重复工作。
这在许多开发人员正在实现不同功能的大型代码库中特别有用。
例如,所有需要获取用户Account
对象的代码的多个路径都可以像这样请求它:
1 |
|
HystrixRequestCache
将执行底层run()
方法一次且仅一次,并且执行该方法的两个线程HystrixCommand
将接收相同的数据,尽管实例化了不同的实例。
- 数据检索在整个请求中是一致的。
不是每次执行命令时都可能返回不同的值(或回退),而是缓存第一个响应并为同一请求中的所有后续调用返回。
- 消除重复的线程执行。
由于请求缓存位于construct()
orrun()
方法调用之前,Hystrix 可以在调用导致线程执行之前对其进行重复数据删除。
如果 Hystrix 没有实现请求缓存功能,那么每个命令都需要在construct
orrun
方法中自己实现它,这将把它放在线程排队并执行之后。