一、背景
Hystrix是Netlifx开源的一款容错框架,防雪崩利器,具备服务降级,服务熔断,依赖隔离,监控(Hystrix Dashboard)等功能。
尽管说Hystrix官方已不再维护,且有Alibaba Sentinel等新框架选择,但从组件成熟度和应用案例等方面看,其实还是有很多项目在继续使用Hystrix中,本人所参与的项目就是其一。故结合个人的Hystrix实战经验与大家分享交流。
二、经验总结
2.1 隔离策略的选择
Hystrix提供两种资源隔离策略,线程池和信号量。它们之间的异同点如下:
|
线程池隔离 |
信号量隔离 |
线程 |
与调用线程非相同线程 |
与调用线程相同(tomcat线程) |
开销 |
排队、调度、上下文切换 |
无线程切换,开销低 |
异步 |
支持 |
不支持 |
并发支持 |
支持(最大线程池大小) |
支持(最大信号量上限) |
当请求的服务网络开销比较大,或者是请求比较好时,我们最好使用线程隔离策略,这样的策略,可以保证大量的容器(tomcat)线程可用,不会因服务原因,一直处于阻塞或等待状态,快速失败返回;
而在使用缓存(本地内存缓存更适合该场景,Redis等网络缓存需要评估)时,我们可以使用信号量隔离策略,因为这类服务响应快,不会占用容器线程太长时间,而且也减少了线程切换的一些开销,提高了服务效率。
具体使用哪种策略,需根据业务场景综合评估。一般情况下,推荐使用线程池隔离。
2.2 线程池大小与超时时间设置
在线程池隔离策略下,线程池大小及超时时间的设置至关重要,直接影响着系统服务的响应能力。如线程池大小若设置的太大会造成资源浪费及线程切换等开销;若设置的太小又支撑不了用户请求,造成请求排队。而超时时间设置的太长会出现部分长耗时请求阻塞线程,造成其它正常请求排队等待;若设置的太短又会造成太多正常请求被熔断。
对此Hystrix官方给的建议如图:
即转换为以下计算公式:
- 线程池大小 = 服务TP99响应时长(单位秒) * 每秒请求量 + 冗余缓冲值
- 超时时间(单位毫秒) = 1000(毫秒) / 每秒请求量
例如某服务TP99情况下每秒钟会接收30个请求,然后每个请求的响应时长是200ms,按如上公式计算可得:
线程池大小 = 0.2 * 30 + 4(冗余缓冲值)= 10,超时时间 = 300ms
2.3 注解叠加
在实际开发中可能会遇到某外部调用方法有Hystrix注解与其它注解一起使用的情况,例如查询方法加上缓存注解。此时需特别注意注解间的执行顺序,避免出现非预期的结果:
- 缓存注解未生效此时Hystrix注解切面的执行是在最外层,由于Hystrix内部执行是通过ProceedingJoinPoint.getTarget()获取目标对象,使用反射调用的方式直接执行到目标对象方法上,从而造成中间其它注解逻辑丢失。可通过指定注解执行顺序@Order解决保证Hystrix注解执行在最里层。
- 因缓存异常造成该查询方法被熔断如果Hystrix注解切面的执行是在最外层,此时Hystrix熔断管理的方法逻辑除了第三方服务远程调用,也包括了缓存调用逻辑。如果缓存调用出现异常就会算作整个方法异常,从而引起整个方法被熔断。
2.4 服务的异常处理
先给大家时间看如下代码,检查是否存在问题:
@HystrixCommand(fallbackMethod="queryUserByIdFallback")
public User queryUserById(String userId) {
if(StringUtils.isEmpty(userId)) {
throw new BizException("参数不合法");
}
Result<User> result;
try {
result = userFacade.queryById(userId);
} catch(Exception e) {
log.error("query user error. id={}", id, e);
}
if(result != null && result.isSuccess()) {
return result.getData();
}
return null;
}
Hystrix在运行过程中会根据调用请求的成功率或失败率信息来确定每个依赖命令的熔断器是否打开。如果打开,后续的请求都会被拒绝。由此可见,对异常的控制是Hystrix运行效果起很大影响。
再回头看上面的例子,会发现两个异常处理问题:
- 参数校验不通过时的异常处理非法参数校验等非系统调用的异常失败不应该影响熔断逻辑,不应该算作失败统计范围内。对此优化建议是将参数校验放到远程调用封装方法的外面,或者封装成HystrixBadRequestException进行抛出。因为在Hystrix内部逻辑中HystrixBadRequestException异常已默认为不算作失败统计范围内。
- try-catch远程调用的异常处理对远程服务的直接调用进行try-catch会把异常直接“吞掉”,会直接造成Hystrix获取不到网络异常等服务不可用异常。建议在catch日志记录处理后将异常再throw出来。
2.5 fallback方法
Hystrix在依赖服务调用时通过增加fallback方法返回默认值的方式来支持服务优雅降级。但fallback的使用也有很多需要注意的地方,大致总结如下:
- fallback 方法访问级别、参数等要与对应依赖服务一致对于需要获取触发fallback的异常实例,可以通过fallback方法增加Throwable类型参数(加到最后一个参数)即可。
- fallback 方法中执行的逻辑尽量轻量,如用本地缓存或静态默认值,避免远程调用
- 如果fallback方法里有远程调用,建议也使用Hystrix包装起来,且保证与主命令线程池的隔离
- 对于写操作的远程调用不建议使用fallback降级写服务的调用失败可以直接抛出给方法调用侧进行业务判断。
2.6 groupKey、commandKey、threadPoolKey
在使用Hystrix开发中肯定都见过这三个key,但很多人并不理解这三个key的意义以及对Hystrix的作用,尤其是threadPooKey,故在此总结下:
- gr