[TOC]
0. Hystrix 熔断机制的使用
当核心链路有依赖非核心链路时,由于下游服务不可用或者性能较差而导致核心链路的服务不可用的时候,也就是服务的级联故障。比如帐号的注册,会拿手机号去过一遍风控,而大流量时风控的能力如果比较差,那么如果是强依赖的话,大流量下就会导致无法注册,这种就是很严重的故障了。
所以就有了成熟的解决方案,hystrix
- 当依赖服务响应过长的时候,核心服务无需等待
- 提供备用方案,当活动不可用的时候执行备用方案
Hystrix 是开源的分布式系统延迟和容错库,能够像保险丝一样给系统提供过载保护等,防止级联的失败造成分布式系统服务的雪崩。提供了几个比较重要的特性
- 熔断器机制
- 资源的隔离
- 线程池
- 信号量
- 执行模型
- 同步模型
- 异步模型
- RxJava模型(观察者)
- 缓存
- 请求合并(HystrixCollapser)
- 运维监控平台等···
1. Hystrix 熔断器机制
1.1 工作流程
- 当服务调用出错时,开启一个时间窗口(默认窗口是10s的)
- 在时间窗口内,统计调用次数是否达到最小请求数?
- 如果没有达到,重置统计信息,回到步骤1(即使全部请求失败了也是回到步骤1)
- 如果达到了,则统计失败的请求数和总请求数的占比,是否达到了阈值?
- 如果达到了阈值,就跳闸(不再继续请求)熔断
- 如果没打到,就重置统计信息回到第1步
- 在跳闸熔断期间,会开启一个时间窗口(默认5s),每隔5s放行1个请求看是否成功
- 如果成功则认为服务恢复了,重置熔断回到第1步
- 如果失败,回到第3步
1.2 熔断器的底层实现HystrixCircuitBreaker
Hystrix 内置的熔断器 HystrixCircuitBreaker 实现中有3种熔断器的状态,3种状态的转换如下图所示
- CLOSE:关闭状态,代表此时应用服务调用等都正常
- OPEN:打开状态,代表此时出现了错误情况需要被保护
- HALF_OPEN:半开状态,自动化关闭以及自动化开启
- 初始时熔断器处于
CLOSE
状态,当满足以下条件时,熔断器转为OPEN状态
- 周期内总的请求量超过一定量级了
- 错误请求占比超过一定的比例了
- 当熔断器处于
OPEN
状态时,当到达一定的时间后熔断器会自动转为HEAL_OPEN
半开的状态,在这个状态下每过一定的时间放行一个请求去验证服务是否恢复正常,如果恢复正常了就重置熔断器置为CLOSE
状态,如果不正常就继续在这个周期去检查。
在源码中熔断器有2个子类的实现,分别是
- NoOpCircuitBreaker 空的熔断器,用于不开启熔断器的情况
- HystrixCircuitBreakerImpl: 完整核心的熔断
在 AbstractCommand 初始化的时候,会初始化 HystrixCircuitBreaker
// AbstractCommand.java 配置中 circuitBreakerEnabled 决定是否开启熔断器,默认是true,如下
// private static final Boolean default_circuitBreakerEnabled = true;
this.circuitBreaker = initCircuitBreaker(this.properties.circuitBreakerEnabled().get(),
circuitBreaker,
this.commandGroup,
this.commandKey,
this.properties,
this.metrics);
// 初始化熔断器的方法 AbstractCommand.java
private static HystrixCircuitBreaker initCircuitBreaker(boolean enabled,
HystrixCircuitBreaker fromConstructor,
HystrixCommandGroupKey groupKey,
HystrixCommandKey commandKey,
HystrixCommandProperties properties,
HystrixCommandMetrics metrics) {
if (enabled) {
// 如果开启了就初始化 HystrixCircuitBreakerImpl
if (fromConstructor == null) {
// 使用工厂方法创建,工厂目前只创建 HystrixCircuitBreakerImpl实现,如下代码
return HystrixCircuitBreaker.Factory.getInstance(commandKey, groupKey, properties, metrics);
} else {
return fromConstructor;
}
} else {
// 如果没开启熔断器,初始化空的熔断器实现
return new NoOpCircuitBreaker();
}
}
在接了动态配置中心的时候,可以手动打开强制熔断等策略,方法是
@Override
public boolean attemptExecution() {
// 强制打开
if (properties.circuitBreakerForceOpen().get()) {
return false;
}
// 强制关闭
if (properties.circuitBreakerForceClosed().get()) {
return true;
}
// 打开时间为空也是强制关闭
if (circuitOpened.get() == -1) {
return true;
} else {
// 如果到达了一个时间窗口(熔断器打开的时候,会有时间窗口尝试通过服务验证是否可用)
if (isAfterSleepWindow()) {
// 判断是否满足尝试调用验证服务是否正常,使用CAS修改熔断器状态,保证只有1个线程可以修改该状态
if (status.compareAndSet(Status.OPEN, Status.HALF_OPEN)) {
return true;
} else {
return false;
}
} else {
return false;
}
}
}
// 当前时间超过熔断器打开时间,默认5000ms,返回true
private boolean isAfterSleepWindow() {
final long circuitOpenTime = circuitOpened.get();
final long currentTime = System.currentTimeMillis();
final long sleepWindowTime = properties.circuitBreakerSleepWindowInMilliseconds().get();
return currentTime > circuitOpenTime + sleepWindowTime;
}
当逻辑调用成功之后,调用 markSuccess()
方法关闭熔断器,并且执行重置统计
@Override
public void markSuccess() {
// CAS 关闭熔断器
if (status.compareAndSet(Status.HALF_OPEN, Status.CLOSED)) {
// 重置统计流
metrics.resetStream();
Subscription previousSubscription = activeSubscription.get();
if (previousSubscription != null) {
// 取消原有订阅
previousSubscription.unsubscribe();
}
// 发起新的订阅
Subscription newSubscription = subscribeToStream();
activeSubscription.set(newSubscription);
// // 设置熔断器打开时间为空
circuitOpened.set(-1L);
}
}
当逻辑调用依然失败,调用markNonSuccess
方法重新打开熔断器
@Override
public void markNonSuccess() {
if (status.compareAndSet(Status.HALF_OPEN, Status.OPEN)) {
//设置熔断器的打开时间为当前时间,这样的话再次执行尝试就是下个新的周期了。
circuitOpened.set(System.currentTimeMillis());
}
}
1.3 熔断器的详细流程
image-20201008101327854熔断器的开关条件
- 如果请求量到达了指定值(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold)
- 如果异常比率超过了指定值(HystrixCommandProperties.circuitBreakerErrorThresholdPercentage)
- 则,熔断器将状态设置为OPEN.
- 之后所有请求都会被直接熔断。
- 在经过指定窗口期(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds)后,状态将会被设置为HALF-OPEN,如果该请求失败了,状态重新被设置为OPEN并且等待下一个窗口期,如果请求成功了,状态设置为CLOSE。
2. hystrix舱壁模式
线程池隔离,防止服务之间共用线程池导致有问题,类似小船的仓库格子,如果一个地方漏了,其它地方不会有较大的影响。
Hystrix 提供了2种方式进行隔离:
- 线程池隔离,也是分布式场景下最常用的隔离方式,多用于服务的依赖等场景下
- 信号量隔离,适合对内部的一些比较复杂的业务逻辑访问隔离,不涉及任何的网络请求,当内部并发量级超过指定的数值时可以直接熔断拒绝。
2.1 隔离实现 ExecutionIsolationStrategy
public static enum ExecutionIsolationStrategy {
THREAD, SEMAPHORE
}
HystrixThreadPoolKey 是线程池的标识接口
public interface HystrixThreadPoolKey extends HystrixKey {
class Factory {
private Factory() {
}
// name与HystrixThreadPoolKey映射达到枚举的效果
private static final InternMap<String, HystrixThreadPoolKey> intern
= new InternMap<String, HystrixThreadPoolKey>(
new InternMap.ValueConstructor<String, HystrixThreadPoolKey>() {
@Override
public HystrixThreadPoolKey create(String key) {
return new HystrixThreadPoolKeyDefault(key);
}
});
// 从intern中获取name的对象
public static HystrixThreadPoolKey asKey(String name) {
return intern.interned(name);
}
private static class HystrixThreadPoolKeyDefault extends HystrixKeyDefault implements HystrixThreadPoolKey {
public HystrixThreadPoolKeyDefault(String name) {
super(name);
}
}
// 获取线程池的数量
static int getThreadPoolCount() {
return intern.size();
}
}
还是在 AbstractCommand中初始化的线程池
this.threadPool = initThreadPool(threadPool,
this.threadPoolKey,
threadPoolPropertiesDefaults);
private static HystrixThreadPool initThreadPool(HystrixThreadPool fromConstructor,
HystrixThreadPoolKey threadPoolKey,
HystrixThreadPoolProperties
.Setter threadPoolPropertiesDefaults) {
if (fromConstructor == null) {
// 获取默认的线程池
return HystrixThreadPool.Factory.getInstance(threadPoolKey, threadPoolPropertiesDefaults);
} else {
// 否则就用指定的
return fromConstructor;
}
}
2.2 使用线程池进行隔离的原因
- 很多应用都可能会执行大量的三方调用,这些三方服务由不同的团队维护,故而稳定性不同
- 每个服务都有自己的依赖,服务内部的依赖在上层是不关心的
- 可以避免级联依赖故障的时候服务的雪崩
- 通过线程池可以实现异步操作。
总之,通过线程池来隔离依赖服务可以很优雅的隔离那些经常发生变化的依赖服务从而保护整个系统的运行。但是它也有缺点
初始化了那么多的线程池,增加了额外的计算机资源,除了timcat本身的调用线程之外,还有hystrix自己管理的线程,但是如果合理的设置对应的参数值将会更有效的进行资源的利用。
每个 command 的执行都依托一个独立的线程,会进行排队,调度,还有上下文切换。
Hystrix 官方自己做了一个多线程异步带来的额外开销统计,通过对比多线程异步调用+同步调用得出,Netflix API 每天通过 Hystrix 执行 10 亿次调用,每个服务实例有 40 个以上的线程池,每个线程池有 10 个左右的线程。)最后发现说,用 Hystrix 的额外开销,就是给请求带来了 3ms 左右的延时,最多延时在 10ms 以内,相比于可用性和稳定性的提升,这是可以接受的。
3. Hystrix工作原理总结
image-20201007224841612-
构造一个 HystrixCommand 或者 HystrixObservableCommand 对象来执行以来请求,创建时需要传递对应的参数,如果请求只返回单一的结果使用 HystrixCommand,如果需要返回多个值,就需要使用HystrixObservableCommand,他们代表了对某个依赖服务发起的一次请求或者调用。创建的时候,可以在构造函数中传入任何需要的参数。
HystrixCommand command = new HystrixCommand(arg1, arg2); HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);
-
执行命令请求,有4种方法执行命令(execute/queue)只对HystrixCommand有用
- execute 阻塞直到收到调用的返回结果
- queue 返回一个Future,通过Future获取调用的返回值
- Observe 监听一个调用返回的Observable对象
- toObservable 返回一个Observable,当监听该Observable后hystrix命令将会执行并返回结果
K value = command.execute(); Future<K> fValue = command.queue(); Observable<K> ohValue = command.observe(); //hot observable Observable<K> ocValue = command.toObservable(); //cold observab
-
是否使用缓存,如果开启缓存的话,请求首先会返回缓存中的结果
-
是否开启熔断,如果已经熔断了,hystix将不会执行命令,直接fallback。
-
线程/队列/信号量是否已经满了,如果满了就不会执行命令,直接执行fallback
-
执:行对应的方法HystrixObservableCommand.construct() or HystrixCommand.run(), 如果方法抛出异常或者超时异常等,会执行fallback流程并且丢弃调用结果的返回值
-
熔断器计算: hystrix在成功、失败、拒绝、timeout时会上报到熔断器模块,熔断器会计算当前的熔断状态,一旦判断可以熔断就修改熔断的状态,此后所有的请求都不会执行命令。
-
执行fallback当命令执行失败、方法抛出异常、超时、熔断器被熔断、线程池、队列、信号量使用完之后,会执行fallback,通过fallback优雅降级
-
返回成功结果,如果hystrix命令执行成功,将会返回一个Observable,根据调用的方法转换成对应的响应结果
4. Hystrix 限流
4.1 服务限流
服务的限流就是为了处理系统资源和访问量出现矛盾的时候,保证有限的资源能够正常服务而对系统按照预设的规则进行流量的限制的一种手段,说白了就是保证一部分人没问题,而不是大家都有问题。
归根结底,服务限流还是因为服务资源的有限,假设系统只能抗住1kw的用户同时访问,突然来了3kw的用户,如果不限流,系统比较容易崩溃,这3kw的用户体验都不好,那么加上限流,我们至少可以保证有1kw的用户在正常使用着的。当然如果是长期3kw那就要做架构上的升级了,比如扩容之类的。当时有时候就是会出现一些我们无法预料的大流量,比如在我们公司,经常就有一些头部主播做活动而导致QPS飙升,所以一般我们日常是5倍容量的正常运行。一般的限流通常指的是以下的集中模式
- 熔断: 当系统出现问题短时间内无法修复就需要系统自动熔断,拒绝流量访问熔断的点,避免级联故障系统雪崩
- 降级:当系统功能出现问题,特别是依赖的服务出现问题,就需要将非核心的功能降级掉,待恢复的时候再启动
- 特权处理: 对请求进行分类,含有某个标识的优先保障请求,其它的延迟处理或者不处理
- 延迟处理:比如下单完成后加积分,可以使用消息队列削峰填谷,或者放在流量缓冲池,等待流量过去之后再进行处理,都会有一定程度的延迟。
而实际的项目中,为了实现对访问流量的限制,可采用以下技术手段
- 熔断: 前面所说,可以让流量对有问题的熔断点拒绝访问
- 计数器方法:系统维护了一个计数器,来一个请求的话计数器+1,完成一个请求的处理之后计数器再-1,当计数器大于指定的阈值之后,就拒绝后续的请求
- 队列方法:基于FIFO或者优先队列,请求先到队列中排队处理,然后应用程序从队里里面取出待处理的请求处理
- 令牌桶算法:还是基于一个队列,请求放到队里,但是除了队列外,还设置了一个令牌桶,有单独的脚本任务按照设定的速度往令牌桶中放令牌,后端每次处理都从桶里面拿出一个令牌,如果令牌拿完了那就不能处理请求了,如果想要控制的话就控制脚本放令牌到令牌桶的速度来达到控制后端处理速度实现动态的控制流量。
限流的注意事项:
- 实时监控:系统需要一个监控来保证实时看到限流的检测和处理进度以及资源的情况
- 手动开关:机器自动限流固然好,但是系统需要手动的开关保证人工可以随时介入
- 性能:限流因为多了一些处理(比如AOP包了方法,上报监控和一些判断等)多多少少都会对系统的性能有一些影响的,系统的故障和突发的流量都是不可避免的,所以我们需要合理的对系统进行限流,做好对应的预案。
4.2 常见的限流算法实现
4.2.1 计数器限流:固定窗口和滑动窗口
1、 固定窗口限流
思想是
- 将时间划分为多个窗口
- 在每个窗口内有一次请求就计数器+1
固定窗口限流比较简单,就是限制单位时间内的请求数,比如QPS为10,那么每秒钟计数1次,每次计数前判断是否达到10,如果达到10了就拒绝后续的请求,然后1s后计数清零。一个明显的弊端就是无法处理突刺流量,比如限流10QPS,第1ms来了10个请求,后续999ms所有的请求都会被拒绝掉。
image-202010081444348562、滑动窗口限流
思想是:
- 将时间划分多个区间
- 在每个区间内每个请求都讲计数器+1维持一个时间窗口,占据多个空间
- 滑动窗口计数器是通过将窗口再细分,并且按照时间滑动避免了固定窗口的弊端,但是时间区间的精度不好把控,算法所需要的空间就比较大
为了解决固定窗口的弊端问题,滑动窗口把窗口细化,用更小的窗口来限制流量,比如1分钟内的固定窗口切分为60个1s的滑动窗口,然后统计的时间范围随着时间的推移同步后移,也就是统计的周期是滑动的,但是这种限流方式还是不能防止在细微时间粒度上面的访问过于集中的问题,于是有了很多的改进版本,比如多层次限流,限制1分钟多少,1ms多少,10ms多少。
image-202010081446020444.2.2 桶限流令牌桶漏桶限流
1、桶限流方式
一个脚本在往桶里塞 token 令牌,请求到达应用程序之前,就需要先取得一个令牌才可以请求到应用程序,否则就在堵塞队列中等待。通过控制令牌的产生速度来控制处理的速度。相当于加了一层安全的保护流程,这个思想就类似我们到景区游玩坐缆车的时候,景区通过控制当前缆车票据的售卖个数来控制缆车内的人数。
image-20201008143647373 image-202010081448343372、漏桶限流的方式
固定一个桶,这个桶的出水速率是恒定的,请求不断往桶里放,然后桶的下面是应用程序不断的在处理这些请求消耗桶内的请求。这样做的好处是当进桶的流量比出桶的速度快的时候,就会溢出无法接受新的请求,其实就是个阻塞队列的实现思想。
image-20201008143755419 image-202010081448133735. 面试相关
-
Hystrix 是啥?
Hystrix 是开源的分布式系统延迟和容错库,能够像保险丝一样给系统提供过载保护等,防止级联的失败造成分布式系统服务的雪崩。此外它提供了一些在分布式系统中稳定性相关的特性,服务的降级、熔断以及资源的隔离等。
-
sentinel和hystrix对比
Sentinel 是面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。对比如下: image-20201008105956827 -
hystrix的设计原则:
- 对依赖的服务调用时出现延迟和失败进行控制和容错
- 在复杂的分布式系统中,阻止某个依赖服务的故障在整个系统中蔓延
- 提供fail-fast和快速回复的支持
- 提供fallback优雅降级的支持
- 支持近实时的监控、报警、运维操作等
附: HystrixCommand 参数
@HystrixCommand是 Hystrix提供的注解,标识在方法上代表组件作用于方法,它的常用参数如下:
-
fallbackMethod
: 当服务发生异常时要去降级执行的方法 -
ignoreExceptions
: 忽略指定的异常,发生该异常时不进行降级处理 -
CommandKey
: 降级的方法签名,默认使用方法名 -
groupKey
: 分组命令键,用于Hystrix 根据不同的分组来统计命令的调用次数、告警灯信息 -
threadPoolKey
: 线程池名称,用于资源的隔离,不配置的话默认使用同一个线程池
网友评论