最近在工作中参与组内服务稳定性建设,梳理我们目前服务现状并接入公司自研稳定性保障平台。对公司内自研组件以及业界流行的Hystrix做了学习,Netflix Hystrix 里面大量RxJava响应式实现,实在看着有点绕。所以在这里梳理一些实践以及Hystrix知识点。
服务的稳定是公司可持续发展的重要基石,随着业务量的快速发展,一些平时正常运行的服务,会出现各种突发状况,而且在分布式系统中,每个服务本身又存在很多不可控的因素,比如线程池处理缓慢,导致请求超时,资源不足,导致请求被拒绝,又甚至直接服务不可用、宕机、数据库挂了、缓存挂了、消息系统挂了...
对于一些非核心服务,如果出现大量的异常,可以通过技术手段,对服务进行降级并提供有损服务,保证服务的柔性可用,避免引起雪崩效应。
例如:一个依赖30个服务的系统,每个服务99.99%可用,99.99%的30次方 ≈ 99.7% ,0.3% 意味着1亿次请求会有 3,000,00次失败 ,换算成时间大约每月有2个小时服务不稳定,随着服务依赖数量的变多,服务总体可用性会变得更差。 假设我们当前服务的外部依赖中,有一个服务出现了故障,可能是网络抖动出现了超时,亦或服务挂掉导致请求超时,短时间内看起来像下图这样:
慢慢的大量业务线程都会阻塞在对故障服务的调用上,请求排队,服务响应缓慢,系统资源渐渐消耗,最终导致服务崩溃,更可怕的是这种影响会持续的向上传递,进而导致服务雪崩。
消除依赖:梳理去除、隔离。 比如系统尽量减少第三方依赖;核心与非核心业务服务化拆分;服务内各场景线程池级别隔离
弱化依赖:旁路、缓存。
控制依赖:熔断降级、服务限流、设置合理的超时重试。 避免级连失败
业界高可用的标准是按照系统宕机时间来衡量的:
首先去梳理各个业务链路的服务依赖关系以及依赖的调用量,识别出哪些服务是强依赖,哪些是弱依赖。 强弱依赖业界定义
感性:就是当下游依赖服务出现问题时,当前系统会受到一些影响,让用户有感觉的是强依赖,没感觉的是弱依赖。
理性:不影响核心业务流程,不影响系统可用性的依赖都可以叫做弱依赖,反之就是强依赖。
对于强依赖尽量具备降级服务逻辑,因为毕竟会影响核心链路。对于弱依赖可随时熔断。
对外部系统和缓存、消息队列等基础组件的依赖。假设这些被依赖方突然发生了问题,我们系统的响应时间是:内部耗时+依赖方超时时间*重试次数。如果超时时间设置过长、重试过多,系统长时间不返回,可能会导致连接池被打满,系统死掉;如果超时时间设置过短,系统的可用性会降低。
以业界比较流行的熔断降级组件Hystix为例,来学习其基本的工作原理
下面将更详细的解析每一个步骤都发生哪些动作:
构建一个 HystrixCommand
或者 HystrixObservableCommand
对象。
第一步就是构建一个 HystrixCommand
或者 HystrixObservableCommand
对象,该对象将代表你的一个依赖请求,向构造函数中传入请求依赖所需要的参数。
如果构建 HystrixCommand
中的依赖返回单个响应,例如:
HystrixCommand command = new HystrixCommand(arg1, arg2); 复制代码
如果依赖需要返回一个 Observable
来发射响应,就需要通过构建 HystrixObservableCommand
对象来完 成,例如:
HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2); 复制代码
有4种方式可以执行一个Hystrix命令。
execute() queue() observe() toObservable()
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来实现的,即使这些命令仅仅是返回一个简单的单个值。
如果这个命令的请求缓存已经开启,并且本次请求的响应已经存在于缓存中,那么就会立即返回一个包含缓存响应的 Observable
。
当命令执行执行时,Hystrix会检查回路器是否被打开。
如果回路器被打开(或者tripped),那么Hystrix就不会再执行命名,而是直接路由到第 8
步,获取fallback方法,并执行fallback逻辑。
如果回路器关闭,那么将进入第 5
步,检查是否有足够的容量来执行任务。(其中容量包括线程池的容量,队列的容量等等)。
如果与该命令相关的线程池或者队列已经满了,那么Hystrix就不会再执行命令,而是立即跳到第 8
步,执行fallback逻辑。
Hystrix会报告成功、失败、拒绝和超时的指标给回路器,回路器包含了一系列的滑动窗口数据,并通过该数据进行统计。
它使用这些统计数据来决定回路器是否应该熔断,如果需要熔断,将在一定的时间内不在请求依赖[短路请求](译者:这一定的时候可以通过配置指定),当再一次检查请求的健康的话会重新关闭回路器。
当命令执行失败时,Hystrix会尝试执行自定义的Fallback逻辑:
construct()
或者 run()
方法执行过程中抛出异常。 1.防止单个服务的故障,耗尽整个系统服务的容器
2.用快速失败代替排队(每个依赖服务维护一个小的线程池或信号量,当线程池满或信号量满,会立即拒绝服务而不会排队等待)和优雅的服务降级;当依赖服务失效后又恢复正常,快速恢复
3.提供接近实时的监控和警报,从而能够快速发现故障和修复。监控信息包括请求成功,失败(客户端抛出的异常),超时和线程拒绝。如果访问依赖服务的错误百分比超过阈值,断路器会跳闸,此时服务会在一段时间内停止对特定服务的所有请求
4.将所有请求依赖服务封装到HystrixCommand或HystrixObservableCommand对象中,然后这些请求在一个独立的线程中执行。使用隔离技术来限制任何一个依赖的失败对系统的影响。每个依赖服务维护一个小的线程池(或信号量),当线程池满或信号量满,会立即拒绝服务而不会排队等待
下面的图展示了 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
实现机制类似于校招常考的那个窗口滑动求最值的问题!
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,并且选择使用线程和线程池来实现隔离机制,有以下几个原因:
类型 | 优点 | 不足 | 适用 |
---|---|---|---|
线程 | 支持排队和超时、支持异步调用 | 线程调用和切换产生额外开销 | 不受信客户(比如第三方服务稳定性是无法推测的) |
信号量 | 轻量且无额外开销 | 不支持任务排队和超时,不支持异步 | 受信客户、高频高速调用服务(网关、cache) |
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import static com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager.*; /* * 注意: @HystrixCommand 注解方式依赖 AOP, 不支持在同一个类的内部方法之间直接调用, 必须将被调用类作为 bean 注入并调用 */ public class DemoCircuitBreakerAnnotation { /** * 使用 THREAD 模式及线程池参数、通用参数说明 */ @HystrixCommand( groupKey = "GroupAnnotation", commandKey = "HystrixAnnotationThread", fallbackMethod = "HystrixAnnotationThreadFallback", /* * 线程池名, 具有同一线程池名的方法将在同一个线程池中执行 * * 默认值: 方法的groupKey */ threadPoolKey = "GroupAnnotationxThreadPool", threadPoolProperties = { /* * 线程池Core线程数及最大线程数 * * 默认值: 10 */ @HystrixProperty(name = CORE_SIZE, value = "10"), /* * 线程池线程 KeepAliveTime 单位: 分钟 * * 默认值: 1 */ @HystrixProperty(name = KEEP_ALIVE_TIME_MINUTES, value = "1"), /* * 线程池最大队列长度 * * 默认值: -1, 此时使用 SynchronousQueue */ @HystrixProperty(name = MAX_QUEUE_SIZE, value = "100"), /* * 达到这个队列长度后, 线程池开始拒绝后续任务 * * 默认值: 5, MaxQueueSize > 0 时有效 */ @HystrixProperty(name = QUEUE_SIZE_REJECTION_THRESHOLD, value = "90"), }, commandProperties = { /* * 以 THREAD (线程池)模式执行, run 方法将被一个线程池中的线程执行 * * 注意: 由于有额外的线程调度开销, THREAD 模式的性能不如 NONE 和 SEMAPHORE 模式, 但隔离性比较好 * * 默认值: THREAD */ @HystrixProperty(name = EXECUTION_ISOLATION_STRATEGY, value = "THREAD"), /* * 方法执行超时后是否中断执行线程 * * 默认值: true, THREAD 模式下有效 */ @HystrixProperty(name = EXECUTION_ISOLATION_THREAD_INTERRUPT_ON_TIMEOUT, value = "true"), /* * 超时时间参数 * 在 THREAD 模式下, 方法超时后 Hystrix 默认会中断原方法的执行线程, 并标记这次方法的执行结果为失败(影响方法的健康值) * 同时另开一个线程执行 fallback, 最终返回 fallback 的结果 * * 默认值: 1000 */ @HystrixProperty(name = EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "500") /* * 其余参数参考上面的例子, 或者使用默认值 */ }) public String HystrixAnnotationThread(String param) { return "Run with " + param; } public String HystrixAnnotationThreadFallback(String param, Throwable ex) { return String.format("Fallback with param: %s, exception: %s", param, ex); } } 复制代码
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import static com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager.*; /* * 注意: @HystrixCommand 注解方式依赖 AOP, 不支持在同一个类的内部方法之间直接调用, 必须将被调用类作为 bean 注入并调用 */ public class DemoCircuitBreakerAnnotation { /** * 使用 SEMAPHORE 模式及通用参数说明 */ @HystrixCommand( groupKey = "GroupAnnotation", commandKey = "HystrixAnnotationSemaphore", fallbackMethod = "HystrixAnnotationSemaphoreFallback", commandProperties = { /* * 以 SEMAPHORE (信号量)模式执行, 原方法将在调用此方法的线程中执行 * * 如果原方法无需信号量限制, 可以选择使用 NONE 模式 * NONE 模式相比 SEMAPHORE 模式少了信号量获取和判断的步骤, 效率相对较高, 其余执行流程与 SEMAPHORE 模式相同 * * 默认值: THREAD */ @HystrixProperty(name = EXECUTION_ISOLATION_STRATEGY, value = "SEMAPHORE"), /* * 执行 run 方法的信号量上限, 即由于方法执行未完成停留在 run 方法内的线程最大个数 * 执行线程退出 run 方法后释放信号量, 其他线程获取不到信号量无法执行 run 方法 * * 默认值: 1000, SEMAPHORE 模式下有效 */ @HystrixProperty(name = EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS, value = "100"), /* * 执行 fallback 方法的信号量上限 * * 注意: 所有模式(NONE|SEMAPHORE|THREAD) fallback 的执行都受这个参数影响 * * 默认值: Integer.MAX_VALUE */ @HystrixProperty(name = FALLBACK_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS, value = "1000"), /* * 超时时间参数 * 在 SEMAPHORE 模式下, 方法超时后 Hystrix 不会中断原方法的执行线程, 只标记这次方法的执行结果为失败(影响方法的健康值) * 同时另开一个线程执行 fallback, 最终返回 fallback 的结果 * * 默认值: 1000 */ @HystrixProperty(name = EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "500"), /* * 方法各项指标值存活的滑动时间窗口长度, 每经过一个时间窗口长度重置各项指标值, 比如: 方法的健康值 * * 默认值: 10000 */ @HystrixProperty(name = METRICS_ROLLING_STATS_TIME_IN_MILLISECONDS, value = "10000"), /* * 滑动时间窗口指标采样的时间分片数, 分片数越高时, 指标汇总更新的频率越高, 指标值的实时度越好, 但同时也占用较多 CPU * 采样过程: 将一个滑动时间窗口时长根据分片数等分成多个时间分片, 每经过一个时间分片将最新一个时间分片的内积累的统计数据汇总更新到时间窗口内存活的已有指标值中 * * 注意: 这个值只影响 Hystrix Monitor 上方法指标值的展示刷新频率,不影响熔断状态的判断 * * 默认值: 10 */ @HystrixProperty(name = METRICS_ROLLING_STATS_NUM_BUCKETS, value = "10"), /* * 健康值采样的间隔, 相当于时间片长度, 每经过一个间隔将这个时间片内积累的统计数据汇总更新到时间窗口内存活的已有健康值中 * * 健康值主要包括: 方法在滑动时间窗口内的总执行次数、成功执行次数、失败执行次数 * * 默认值: 500 */ @HystrixProperty(name = METRICS_HEALTH_SNAPSHOT_INTERVAL_IN_MILLISECONDS, value = "500"), /* * 一个滑动时间窗口内, 方法的执行次数达到这个数量后方法的健康值才会影响方法的熔断状态 * * 默认值: 20 */ @HystrixProperty(name = CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "10"), /* * 一个采样滑动时间窗口内, 方法的执行失败次数达到这个百分比且达到上面的执行次数要求后, 方法进入熔断状态, 后续请求将执行 fallback 流程 * * 默认值: 50 */ @HystrixProperty(name = CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "50"), /* * 熔断状态停留时间, 方法进入熔断状态后需要等待这个时间后才会再次尝试执行原方法重新评估健康值. 再次尝试执行原方法时若请求成功则重置健康值 * * 默认值: 5000 */ @HystrixProperty(name = CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS, value = "5000") }) public String HystrixAnnotationSemaphore(String param) { return "Run with " + param; } public String HystrixAnnotationSemaphoreFallback(String param, Throwable ex) { return String.format("Fallback with param: %s, exception: %s", param, ex); } } 复制代码