Hystrix是什么
在分布式环境中,许多服务依赖项中的一些不可避免地会失败。Hystrix是一个库,它通过添加延迟容忍和容错逻辑来帮助您控制这些分布式服务之间的交互。Hystrix通过隔离服务之间的访问点,停止它们之间的级联故障,并提供回退选项来实现这一点,所有这些都提高了系统的整体弹性。
Hystrix是做什么的
-
对通过第三方客户端库访问的依赖项(通常通过网络)的延迟和故障提供保护和控制。
-
停止复杂分布式系统中的级联故障。
-
失败后快速恢复。
-
如果可能的话,回退并优雅地降级。
-
启用接近实时的监视、警报和操作控制。
Hystrix解决了什么问题
在复杂的分布式体系结构中的应用程序有几十个依赖项,每个依赖项在某一时刻都不可避免地会失败。如果主机应用程序不能与这些外部故障隔离,它就有可能随着这些外部故障而宕机。
例如,对于一个依赖于30个服务的应用程序,每个服务都有99.99%的正常运行时间,以下是你可以期待的:
99.9930 = 99.7% 正常运行时间
10亿次请求的0.3% = 300万次失败
2个小时以上的停机时间/月,即使所有依赖有良好的正常运行时间。
现实情况通常更糟
即使所有依赖项都表现良好,如果您没有为整个系统进行弹性设计,那么即使0.01%的停机时间对数十个服务中的每个服务造成的总体影响也相当于一个月可能停机几个小时。
当许多后端系统中的一个不可用时,它可以阻塞整个用户请求:
在高流量的情况下,单个后端依赖关系不可用时,可能会导致所有服务器上的所有资源在数秒内饱和。
应用程序中可能会导致网络请求通过网络或客户端库传播的每个点都是潜在故障的根源。比故障更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,从而备份队列(backs up queues)、线程和其他系统资源,从而导致跨系统发生更多的级联故障。
当通过第三方客户端执行网络访问时,这些问题会更加严重。第三方客户端是一个“黑盒”,实现细节被隐藏,可以随时更改,每个客户端库的网络或资源配置不同,通常很难监控和更改。
更糟糕的是,传递依赖项执行潜在的昂贵或容易出错的网络调用,而应用程序没有显式地调用它们。
网络连接失败或降级。服务和服务器故障或变慢。新的库或服务部署会改变行为或性能特征。客户端库有bug。
所有这些都表示需要隔离和管理的故障和延迟,以便单个故障依赖项不会导致整个应用程序或系统停机。
Hystrix的设计原则是什么?
-
防止任何单一依赖耗尽所有容器(如Tomcat)用户线程。
-
减少负载和快速失败,而不是排队。
-
在可行的情况下提供后备方案以保护用户不受故障的影响。
-
使用隔离技术(如舱壁(bulkhead)、泳道(swimlane)和断路器(circuit breaker)模式)来限制任何一个依赖项的影响。
-
通过接近实时的度量、监视和警报来优化发现时间
-
通过配置更改的低延迟传播和Hystrix在大多数方面的动态属性更改来优化恢复时间,这允许您通过低延迟反馈循环进行实时操作修改。
-
防止在整个依赖客户端执行过程中发生故障,而不仅仅是在网络流量中。
Hystrix如何实现其目标?
-
将所有对外部系统(或“依赖”)的调用包装到HystrixCommand或hystrixobservableccommand对象中,这些对象通常在单独的线程中执行(这是command模式的一个例子)。
-
超时调用的时间超过您定义的阈值。这是默认的,但是对于大多数依赖项,您通过“属性”自定义设置这些超时,因此它们略高于每个依赖项的99.5%的性能。
-
为每个依赖项维护一个小线程池(或信号量);如果该依赖项已满,那么针对该依赖项的请求将立即被拒绝,而不是排队等待。
-
度量成功、失败(客户端抛出的异常)、超时和线程拒绝。
-
触发断路器,在一段时间内停止对某一特定服务的所有请求,如果该服务的错误率超过阈值,可以手动或自动停止。
-
在请求失败、被拒绝、超时或短路时执行回退逻辑。
-
以接近实时的方式监控度量和配置更改。
当您使用Hystrix包装每个底层依赖项时,上面图表中所示的架构会改变为类似于下面的图表。每个依赖都是相互隔离的,当延迟发生时,它可能饱和的资源受到限制,并在回退逻辑中涵盖,决定在依赖中发生任何类型的故障时做出何种响应:
Hystrix如何工作
流程图
下图显示了当你通过Hystrix向服务依赖请求时会发生什么:
-
构造一个
HystrixCommand
或Hystrixobservableccommand
对象 -
执行命令
-
是否缓存了响应?
-
断路器是否打开?
-
线程池/队列/信号量是否已满?
-
HystrixObservableCommand.construct()
或HystrixCommand.run ()
-
计算断路器是否健康
-
执行Fallback逻辑
-
返回成功的响应
断路器(Circuit Breaker)
下图展示了HystrixCommand
或Hystrixobservableccommand
如何与HystrixCircuitBreaker
交互,以及它的逻辑和决策流程,包括计数器在断路器中的行为。
Hystrix是基于滚筒式来处理,每一秒会产生一个buckets,每产生一个新的buckets就会移除一个最老的buckets,默认是10秒一个窗口。buckets在内存中就是一种数据结构,每个buckets会记录Metrics的相关数据,比如成功、失败、超时、拒绝。
当一个HystrixCommand进来后,会先通过allowRequest()方法判断是否允许通过该次请求,allowRequest()方法会通过isOpen判断断路器是否打开。
断路器关闭,则允许通过该次请求;断路器打开,则会判断是否过了睡眠周期。没有过睡眠周期则返回false,拒绝通过该次请求,过了睡眠周期则会尝试放行。
isOpen()方法会按照(failure) / (success+failure)
公式计算出失败率,如果失败率大于阈值,则会触发熔断。公式中的成功、失败的数据就来源于每10秒中一个窗口的滚筒数据。
对于一个依赖调用,要么调用成功,要么调用失败(包括异常、超时、拒绝),这些调用结果都会记录到buckets中。对于调用成功结果来说,还会判断断路器开关是否打开,如果是打开状态的话,则会关闭断路器并重置相关的计数器。
断路器打开闭合的精确计算方式:
-
假设电路中的音量满足某个阈值(
HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
) -
并且假设错误百分比超过了阈值错误百分比(
HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
-
断路器打开
CLOSED
->OPEN
-
当它打开时,它会短路所有针对断路器的请求。
-
在一段时间之后(
Hystrixcommandproperties . circuitbreakersleepwindowwinmilliseconds()
),下一个请求被允许通过(这是半打开状态)。如果请求失败,断路器在睡眠窗口期间返回打开状态(OPEN
)。如果请求成功,断路器转换为CLOSED
,执行步骤1.
隔离
Hystrix使用壁仓(bulkhead)模式来隔离彼此的依赖关系,并限制对其中任何一个的并发访问。
线程 & 线程池
客户端(库、网络调用等)在单独的线程上执行。这将它们与调用线程(Tomcat线程池)隔离开来,以便调用者可以“walk away”花费长时间来调用依赖。
Hystrix使用独立的、一定大小的(per-dependency)线程池来约束任何给定的依赖项,因此底层执行的延迟只会使池中的可用线程饱和。
您可以在不使用线程池的情况下防止失败,但这需要信任的客户机非常快地失败(网络连接/读取超时和重试配置),并始终保持良好的行为。
Netflix在Hystrix的设计中,选择使用线程和线程池来实现隔离,原因有很多,包括:
-
许多应用程序对许多不同团队开发的几十种不同服务执行几十种(有时超过100种)不同的后端服务调用。
-
每个服务都提供自己的客户端库。
-
客户端库一直在变化。
-
客户端库逻辑可以更改以添加新的网络调用。
-
客户端库可以包含重试、数据解析、缓存(内存中或跨网络)等逻辑,以及其他此类行为。
-
客户端库往往是“黑盒子”——对用户不透明的实现细节、网络访问模式、默认配置等。
-
在一些实际的生产中断中,判断结果是“噢,有些东西发生了变化,应该调整属性”或“客户机库改变了其行为”。
-
即使客户端本身没有改变,服务本身也会改变,这会影响性能特征,从而导致客户端配置无效。
-
可传递依赖项可以拉入其他未预期的、可能没有正确配置的客户端库。
-
大多数网络访问是同步执行的。
-
失败和延迟也可能发生在客户端代码中,而不仅仅是在网络调用中。
线程池的好处
通过线程在它们自己的线程池中进行隔离的好处是:
-
应用程序完全受到保护,不受失控客户端库的影响。给定依赖库的池可以填满,而不会影响应用程序的其余部分。
-
应用程序可以以低得多的风险接受新的客户端库。如果发生了问题,它会被隔离到库中,不会影响到其他任何事情。
-
当一个失败的客户端重新恢复健康时,线程池将被清除,应用程序将立即恢复健康的性能,而不是在整个Tomcat容器不堪重负时进行长时间的恢复。
-
如果客户端库配置错误,线程池的健康状况将迅速证明这一点(通过增加错误、延迟、超时、拒绝等),而且您可以处理它(通常通过动态属性实时处理),而不会影响应用程序的功能。
-
如果客户服务更改性能特征(经常发生足以成为问题)进而导致需要调整属性(增加/减少超时,改变重试,等等)又可以通过线程池指标(错误、延迟、超时、拒绝),可以处理而不影响其他客户,请求,或用户。
-
除了隔离的好处之外,拥有专用的线程池还提供了内置的并发性,可以利用它在同步客户端库之上构建异步facade(类似于Netflix API在Hystrix命令之上构建响应式、完全异步的Java API)。
简而言之,线程池提供的隔离允许优雅地处理客户端库和子系统性能特征的不断变化和动态组合,而不会导致停机。
**注意:**尽管单独的线程提供了隔离,但您的底层客户端代码也应该有超时和/或响应线程中断,这样它就不会无限期地阻塞和饱和Hystrix线程池。
线程池的缺点
线程池的主要缺点是它们增加了计算开销。每个命令执行都涉及到在单独的线程上运行命令所涉及的排队、调度和上下文切换。
Netflix在设计这一系统时,决定接受这种管理费用的成本,以换取它所提供的利益,并认为它足够小,不会对成本或性能产生重大影响。
信号量
您可以使用信号量(或计数器)来限制任何给定依赖项的并发调用数量,而不是使用线程池/队列大小。这允许Hystrix在不使用线程池的情况下摆脱负载,但它不允许超时和退出。如果您信任客户机,并且只希望减少负载,那么您可以使用这种方法。
HystrixCommand
和Hystrixobservableccommand
在两个地方支持信号量:
- Fallback: 当Hystrix执行Fallback时,它总是在调用Tomcat线程上执行。
- Execution: 如果您将属性
execution.isolation.strategy
设置为SEMAPHORE
,那么Hystrix将使用信号量而不是线程来限制调用该命令的父线程并发的数量。
您可以通过定义可以执行多少并发线程的动态属性来配置这两种信号量的使用。您应该使用与调整线程池大小类似的计算来调整它们的大小(一个以sub-millisecond时间返回的内存调用可以在信号量为1或2的情况下执行超过5000rps……但默认值是10)。
注意:如果依赖关系被信号量隔离,然后变得潜在(latent),父线程将保持阻塞状态,直到底层网络调用超时。
一旦达到限制,信号量拒绝就会开始,但是填充信号量的线程不能离开。
请求和并
不经常使用
结果缓存
不经常使用
如何配置和调优断路器
部署新应用的典型方法是使用经验的配置(超时/线程/信号量)将其发布到生产环境中,然后在看到它在峰值生产周期中运行后将其调优为更严格的配置。
在实践中,这通常看起来是:
-
保留默认的1000ms超时,除非知道需要更多的时间。
-
保持线程池默认为10个线程,除非知道需要更多线程。
-
使用金丝雀部署,如果一切顺利,则继续。
-
投入生产24小时
-
依赖标准的告警和监控来捕捉问题(如果有的话)。
-
24小时后,使用延迟百分比和流量来计算对断路器有意义的最低配置值。
-
在生产中实时更改这些值,并使用实时仪表板对其进行监视,直到您放心为止。
-
仅当断路器的行为或性能特征发生变化并通过警报和/或仪表板监视引起您注意时,才再次查看该断路器的配置。
下图展示了如何选择线程池、队列和执行超时(或信号量大小)的典型思维过程:
对于大多数电路,您应该尝试将它们的超时值设置为正常运行系统的99.5%,这样它们就可以切断坏请求,不让它们占用系统资源或影响用户行为。
您必须调整线程池和队列的大小,使它们只占整个应用程序资源的一小部分,否则它们将无法防止依赖关系使可用资源饱和。
关于配置和调谐电路的重要事情是:
- 您应该在生产环境中根据真实的流量模式进行调优。
- 您可以轻松地实时调整设置,同时监视以查看不同设置的影响。
预期抖动(jitter)和故障
Hystrix以毫秒为单位测量和报告指标。 这里"抖动(jitter)"的定义是—超时、线程池拒绝、处理任务速度减慢以及其他类似情况。 在大型集群中,尤其对于大容量电路,通常在任何特定时间都会发生其中一些情况。
在这张来自Netflix API仪表板的截图中,你可以看到橙色和紫色的数字,这些数字在表示243台服务器的10秒统计窗口中显示了少量请求发生的超时和线程池拒绝。
一些导致抖动的原因:
-
客户端机器垃圾收集(您的机器在请求的中间进行垃圾收集)
-
服务机器垃圾收集(远程服务器在向它发出请求时执行垃圾收集)
-
网络问题
-
不同的请求参数有不同的负载大小
-
缓存没有命中
-
突发流量
-
新机器启动(部署、自动扩展事件)和“热身”
When Things Are Latent
如果你注意到了延迟,不要急于重新配置。如果Hystrix命令正在减轻负载,那么它就在做它应该做的事情。
最初,Hystrix在Netflix上被采用时,当电路Latent时,电路倾向于动态更改属性以增加线程池、队列、超时等,“尝试并为其提供喘息的空间”,让它重新工作时,这是一种常见的反应。但这与你应该做的相反。如果为正常运行的系统配置了正确的命令,并且该命令现在正在拒绝、超时和/或短路(short-circuiting),那么您应该集中精力解决根本原因,而不是去修改断路器的配置。
不要犯这样的错误,即在响应时给命令提供更多的资源以满足它的需要。
例如,假设您有一个由100个服务器组成的集群,每个服务器允许有10个并发连接到一个服务,即:1000个可能的并发连接。在健康的情况下,它通常在任何给定时间使用200-300个。如果出现延迟并导致他们全部被挂起(backs them all up),那么现在将使用1000个连接。10个连接对于客户来说似乎不多,所以让我们尝试增加到20个,对吗? 很可能10个是饱和的,20个也会变成饱和的。现在有2000个连接对后端开放,这使情况变得更糟。
这是断路器存在的原因之一,即“释放底层系统上的压力”以使它们恢复,而不是在重试循环,挂起的连接等中对它们施加更多请求。
例如,这是单个依赖项经历延迟的示例,导致延迟足够高,从而导致断路器在群集的大约三分之一上跳闸。 它是系统中唯一存在运行状况问题的电路,Hystrix会在出现延迟问题时阻止其占用其他资源。
简而言之,让系统释放负载,短路,超时和拒绝,直到底层系统恢复健康,并在Hystrix层恢复健康。Hystrix正是为这种场景而设计的,其重点是减少潜在系统的资源利用,通过隔离大多数资源并远离那些挂起在潜在连接上的资源,从而快速恢复。
依赖失败是什么样的
分布式系统中最典型的故障类型是单个依赖项出现故障或潜伏,而其他所有依赖项都保持健康状态。在这些情况下,指标和仪表板会非常明显地显示正在发生的事情:
上面的截图显示了一个错误率为20%的电路:高到足以产生影响,但不足以开始跳闸断路器。其他三条线路则不受影响。
在这个特定的例子中,导致问题的是实际错误,而不是延迟——如红色数字所示,而不是橙色。
服务降级
这是另一个影响单个电路的事故的截图。请注意,99.5%的延迟非常高。这是底层工作线程完成所需的时间,这会使线程池饱和并导致调用线程超时。
集群中除了一台机器之外的所有机器都触发了断路器,这导致了大多数通信发生短路(蓝色),而在一台仍在尝试的机器上,大多数请求都超时了(橙色)。
请注意,其他电路运行良好,左边的折线图显示返回值没有增加500,因为该电路返回了一个回退,因此用户得到的是降级但仍然有效的体验。
级联依赖失败
其他问题
两个可能导致这种情况的系统问题的例子是:
-
系统过载(高平均负载、CPU占用率等)
- 这种情况可能发生的一个例子是,自动伸缩策略失败或伸缩速度不够快,导致流量激增,机器接收的流量超过了它们的处理能力。
-
内存泄漏最终导致GC抖动,GC抖动会窃取CPU并导致暂停,而暂停又会导致电路超时、备份和拒绝