博主就职于字节跳动直播中台,预计8月在组内做一次技术分享,分享的内容是Hystrix容错框架,以下为内容正文。
容错原理(理论部分)
高可用性(英语:high availability,缩写为 HA),IT术语,指系统无中断地执行其功能的能力,代表系统的可用性程度。是进行系统设计时的准则之一。高可用性系统与构成该系统的各个组件相比可以更长时间运行。
在微服务时代,追求高可用变得更为困难。hystrix官方github举了这样一个例子,如果某个应用依赖了30个微服务,每个微服务的可用性都是4个9,那么将得到:
99.9930 = 99.7% uptime 0.3% of 1 billion requests = 3,000,000 failures 2+ hours downtime/month even if all dependencies have excellent uptime. 高可用性通常通过提高系统的容错能力来实现。
我们应力图通过一系列的保证措施,以达到以下目标:
- 一个依赖服务的故障不会严重破坏用户的体验。
- 系统能自动或半自动处理故障,具备自我恢复能力。
而这一系列的保障措施,可以统称为服务容错。
容错的基本理念
- 凡是依赖都可能会失败
- 凡是资源都有限制
- CPU
- Memory
- Threads/Goroutines
- Queue
- 网络并不可靠
- 延迟是应用稳定性杀手
基本容错模式
服务容错的设计有个基本原则,就是“Design for Failure”,一般有以下几类容错模式:
1. 主动超时与重试
超时模式:在分布式服务调用的场景中,它主要解决了当依赖服务出现建立网络连接或响应延迟,不用无限等待的问题,调用方可以根据事先设计的超时时间中断调用,及时释放关键资源,fast-fail。
重试模式:一般和超时模式结合使用,适用于对于下游服务的数据强依赖的场景(不强依赖的场景不建议使用!),通过重试来保证数据的可靠性或一致性,常用于因网络抖动等导致服务调用出现超时的场景。与超时时间设置结合使用后,需要考虑接口的响应时间分布情况,超时时间可以设置为依赖服务接口99.5%响应时间的值,重试次数一般1-2次为宜,否则会导致请求响应时间延长,拖累到整个系统。
2. 限流
限流模式,常用于下游服务容量有限,但又怕出现突发流量猛增(如恶意爬虫,节假日大促等)而导致下游服务因压力过大而拒绝服务的场景。 提到限流,我们常常想到的就是限流算法,而其实限流算法只是限流模式的一种实现。而常见的限流模式其实是有两种:控制并发和控制速率。争取三句话把两个概念厘清。
- 一个是限制并发的数量,一个是限制并发访问的速率。
- 一个是数量,一个是速度。
- 前者表示系统在同一时刻能处理的最大请求数量,比如xxx次的并发。后者表示在一个时间单位内能够处理的请求数量,比如xxx次请求/秒。 常用的策略可以简称为「两窗两桶」。 两窗就是:固定窗口、滑动窗口,两桶就是:漏桶、令牌桶。
3. 熔断
断路器(熔断器)模式
状态机:开状态、关状态、半开状态
4. 降级
在超时,重试失败,熔断或者限流发生的时候,为了及时恢复服务或者不影响到用户体验,需要提供降级的机制,常见的降级策略有:
- 自定义处理:在这种场景下,可以使用默认数据,本地数据,缓存数据来临时支撑,也可以将请求放入队列,或者使用备用服务获取数据等,适用于业务的关键流程与严重影响用户体验的场景,如商家/产品信息等核心服务。
- 故障沉默(fail-silent):直接返回空值或缺省值,适用于可降级功能的场景,如产品推荐之类的功能,数据为空也不太影响用户体验。
- 快速失败(fail-fast):直接抛出异常,适用于数据非强依赖的场景,如非核心服务超时的处理。
5. 隔离
在造船行业,往往使用舱壁隔离(Bulkhead Isolation)模式对船舱进行隔离,利用舱壁将不同的船舱隔离起来,这样如果一个船舱破了进水,只损失一个船舱,其它船舱可以不受影响,而借鉴造船行业的经验,这种模式也在软件行业得到使用。
线程隔离(Thread Isolation)就是这种模式的常见的一个场景。例如,系统A调用了ServiceB/ServiceC/ServiceD三个远程服务,且部署A的容器一共有120个工作线程,采用线程隔离机制,可以给对ServiceB/ServiceC/ServiceD的调用各分配40个线程。当ServiceB慢了,给ServiceB分配的40个线程因慢而阻塞并最终耗尽,线程隔离可以保证给ServiceC/ServiceD分配的80个线程可以不受影响。如果没有这种隔离机制,当ServiceB慢的时候,120个工作线程会很快全部被对ServiceB的调用吃光,整个系统会全部慢下来,甚至出现系统停止响应的情况。Hystrix简介
本节内容均摘自官方Github
历史
很多博客对这段话的翻译都有问题,主要体现的数量级上,比较信达雅的翻译应该是:hystrix每天为奈飞数百亿基于线程隔离、数千亿基于信号量隔离的服务间调用保驾护航...
Hystrix evolved out of resilience engineering work that the Netflix API team began in 2011. In 2012, Hystrix continued to evolve and mature, and many teams within Netflix adopted it. Today tens of billions of thread-isolated, and hundreds of billions of semaphore-isolated calls are executed via Hystrix every day at Netflix. ——摘自官方Github
致力于解决的问题
- 对依赖服务调用时出现的调用延迟和调用失败进行控制和容错保护。
- 在复杂的分布式系统中,阻止某一个依赖服务的故障在整个系统中蔓延。比如某一个服务故障了,导致其它服务也跟着故障。
- 提供 fail-fast(快速失败)和快速恢复的支持。
- 提供 fallback 优雅降级的支持。
- 支持近实时的监控、报警以及运维操作。
设计原则
- 阻止任何一个依赖服务耗尽所有的资源,比如 tomcat 中的所有线程资源。
- 避免请求排队和积压,采用限流和 fail fast 来控制故障。
- 提供 fallback 降级机制来应对故障。
- 使用资源隔离技术,比如 bulkhead(舱壁隔离技术)、swimlane(泳道技术)、circuit breaker(断路技术)来限制任何一个依赖服务的故障的影响。
- 通过近实时的统计/监控/报警功能,来提高故障发现的速度。
- 通过近实时的属性和配置热修改功能,来提高故障处理和恢复的速度。
- 保护依赖服务调用的所有故障情况,而不仅仅只是网络故障情况。
深入Hystrix执行流程
- 构建Hystrix的Command对象, 调用执行方法.
- Hystrix检查当前服务的熔断器开关是否开启, 若开启, 则执行降级服务getFallback方法.
- 若熔断器开关关闭, 则Hystrix检查当前服务的线程池是否能接收新的请求, 若超过线程池已满, 则执行降级服务getFallback方法.
- 若线程池接受请求, 则Hystrix开始执行服务调用具体逻辑run方法.
- 若服务执行失败, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.
- 若服务执行超时, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况.
- 若服务执行成功, 返回正常结果.
- 若服务降级方法getFallback执行成功, 则返回降级结果.
- 若服务降级方法getFallback执行失败, 则抛出异常.
Hystrix资源隔离
资源未进行隔离导致的Case在我们实践中经常遇到,如某接口由于数据库慢查询,外部RPC调用超时导致整个系统的线程数过高,连接数耗尽等。 资源隔离是hystrix非常核心的一块功能,解决的最重要的问题,通过将多个依赖服务的调用分别隔离到各自的资源池内,避免当一个依赖服务由于响应慢导致线程池任务满的时候,影响到其他依赖服务的调用。
服务雪崩效应形成的原因
名词解释时间 扇入fan-in:是指直接调用该模块的上级模块的个数。扇入大表示模块的复用程序高。 扇出fan-out:是指该模块直接调用的下级模块的个数。扇出大表示模块的复杂度高,需要控制和协调过多的下级模块;但扇出过小(例如总是1)也不好。扇出过大一般是因为缺乏中间层次,应该适当增加中间层次的模块。扇出太小时可以把下级模块进一步分解成若干个子功能模块,或者合并到它的上级模块中去。
我们把服务雪崩的参与者简化为 服务提供者 和 服务调用者, 并将服务雪崩产生的过程分为以下三个阶段来分析形成的原因:
- 服务提供者不可用
- 重试加大流量
- 服务调用者不可用
服务雪崩的每个阶段都可能由不同的原因造成, 比如造成 服务不可用 的原因有:
- 硬件故障
- 程序Bug
- 缓存击穿
- 用户大量请求 硬件故障可能为硬件损坏造成的服务器主机宕机, 网络硬件故障造成的服务提供者的不可访问. 缓存击穿一般发生在缓存应用重启, 所有缓存被清空时,以及短时间内大量缓存失效时. 大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用. 在秒杀和大促开始前,如果准备不充分,用户发起大量请求也会造成服务提供者的不可用. 而形成 重试加大流量 的原因有:
- 用户重试
- 代码逻辑重试 在服务提供者不可用后, 用户由于忍受不了界面上长时间的等待,而不断刷新页面甚至提交表单. 服务调用端的会存在大量服务异常后的重试逻辑. 这些重试都会进一步加大请求流量. 最后, 服务调用者不可用 产生的主要原因是:
- 同步等待造成的资源耗尽 当服务调用者使用 同步调用 时, 会产生大量的等待线程占用系统资源. 一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了.
两种隔离模式
- 线程池隔离
- 信号量隔离
两者的优缺点及适用场景:
隔离策略 | 线程池隔离 | 信号量隔离 |
---|---|---|
是否支持超时 | 支持,可直接返回 | 不支持,如果阻塞,只能通过调用协议(如:socket超时才能返回) |
是否支持熔断 | 支持,当线程池到达maxSize后,再请求会触发fallback接口进行熔断 | 支持,当信号量达到maxConcurrentRequests后。再请求会触发fallback |
隔离原理 | 每个服务单独用线程池 | 通过信号量的计数器 |
是否是异步调用 | 可以是异步,也可以是同步。看调用的方法 | 同步调用,不支持异步 |
资源消耗 | 大,大量线程的上下文切换,容易造成机器负载高 | 小,只是个计数器 |
适用场景 | 不受信client有限扇出 | 受信客户高扇出(网关)、高频高速调用(cache) |
熔断器
断路器的原理图如下:
其执行步骤如下:- 首先通过断路器的流量达到配置的阈值,流量太小不会触发断路器
- 错误率达到阈值
- 满足上述两个条件后,会触发hystrix将断路器状态从关闭切换为开通
- 之后的请求进来,会自动走降级逻辑
- 期间hystrix会判断是否达到休眠时间,如果已达到,会尝试进入半开状态
- 半开状态会放一部分流量进行尝试,如果请求成功,则恢复断路器为关闭状态,服务恢复正常;否则重置断路器状态为开通状态。
降级
降级,通常指务高峰期,为了保证核心服务正常运行,需要停掉一些不太重要的业务,或者某些服务不可用时,执行备用逻辑从故障服务中快速失败或快速返回,以保障主体业务不受影响。Hystrix提供的降级主要是为了容错,保证当前服务不受依赖服务故障的影响,从而提高服务的健壮性。 hystrix支持以下几类降级:
Fail Fast 快速失败
快速失败是最普通的命令执行方法,命令没有重写降级逻辑。 如果命令执行发生任何类型的故障,它将直接抛出异常。 Fail Silent 无声失败 指在降级方法中通过返回null,空Map,空List或其他类似的响应来完成。
Fallback: Static
指在降级方法中返回静态默认值。 这不会导致服务以“无声失败”的方式被删除,而是导致默认行为发生。如:应用根据命令执行返回true / false执行相应逻辑,但命令执行失败,则默认为true
Fallback: Cache via Network
有时,如果调用依赖服务失败,可以从缓存服务(如redis)中查询旧数据版本。由于又会发起远程调用,所以建议重新封装一个Command,使用不同的ThreadPoolKey,与主线程池进行隔离。
代码示例
讲了那么多隔离、熔断、降级,hystrix具体要怎么操作呢,我们简单看一个例子
public class QueryOrderIdCommand extends HystrixCommand<Integer> {
private final static Logger logger = LoggerFactory.getLogger(QueryOrderIdCommand.class);
private OrderServiceProvider orderServiceProvider;
public QueryOrderIdCommand(OrderServiceProvider orderServiceProvider) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("orderService"))
.andCommandKey(HystrixCommandKey.Factory.asKey("queryByOrderId"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(10)//至少有10个请求,熔断器才进行错误率的计算
.withCircuitBreakerSleepWindowInMilliseconds(5000)//熔断器中断请求5秒后会进入半打开状态,放部分流量过去重试
.withCircuitBreakerErrorThresholdPercentage(50)//错误率达到50开启熔断保护
.withExecutionTimeoutEnabled(true))
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties
.Setter().withCoreSize(10)));
this.orderServiceProvider = orderServiceProvider;
}
@Override
protected Integer run() {
return orderServiceProvider.queryByOrderId();
}
@Override
protected Integer getFallback() {
return -1;
}
}
Sentinel
- 轻量级、高性能 Sentinel 作为一个功能完备的高可用流量管控组件,其核心 sentinel-core 没有任何多余依赖,打包后只有不到 200KB,非常轻量级。开发者可以放心地引入 sentinel-core 而不需担心依赖问题。同时,Sentinel 提供了多种扩展点,用户可以很方便地根据需求去进行扩展,并且无缝地切合到 Sentinel 中。 引入 Sentinel 带来的性能损耗非常小。只有在业务单机量级超过 25W QPS 的时候才会有一些显著的影响(5% - 10% 左右),单机 QPS 不太大的时候损耗几乎可以忽略不计。
- 流量控制 Sentinel 可以针对不同的调用关系,以不同的运行指标(如 QPS、并发调用数、系统负载等)为基准,对资源调用进行流量控制,将随机的请求调整成合适的形状。 Sentinel 支持多样化的流量整形策略,在 QPS 过高的时候可以自动将流量调整成合适的形状。常用的有:
- 直接拒绝模式:即超出的请求直接拒绝。
- 慢启动预热模式:当流量激增的时候,控制流量通过的速率,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
- 匀速器模式:利用 Leaky Bucket 算法实现的匀速模式,严格控制了请求通过的时间间隔,同时堆积的请求将会排队,超过超时时长的请求直接被拒绝。Sentinel 还支持基于调用关系的限流,包括基于调用方限流、基于调用链入口限流、关联流量限流等,依托于 Sentinel 强大的调用链路统计信息,可以提供精准的不同维度的限流。
- 系统负载保护 Sentinel 对系统的维度提供保护,负载保护算法借鉴了 TCP BBR 的思想。当系统负载较高的时候,如果仍持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。针对这个情况,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
- 实时监控和控制面板 Sentinel 提供 HTTP API 用于获取实时的监控信息,如调用链路统计信息、簇点信息、规则信息等。如果用户正在使用 Spring Boot/Spring Cloud 并使用了Sentinel Spring Cloud Starter,还可以方便地通过其暴露的 Actuator Endpoint 来获取运行时的一些信息,如动态规则等。未来 Sentinel 还会支持标准化的指标监控 API,可以方便地整合各种监控系统和可视化系统,如 Prometheus、Grafana 等。 Sentinel 控制台(Dashboard)提供了机器发现、配置规则、查看实时监控、查看调用链路信息等功能,使得用户可以非常方便地去查看监控和进行配置。
- 生态 Sentinel 目前已经针对 Servlet、Dubbo、Spring Boot/Spring Cloud、gRPC 等进行了适配,用户只需引入相应依赖并进行简单配置即可非常方便地享受 Sentinel 的高可用流量防护能力。未来 Sentinel 还会对更多常用框架进行适配,并且会为 Service Mesh 提供集群流量防护的能力。
sentinel 和 hystrix 对比可以总结为下表:
# | Sentinel | Hystrix |
---|---|---|
隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于响应时间或失败比率 | 基于失败比率 |
实时指标实现 | 滑动窗口 | 滑动窗口(基于 RxJava) |
规则配置 | 支持多种数据源 | 支持多种数据源 |
扩展性 | 多个扩展点 | 插件的形式 |
基于注解的支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 不支持 |
流量整形 | 支持慢启动、匀速器模式 | 不支持 |
系统负载保护 | 支持 | 不支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
常见框架的适配 | Servlet、Spring Cloud、Dubbo、gRPC | Servlet、Spring Cloud Netflix |