背景:最近因为一些外部原因导致部分线上应用大面积短时报异常,现象为线程池被打满,异步请求经常被hang住或超时。虽然业界已不再维护hystrix转而使用sentinel,但由于项目中使用的OpenFeign集成了hystrix,因此借此机会系统了解下其原理。
一、Hystrix特性
线程隔离
- 分类:线程池隔离,信号量隔离
- 作用:提高系统稳定性和可用性
- 维度:默认服务维度,可自定义
熔断
- 状态:关闭状态,半开状态,断开状态
- 作用:保护下游服务;保护本系统不会因资源被占满而拖垮
- 维度:方法维度
降级
- 正常逻辑执行失败后走降级逻辑
- 作用:下游出问题时依然能提供有限的服务
- 维度:方法维度
缓存
- 命中缓存后直接返回结果
- 作用:快速返回结果;不占用重要资源
- 维度:方法维度
二、原理
基本概念
- groupKey:全局唯一标识服务分组的名称,默认为服务名,对应一个service
- commandKey:全局唯一标识服务的名称,默认为方法名,对应一个method
- threadPoolKey:全局唯一标识线程池的名称,多个服务分组可以配置同一个线程池;在线程池隔离模式下,如果不配置,相同的groupKey(service)会使用同一线程池
线程隔离
隔离模式 | 隔离维度 | 业务线程与请求线程的关系 | 优缺点 | 适用场景 |
---|---|---|---|---|
线程池隔离 | 有指定线程池则使用指定的 没有则groupKey(service)下共用 | 不同 | 支持异步,支持超时 线程切换开销大 | 绝大多数情况 |
信号量隔离 | 无此概念 | 相同 | 无线程切换 不支持异步和超时 | 纯内存操作 |
线程池默认配置
- 核心线程数:10
- 最大线程数:10
- 空闲线程最大存活时间:1分钟
- 队列最大长度:-1
- 队列:SynchronousQueue,无界阻塞队列
拒绝策略:AbortPolicy,丢弃并抛异常
说明:若无明确配置,OpenFeign中使用的线程池即为上述默认配置,且为group维度,即每个service一个默认配置的线程池
默认配置的问题
- 核心线程数为10,一旦因下游服务或网络原因导致某瞬时线程使用量上去后就不会再降下来,白白浪费线程
- 最大线程数为10,高并发量的请求将导致排队
- 无界队列可能导致故障发生时,请求被严重积压
熔断
状态 | 请求是否放行 | 状态转换条件 | 说明 |
---|---|---|---|
关闭 | 可以 | 请求总数达到设定值 & 失败率达到设定值,状态转为断开 | |
半开 | 部分可以 | 当断开时间超过设定值,且第一个重置断开时间戳成功的请求会被放行 有请求执行成功则状态转为关闭 | 半开其实不是一个持续的状态,而是一次逻辑判断的结果 |
断开 | 不可以 | 有请求执行成功则状态转为关闭 |
降级
- 如何使用:开启降级开关(默认)并指定目标方法对应的fallback方法
- 何时触发:
- 熔断状态下
- 资源不足时:线程池队列拒绝接收,信号量限制
- 非忽略的异常(可自定义):业务异常,网络异常,超时等
- 无降级时怎么处理?异常直接往上抛
缓存
-
什么维度:cacheKey维度,每个方法可自定义生成规则,默认为空,为空时不缓存
-
过期时间:底层为ConcurrentHashMap实现,理论上不过期,但可以手动或基于注解清除
-
什么时候获取:在判断断路器状态及是否有资源之前,意味着缓存有效性几乎不受任何限制
-
命中缓存是否计入统计:不计入
注意事项:
-
内存缓存,update时需要及时删除或更新
-
内存占用问题
-
分布式环境下可能存在一致性问题
执行流程
总体流程如下:
- 有缓存走缓存(紫色区域)
- 状态正常且资源未受限(蓝色区域) - 则执行业务逻辑(绿色区域)
- 否则执行fallback(黄色区域)
- 没有降级直接抛异常(红色区域)
三、应用
1、直接使用
继承HystrixCommand方式
public class SemaphoreCommand extends HystrixCommand<String> {
private final String name;
public SemaphoreCommand(String name) {
// 线程池隔离,默认方式
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("commandGroupKey"))
.andCommandKey(HystrixCommandKey.Factory.asKey("commandKey"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(10) // 至少有10个请求,熔断器才进行错误率的计算
.withCircuitBreakerSleepWindowInMilliseconds(5000) // 熔断器中断请求5秒后会进入半打开状态,放部分流量过去重试
.withCircuitBreakerErrorThresholdPercentage(50) // 错误率达到50%开启熔断保护
.withRequestCacheEnabled(true) // 支持请求缓存
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)// 指定隔离策略为信号量隔离,默认为线程池隔离
).andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(10))); // 10个线程的线程池
this.name = name;
}
@Override
protected String run() throws Exception {
// 线程名称在线程池隔离模式下为hystrix-commandGroupKey-n,在信号量模式下为主线程名main(或http线程名)
System.out.println(Thread.currentThread().getName());
return name;
}
}
2、注解方式
@HystrixCommand(ignoreExceptions = {BizException.class, RPCException.class}, fallbackMethod = "hystrixMethodFallback", commandProperties = {
@HystrixProperty(name = "execution.isolation.strategy", value = "SEMAPHORE"),
@HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "1")})
@GetMapping(value = "/hystrixMethod")
public Result<String> hystrixMethod1(Long sleepTime) throws InterruptedException {
log.info("hystrixMethod: " + Thread.currentThread().getName());
Thread.sleep(sleepTime);
// 有HystrixCommand的线程名为:hystrix-${controllerName}-n
return Result.success(Thread.currentThread().getName());
}
3、OpenFeign集成
开源框架OpenFeign集成了对hystrix的支持。
Feign的重试机制
需要说明的是,hystrix并没有重试机制,上次异常的重试是feign负责管理的,详见https://www.jianshu.com/p/503808e6041b
这也是为什么出现问题时会有大量重复异常的原因。
- 重试次数:默认为5(第一次也算
- 重试间隔:默认第一次100ms,以后每次为上一次的1.5倍,最大为1000ms
- 重试时机:异常为RetryableException时,如部分IOException
四、问题分析
应用都有哪些线程?
- http线程
- pigeon线程
- cat线程
- hystrix线程(feign封装)
- mq线程(消费及异步发送)
- 自定义线程池
- 同步工具创建的线程(ForkJoinPool等)
- 手动创建的线程
为什么线程数上去后往往就下不来了?
因为部分线程池定义了较高的核心线程数,即使不再使用也会一直维持这一数量
为什么RPC请求会hang住?
可能的原因:
- 下游接口RT增加或超时,调用下游的hystrix线程池被打满,请求进入队列等待调度,如果规定时间内还未被调度,则主(http)线程报超时异常
- feign依赖的底层http客户端(okhttp等)在等待获取连接
- pigeon原理类似,详见:http://techface.amh-group.com/doc/430
思考:线程数设置到多大合适?
呃,这是个稍微有点棘手的问题,先看下最多可以支持到多少?理论上是个比较大的值,一般为32768或者65536,当然实际情况下会受到多重因素的影响。
- 宿主机CPU核数或分配的核数
- Xmx与Xss取值:堆内存大小越大,栈内存就越小;单个栈大小越大,线程数就越少
再看下设置到多大合适?(只考虑固定CPU核数情况)
原则:尽量让CPU利用起来,同时要减少线程切换时间
假设只有一个线程池,首先要区分是CPU密集型、IO密集型还是混合任务型;实际场景下绝大多数是后两者
- CPU密集型:核心线程数=CPU个数+1,最大线程数=(线程等待时间/线程CPU时间 + 1)* CPU数目 * 安全系数
- IO密集型:核心线程数=2*CPU个数+1,最大线程数同上
- 混合型:介于CPU密集型与IO密集型之间
而现实中通常不止一个线程池,也不好明确划分类型,还需要预留一些资源作为buffer,所以要多方权衡,尽量把资源留给容易造成系统瓶颈的线程池使用。
参考资料
- https://blog.csdn.net/lan861698789/article/details/109711698
- https://www.cnblogs.com/xinzhao/p/11398534.html
- https://www.jianshu.com/p/503808e6041b