Fork me on GitHub

微服务高可用利器——Hystrix 熔断降级原理 & 实践总结

head

前言

最近在工作中参与组内服务稳定性建设,梳理我们目前服务现状并接入公司自研稳定性保障平台。对公司内自研组件以及业界流行的Hystrix做了学习,Netflix Hystrix 里面大量RxJava响应式实现,实在看着有点绕。所以在这里梳理一些实践以及Hystrix知识点。

为什么要做这个事情

服务的稳定是公司可持续发展的重要基石,随着业务量的快速发展,一些平时正常运行的服务,会出现各种突发状况,而且在分布式系统中,每个服务本身又存在很多不可控的因素,比如线程池处理缓慢,导致请求超时,资源不足,导致请求被拒绝,又甚至直接服务不可用、宕机、数据库挂了、缓存挂了、消息系统挂了...

对于一些非核心服务,如果出现大量的异常,可以通过技术手段,对服务进行降级并提供有损服务,保证服务的柔性可用,避免引起雪崩效应。

例如:一个依赖30个服务的系统,每个服务99.99%可用,99.99%的30次方 ≈ 99.7% ,0.3% 意味着1亿次请求会有 3,000,00次失败 ,换算成时间大约每月有2个小时服务不稳定,随着服务依赖数量的变多,服务总体可用性会变得更差。
假设我们当前服务的外部依赖中,有一个服务出现了故障,可能是网络抖动出现了超时,亦或服务挂掉导致请求超时,短时间内看起来像下图这样:
7430ba47b3e649c4a27fcacd12103453-image.png

慢慢的大量业务线程都会阻塞在对故障服务的调用上,请求排队,服务响应缓慢,系统资源渐渐消耗,最终导致服务崩溃,更可怕的是这种影响会持续的向上传递,进而导致服务雪崩。
5413341075ba4846933350395ec5581d-image.png

如何去做

  • 消除依赖: 梳理去除、隔离。 比如系统尽量减少第三方依赖;核心与非核心业务服务化拆分;服务内各场景线程池级别隔离

  • 弱化依赖: 旁路、缓存。

  • 控制依赖: 熔断降级、服务限流、设置合理的超时重试。 避免级连失败

可用性指标

业界高可用的标准是按照系统宕机时间来衡量的:

首先去梳理各个业务链路的服务依赖关系以及依赖的调用量,识别出哪些服务是强依赖,哪些是弱依赖。
强弱依赖业界定义

感性: 就是当下游依赖服务出现问题时,当前系统会受到一些影响,让用户有感觉的是强依赖,没感觉的是弱依赖。

理性: 不影响核心业务流程,不影响系统可用性的依赖都可以叫做弱依赖,反之就是强依赖。

对于强依赖尽量具备降级服务逻辑,因为毕竟会影响核心链路。对于弱依赖可随时熔断。

设置合理的超时和重试

对外部系统和缓存、消息队列等基础组件的依赖。假设这些被依赖方突然发生了问题,我们系统的响应时间是:内部耗时+依赖方超时时间*重试次数。如果超时时间设置过长、重试过多,系统长时间不返回,可能会导致连接池被打满,系统死掉;如果超时时间设置过短,系统的可用性会降低。

  • 首先要调研被依赖服务自己调用下游的超时时间是多少。调用方的超时时间要大于被依赖方调用下游的时间。
  • 统计这个接口99%的响应时间是多少,设置的超时时间在这个基础上加50%。如果接口依赖第三方,而第三方的波动比较大,也可以按照95%的响应时间。
  • 重试次数如果系统服务重要性高,则按照默认,一般是重试三次。否则,可以不重试。

Hystix

以业界比较流行的熔断降级组件Hystix为例,来学习其基本的工作原理

Hystix工作流程图

3558e27a37a24d92a8530df3eeb52713-image.png

