前言
最近在工作中参与组内服务稳定性建设,梳理我们目前服务现状并接入公司自研稳定性保障平台。对公司内自研组件以及业界流行的Hystrix做了学习,Netflix Hystrix 里面大量RxJava响应式实现,实在看着有点绕。所以在这里梳理一些实践以及Hystrix知识点。
为什么要做这个事情
服务的稳定是公司可持续发展的重要基石,随着业务量的快速发展,一些平时正常运行的服务,会出现各种突发状况,而且在分布式系统中,每个服务本身又存在很多不可控的因素,比如线程池处理缓慢,导致请求超时,资源不足,导致请求被拒绝,又甚至直接服务不可用、宕机、数据库挂了、缓存挂了、消息系统挂了…
对于一些非核心服务,如果出现大量的异常,可以通过技术手段,对服务进行降级并提供有损服务,保证服务的柔性可用,避免引起雪崩效应。
例如:一个依赖30个服务的系统,每个服务99.99%可用,99.99%的30次方 ≈ 99.7% ,0.3% 意味着1亿次请求会有 3,000,00次失败 ,换算成时间大约每月有2个小时服务不稳定,随着服务依赖数量的变多,服务总体可用性会变得更差。
假设我们当前服务的外部依赖中,有一个服务出现了故障,可能是网络抖动出现了超时,亦或服务挂掉导致请求超时,短时间内看起来像下图这样:
慢慢的大量业务线程都会阻塞在对故障服务的调用上,请求排队,服务响应缓慢,系统资源渐渐消耗,最终导致服务崩溃,更可怕的是这种影响会持续的向上传递,进而导致服务雪崩。
如何去做
- 消除依赖: 梳理去除、隔离。 比如系统尽量减少第三方依赖;核心与非核心业务服务化拆分;服务内各场景线程池级别隔离
- 弱化依赖: 旁路、缓存。
- 控制依赖: 熔断降级、服务限流、设置合理的超时重试。 避免级连失败
可用性指标
业界高可用的标准是按照系统宕机时间来衡量的:
首先去梳理各个业务链路的服务依赖关系以及依赖的调用量,识别出哪些服务是强依赖,哪些是弱依赖。
强弱依赖业界定义
感性: 就是当下游依赖服务出现问题时,当前系统会受到一些影响,让用户有感觉的是强依赖,没感觉的是弱依赖。
理性: 不影响核心业务流程,不影响系统可用性的依赖都可以叫做弱依赖,反之就是强依赖。
对于强依赖尽量具备降级服务逻辑,因为毕竟会影响核心链路。对于弱依赖可随时熔断。
设置合理的超时和重试
对外部系统和缓存、消息队列等基础组件的依赖。假设这些被依赖方突然发生了问题,我们系统的响应时间是:内部耗时+依赖方超时时间*重试次数。如果超时时间设置过长、重试过多,系统长时间不返回,可能会导致连接池被打满,系统死掉;如果超时时间设置过短,系统的可用性会降低。
- 首先要调研被依赖服务自己调用下游的超时时间是多少。调用方的超时时间要大于被依赖方调用下游的时间。
- 统计这个接口99%的响应时间是多少,设置的超时时间在这个基础上加50%。如果接口依赖第三方,而第三方的波动比较大,也可以按照95%的响应时间。
- 重试次数如果系统服务重要性高,则按照默认,一般是重试三次。否则,可以不重试。
Hystix
以业界比较流行的熔断降级组件Hystix为例,来学习其基本的工作原理
Hystix工作流程图
下面将更详细的解析每一个步骤都发生哪些动作:
- 构建一个
HystrixCommand
或者HystrixObservableCommand
对象。
第一步就是构建一个HystrixCommand
或者HystrixObservableCommand
对象,该对象将代表你的一个依赖请求,向构造函数中传入请求依赖所需要的参数。
如果构建HystrixCommand
中的依赖返回单个响应,例如:
1 | 复制代码HystrixCommand command = new HystrixCommand(arg1, arg2); |
如果依赖需要返回一个Observable
来发射响应,就需要通过构建HystrixObservableCommand
对象来完 成,例如:
1 | 复制代码HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2); |
- 执行命令
有4种方式可以执行一个Hystrix命令。
+ `execute()`—该方法是阻塞的,从依赖请求中接收到单个响应(或者出错时抛出异常)。
+ `queue()`—从依赖请求中返回一个包含单个响应的Future对象。
+ `observe()`—订阅一个从依赖请求中返回的代表响应的Observable对象。
+ `toObservable()`—返回一个Observable对象,只有当你订阅它时,它才会执行Hystrix命令并发射响应。
1 | 复制代码K value = command.execute(); |
同步调用方法execute()
实际上就是调用queue().get()
方法,queue()
方法的调用的是toObservable().toBlocking().toFuture()
.也就是说,最终每一个HystrixCommand都是通过Observable来实现的,即使这些命令仅仅是返回一个简单的单个值。
- 响应是否被缓存
如果这个命令的请求缓存已经开启,并且本次请求的响应已经存在于缓存中,那么就会立即返回一个包含缓存响应的Observable
。
- 回路器是否打开
当命令执行执行时,Hystrix会检查回路器是否被打开。
如果回路器被打开(或者tripped),那么Hystrix就不会再执行命名,而是直接路由到第8
步,获取fallback方法,并执行fallback逻辑。
如果回路器关闭,那么将进入第5
步,检查是否有足够的容量来执行任务。(其中容量包括线程池的容量,队列的容量等等)。
- 线程池、队列、信号量是否已满
如果与该命令相关的线程池或者队列已经满了,那么Hystrix就不会再执行命令,而是立即跳到第8
步,执行fallback逻辑。
- 计算回路指标[Circuit Health]
Hystrix会报告成功、失败、拒绝和超时的指标给回路器,回路器包含了一系列的滑动窗口数据,并通过该数据进行统计。
它使用这些统计数据来决定回路器是否应该熔断,如果需要熔断,将在一定的时间内不在请求依赖[短路请求](译者:这一定的时候可以通过配置指定),当再一次检查请求的健康的话会重新关闭回路器。
- 获取FallBack
当命令执行失败时,Hystrix会尝试执行自定义的Fallback逻辑:
+ 当`construct()`或者`run()`方法执行过程中抛出异常。
+ 当回路器打开,命令的执行进入了熔断状态。
+ 当命令执行的线程池和队列或者信号量已经满容。
+ 命令执行超时。
Hystrix设计原则
1.防止单个服务的故障,耗尽整个系统服务的容器
2.用快速失败代替排队(每个依赖服务维护一个小的线程池或信号量,当线程池满或信号量满,会立即拒绝服务而不会排队等待)和优雅的服务降级;当依赖服务失效后又恢复正常,快速恢复
3.提供接近实时的监控和警报,从而能够快速发现故障和修复。监控信息包括请求成功,失败(客户端抛出的异常),超时和线程拒绝。如果访问依赖服务的错误百分比超过阈值,断路器会跳闸,此时服务会在一段时间内停止对特定服务的所有请求
4.将所有请求依赖服务封装到HystrixCommand或HystrixObservableCommand对象中,然后这些请求在一个独立的线程中执行。使用隔离技术来限制任何一个依赖的失败对系统的影响。每个依赖服务维护一个小的线程池(或信号量),当线程池满或信号量满,会立即拒绝服务而不会排队等待
Hystrix特性
断路器
下面的图展示了HystrixCommand
和HystrixObservableCommand
如何与HystrixCircuitBroker
进行交互。
回路器打开和关闭有如下几种情况:
- 假设回路中的请求满足了一定的阈值(
HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
) - 假设错误发生的百分比超过了设定的错误发生的阈值
HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
- 回路器状态由
CLOSE
变换成OPEN
- 如果回路器打开,所有的请求都会被回路器所熔断。
- 一定时间之后
HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()
,下一个的请求会被通过(处于半打开状态),如果该请求执行失败,回路器会在睡眠窗口期间返回OPEN
,如果请求成功,回路器会被置为关闭状态,重新开启1
步骤的逻辑。
下图是熔断自动回复流程图:
当出现问题时,Hystrix会检查一个肯定时长(图中为10s)的一个时间窗(window),在这个时间窗内能否有足够多的请求,假如有足够多的请求,能否错误率已经达到阈值,假如达到则启动断路器熔断机制,这时再有请求过来就会直接到fallback路径。在断路器打开之后,会有一个sleep window
(图中为5s),每经过一个sleep window
,当有请求过来的时候,断路器会放掉一个请求给remote 服务,让它去试探下游服务能否已经恢复,假如成功,断路器会恢复到正常状态,让后续请求重新请求到remote 服务,否则,保持熔断状态。sleep window
实现机制类似于校招常考的那个窗口滑动求最值的问题!
fallback
资源隔离
Hystrix采用舱壁模式来隔离相互之间的依赖关系,并限制对其中任何一个的并发访问。
- 线程和线程池
客户端(第三方包、网络调用等)会在单独的线程执行,会与调用的该任务的线程进行隔离,以此来防止调用者调用依赖所消耗的时间过长而阻塞调用者的线程。
[Hystrix uses separate, per-dependency thread pools as a way of constraining any given dependency so latency on the underlying executions will saturate the available threads only in that pool]
Netflix,设计Hystrix,并且选择使用线程和线程池来实现隔离机制,有以下几个原因:
- 很多应用会调用多个不同的后端服务作为依赖。
- 每个服务会提供自己的客户端库包。
- 每个客户端的库包都会不断的处于变更状态。
- [Client library logic can change to add new network calls]
- 每个客户端库包都可能包含重试、数据解析、缓存等等其他逻辑。
- 对用户来说,客户端库往往是“黑盒”的,对于实现细节、网络访问模式。默认配置等都是不透明的。
- [In several real-world production outages the determination was “oh, something changed and properties should be adjusted” or “the client library changed its behavior.]
- 即使客户端本身没有改变,服务本身也可能发生变化,这些因素都会影响到服务的性能,从而导致客户端配置失效。
- 传递依赖可以引入其他客户端库,这些客户端库不是预期的,也许没有正确配置。
- 大部分的网络访问是同步执行的。
- 客户端代码中也可能出现失败和延迟,而不仅仅是在网络调用中。
信号量
类型 | 优点 | 不足 | 适用 |
---|---|---|---|
线程 | 支持排队和超时、支持异步调用 | 线程调用和切换产生额外开销 | 不受信客户(比如第三方服务稳定性是无法推测的) |
信号量 | 轻量且无额外开销 | 不支持任务排队和超时,不支持异步 | 受信客户、高频高速调用服务(网关、cache) |
参数配置项
线程池相关
1 | 复制代码import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; |
信号量相关
1 | 复制代码import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; |
参考
github Hystrix wiki
高可用系统常用利器(一) - 服务降级 Hystrix
Hystrix工作原理(官方文档翻译)
高可用系统常用利器(一) - 服务降级 Hystrix
本文转载自: 掘金