Hystrix熔断优化

[原创]个人理解,请批判接受,有误请指正。转载请注明出处: https://heyfl.gitee.io/design/Hystrix-optimization.html

简述

本文说的其实就是

  • 合理配置熔断,防止依赖的第三方接口响应过慢导致系统tomcat链接大量阻塞,最终导致系统崩溃的问题
  • 顺便,将熔断配置从配置文件中提取出来,动态配置中心中,这样就可以通过动态配置中心来动态配置熔断参数了
  • 除此,主要是团队里大家对单线程并发数QPS概念有些混淆且计算方式了解不多,以及信号量线程池方案选择上有些歧义,需要花了不少时间在会议上让团队达成一致。

背景&分析问题

SISP客服系统、SISP查单系统等系统提供的服务都通过HTTP依赖于大量外部各种接口的响应,
这些接口的响应时间不可控,有时候会出现响应时间过长的情况,这时候如果不做任何处理,那么这些请求就会一直等待,这样就会导致系统的响应时间过长,甚至出现系统崩溃的情况。

双十一期间,SISP客服系统出现了系统崩溃问题,经排查发现tomcat线程池被耗尽,进一步排查是因为所依赖的PIS接口响应慢了
简单来说问题大概长这样:
img.png

其实在进入团队前,其实关键服务已经配置了相关的熔断,不过仔细看了下配置:execution.isolation.semaphore.maxConcurrentRequests=100000
这代表着需要同时有100000个线程进入该程序里才有可能触发熔断,在这种接口响应缓慢要死不死的情况下简直形同虚设
而hystrix错误率50%因为时间窗口太短+外部接口可用性也并不是在50%以下,难以触发,故难以触发熔断机制,导致系统崩溃;
以下是系统曾经的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@HystrixCommand(
fallbackMethod = "singleBack",
commandProperties = {
@HystrixProperty(name = "execution.timeout.enabled", value = "false"),
// 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候,
// 当在配置时间窗口内达到此数量19的失败后,进行短路,默认20。
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
// 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过 circuitBreaker.requestVolumeThreshold 的情况下,
// 如果错误请求数的百分比超过50, 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
// 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间.默认:10000
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "10000"),
// 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后,会将断路器置为 "半开" 状态,尝试熔断的请求命令,
// 如果依然失败就将断路器继续设置为 "打开" 状态,如果成功就设置为 "关闭" 状态。默认:5000
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),
@HystrixProperty(name = "execution.isolation.strategy", value = "SEMAPHORE"),
@HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "100000")
})

方案论述

看出问题就得出方案了,其实上面问题总结来说就2点:
1. 熔断配置在配置文件中,不方便动态调整(人看着都已经出问题了,还不能手动熔断快速恢复,领导都急疯了hhh)
2. 熔断配置maxConcurrentRequests不合理,导致熔断不起作用

maxConcurrentRequests配置

对于1.就不做过多叙述了,对于2.这里与团队成员有点分歧。。。
他们认为maxConcurrentRequests应该配置为接口的并发数。。。实际上,这个配置应该是单个节点最大并发数,而不是接口的并发数,这里花了我不少时间给团队成员解释这个问题。。。有点醉了。。。

这里简单说下,如果是单节点调用,那么maxConcurrentRequests就是接口的并发数,如果是多节点调用,那么maxConcurrentRequests就是单个节点的并发数
所以得出QPS和RT的关系:

  • 对于单线程:QPS=1000/RT
  • 对于多线程:QPS=1000*单节点并发线程数量/RT
  • 对于多线程多接点:QPS=1000单节点并发线程数量节点数量/RT

因此,我们可以得出:

1
2
3
maxConcurrentRequests(单节点线程数) = QPS /节点数/ ( 1000 / 被熔断方法的P99耗时ms )
即:
27000/128/(1000/20)=4.2QPS

所以得出,maxConcurrentRequests配置为5,理论上即可满足需求。
考虑到我们主要是为了解决单节点因为单个服务耗光tomcat容器所有http线程,这个值不需要设置得太严格,只要能保证单节点不会因为单个服务耗光tomcat容器所有http线程即可。所以,我们保守地设置为100

其实查阅官网,maxConcurrentRequests默认是10,并且说对于绝大部分、正常服务,一般来说都不需要修改这个值,他可以很好的满足绝大部分的场景,一定程度上说明我们上面求出的服务的maxConcurrentRequests=5也算是合理的

Other More

部门新来了架构师,在我在团队方案论述的说Hystrix就得用线程池,不要用信号量,他们以前公司就是这样用的,他和网上也推荐这样用,说可以异步、不占用tomcat线程什么的。
也许这个架构师不太理解我们的系统,又或者多Hystrix有什么误解,又或者过于相信网上的言论,当然,也可能是我个人理解有误,但是我觉得这个方案是不合理的,我这里说下我的理解:

(信号量和线程池的区别这里就不多叙述了,自查吧 )

  • 因为我们系统有大量面向前端的服务,这些服务很多都依赖于其他第三方接口服务,如果每个都加上熔断,每个都设置自己的线程池,那么将会是一个恐怖的线程开销
  • 同时,我们99%的前端、后端业务调用这些服务,都需要这些服务有一个同步的响应,因此额外开线程池,释放tomcat线程就无从说起了,因此线程池的方式并不适合我们的场景。