​ 下面将更详细的解析每一个步骤都发生哪些动作:

  • 构建一个HystrixCommand或者HystrixObservableCommand对象。

    第一步就是构建一个HystrixCommand或者HystrixObservableCommand对象,该对象将代表你的一个依赖请求,向构造函数中传入请求依赖所需要的参数。

    如果构建HystrixCommand中的依赖返回单个响应,例如:

    HystrixCommand command = new HystrixCommand(arg1, arg2);
    

    如果依赖需要返回一个Observable来发射响应,就需要通过构建HystrixObservableCommand对象来完 成,例如:

    HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);
    
  • 执行命令

    有4种方式可以执行一个Hystrix命令。

    • execute()—该方法是阻塞的,从依赖请求中接收到单个响应(或者出错时抛出异常)。
    • queue()—从依赖请求中返回一个包含单个响应的Future对象。
    • observe()—订阅一个从依赖请求中返回的代表响应的Observable对象。
    • toObservable()—返回一个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来实现的,即使这些命令仅仅是返回一个简单的单个值。

  • 响应是否被缓存

    如果这个命令的请求缓存已经开启,并且本次请求的响应已经存在于缓存中,那么就会立即返回一个包含缓存响应的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对象中,然后这些请求在一个独立的线程中执行。使用隔离技术来限制任何一个依赖的失败对系统的影响。每个依赖服务维护一个小的线程池(或信号量),当线程池满或信号量满,会立即拒绝服务而不会排队等待

38d764f8416e4320921193551dbeceac-image.png

Hystrix特性

断路器

下面的图展示了HystrixCommandHystrixObservableCommand如何与HystrixCircuitBroker进行交互。

7237eed4c0e147e29b8ebd7ce1610acd-image.png
回路器打开和关闭有如下几种情况:

  • 假设回路中的请求满足了一定的阈值(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
  • 假设错误发生的百分比超过了设定的错误发生的阈值HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
  • 回路器状态由CLOSE变换成OPEN
  • 如果回路器打开,所有的请求都会被回路器所熔断。
  • 一定时间之后HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds(),下一个的请求会被通过(处于半打开状态),如果该请求执行失败,回路器会在睡眠窗口期间返回OPEN,如果请求成功,回路器会被置为关闭状态,重新开启1步骤的逻辑。

下图是熔断自动回复流程图:
4ed767dd368b4b35b179779db390a331-image.png

当出现问题时,Hystrix会检查一个肯定时长(图中为10s)的一个时间窗(window),在这个时间窗内能否有足够多的请求,假如有足够多的请求,能否错误率已经达到阈值,假如达到则启动断路器熔断机制,这时再有请求过来就会直接到fallback路径。在断路器打开之后,会有一个sleep window(图中为5s),每经过一个sleep window,当有请求过来的时候,断路器会放掉一个请求给remote 服务,让它去试探下游服务能否已经恢复,假如成功,断路器会恢复到正常状态,让后续请求重新请求到remote 服务,否则,保持熔断状态。sleep window 实现机制类似于校招常考的那个窗口滑动求最值的问题!

fallback

资源隔离

Hystrix采用舱壁模式来隔离相互之间的依赖关系,并限制对其中任何一个的并发访问。

5e42ff3d68f4476a88ef201cf98284a6-image.png

3392f44f296345c9ae1197d1f924ef09-image.png

  • 线程和线程池

    客户端(第三方包、网络调用等)会在单独的线程执行,会与调用的该任务的线程进行隔离,以此来防止调用者调用依赖所消耗的时间过长而阻塞调用者的线程。

    [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.]
  • 即使客户端本身没有改变,服务本身也可能发生变化,这些因素都会影响到服务的性能,从而导致客户端配置失效。
  • 传递依赖可以引入其他客户端库,这些客户端库不是预期的,也许没有正确配置。
  • 大部分的网络访问是同步执行的。
  • 客户端代码中也可能出现失败和延迟,而不仅仅是在网络调用中。

信号量
772f79f5ac94478caa84d23f430d39b1-image.png

类型 优点 不足 适用
线程 支持排队和超时、支持异步调用 线程调用和切换产生额外开销 不受信客户(比如第三方服务稳定性是无法推测的)
信号量 轻量且无额外开销 不支持任务排队和超时,不支持异步 受信客户、高频高速调用服务(网关、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);
    }
}

参考

github Hystrix wiki
美团点评智能支付核心交易系统的可用性实践
高可用系统常用利器(一) - 服务降级 Hystrix
Hystrix工作原理(官方文档翻译)
高可用系统常用利器(一) - 服务降级 Hystrix
超全总结 | 阿里电商故障治理和故障演练实践


本文地址:https://www.6aiq.com/article/1574665111167
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出