因此当时否决回去了,但因团队成员坚持保守看法,所以后面又开了个会论述了一下这个事,终于如愿达成一致,最后还是采用了信号量的方式。

关键代码

动态配置关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 描述:熔断配置加载类,项目启动时会加载一次,然后修改动态配置中心的配置时会自动加载
*
* <pre>
* HISTORY
* ****************************************************************************
* ID DATE PERSON REASON
* 1 2022/7/6 01390559 Create
* ****************************************************************************
* </pre>
*
* @author Chris Cai
* @version 1.0
*/
@Component
@DisconfFile(filename = HystrixConfig.HYSTRIX_CONFIG_NAME)
@DisconfUpdateService(classes = {HystrixConfig.class})
public class HystrixConfig implements InitializingBean {
/**
* 熔断配置文件名
*/
public static final String HYSTRIX_CONFIG_NAME = "hystrix.properties";
private static final Logger logger = LoggerFactory.getLogger(HystrixConfig.class);
private static final int REFRESH_TIME = (int) TimeUnit.SECONDS.toMillis(30L);


@Override
public void afterPropertiesSet() {
// 读取配置文件实现类
PolledConfigurationSource source = newConfigurationSource();
// 设置定时器,每隔30s检查一次读取一次配置文件
AbstractPollingScheduler scheduler = new FixedDelayPollingScheduler(REFRESH_TIME, REFRESH_TIME, false);
// 创建动态配置类,并设置定时器和配置文件读取类,如配置发生变化,会更新对应的配置属性
DynamicConfiguration configuration = new DynamicConfiguration(source, scheduler);
// 加入配置管理器
ConfigurationManager.install(configuration);
}

// 具体轮询业务逻辑
private PolledConfigurationSource newConfigurationSource() {
return (initial, checkPoint) -> {
Properties properties = new Properties();
try (InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(HYSTRIX_CONFIG_NAME)) {
properties.load(is);
} catch (Exception e) {
logger.error("fail load hystrix configs", e);
throw e;
}
return PollResult.createFull((Map) properties);
};
}
}

熔断配置文件

各个服务通用default,各个服务可以单独配置,配置会覆盖default配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

### 全局默认配置 实例配置刷新清空时会临时取全局默认配置
## 执行/隔离策略
hystrix.command.default.execution.isolation.strategy=SEMAPHORE
## 最大同时处理请求数
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests=100000

hystrix.command.default.execution.timeout.enabled=false

#熔断开关
hystrix.command.default.circuitBreaker.enabled=true
#熔断开关 如果为true 将会拒绝所有请求进入降级方法
hystrix.command.default.circuitBreaker.forceOpen=false
#熔断开关 如果为true 将会强制关闭熔断功能,无论错误百分比如何,它都会允许请求
hystrix.command.default.circuitBreaker.forceClosed=false
##该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候,
##当在配置时间窗口内达到此数量19的失败后,进行短路,默认20。
hystrix.command.default.circuitBreaker.requestVolumeThreshold=20
## 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过 circuitBreaker.requestVolumeThreshold 的情况下,
## 如果错误请求数的百分比超过50, 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
##该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后,会将断路器置为 "半开" 状态,尝试熔断的请求命令,
##如果依然失败就将断路器继续设置为 "打开" 状态,如果成功就设置为 "关闭" 状态。默认:5000
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5000
##滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间.默认:10000毫秒.
hystrix.command.default.metrics.rollingStats.timeInMilliseconds=10000


### 实例配置 ###

### 备注脱敏接口熔断配置
## 最大同时处理请求数
hystrix.command.remarkMask.execution.isolation.semaphore.maxConcurrentRequests=100



### 运单查询接口熔断配置
## 最大同时处理请求数
hystrix.command.waybillQuery.execution.isolation.semaphore.maxConcurrentRequests=100


### 批量运单查询接口熔断配置
## 最大同时处理请求数
hystrix.command.batchWaybillQuery.execution.isolation.semaphore.maxConcurrentRequests=100



### 批量子单查询接口熔断配置
## 最大同时处理请求数
hystrix.command.batchChildWaybillQuery.execution.isolation.semaphore.maxConcurrentRequests=100


### 历史运单查询接口熔断配置
## 最大同时处理请求数
hystrix.command.hisWaybillQuery.execution.isolation.semaphore.maxConcurrentRequests=100



### 操作运单DSL查询接口熔断配置
## 最大同时处理请求数
hystrix.command.dslWaybillQuery.execution.isolation.semaphore.maxConcurrentRequests=100


### 综合查单接口熔断配置
## 最大同时处理请求数
hystrix.command.conditionsWaybillQuery.execution.isolation.semaphore.maxConcurrentRequests=100


### PIS时效查询接口熔断配置
## 最大同时处理请求数
hystrix.command.pisTimeQuery.execution.isolation.semaphore.maxConcurrentRequests=100


### 对内路由查询接口熔断配置
## 最大同时处理请求数
hystrix.command.insideRoute.execution.isolation.semaphore.maxConcurrentRequests=100


### 对外路由查询接口熔断配置
## 最大同时处理请求数
hystrix.command.outsideRoute.execution.isolation.semaphore.maxConcurrentRequests=100

作者

神奇宝贝大师

发布于

2022-09-12

更新于

2022-09-13

许可协议

评论