记几次 [线上环境] Dubbo 线程池占满原因分析(第二次:CompletableFuture)

发布时间 2023-04-04 18:28:53作者: 小学生II

转载:https://blog.csdn.net/wsmalltiger/article/details/124236189

文章目录
[线上环境] Dubbo 线程池占满原因排查系列
前言
一、问题分析
1、分析日志
2、定位原因
二、解决方案
三、总结
前言
  某天早上9点左右收到线上故障报警,超过3个商家反馈“无法正常进入功能页面,点击相关操作提示报错”。
  经过排查定位,故障应用线上部署了30台机器(4c8g),由于代码中使用 CompletableFuture不规范引起部分机器(8/30台)dubbo 线程池耗尽出现故障。而我们的dubbo框架采用随机负载均衡策略,导致故障机器还会有流量过去,打在故障机器的请求都会出现异常。

一、问题分析
1、分析日志
  商家反馈的是“无法正常进入页面,点击操作报错”,所以第一反应就是去看看线上是否有ERROR日志,如果能够找到报错日志那么就能很快定位到问题的原因,先看前端node应用系统日志:

根据上面日志信息很快得到两个结论:
1、在9:09至9:23期间出现了较多的error日志,目前已经逐步平稳;
2、问题期间后端接口出现了dubbo线程池占满的情况。

继续查看下游后端应用日志,发现dubbo线程池占满的还是更底层的一个应用(底层应用dubbo线程池占满引起了雪崩效应):

根据上游日志traceId(调用链ID,上下游所有相关系统都会打印,方便查找整条请求链路的日志)继续排查底层应用日志:

这里发现同一个请求在底层应用中产生了大量的日志,这明显不正常。继续查看dubbo服务监控,发现底层应用确实出现了大量的dubbo线程池占满的情况,线程队列堆积较多:

底层应用接口RT情况统计,可以看到由于dubbo线程池出现排队,RT普遍增高:

这里基本已经定位到问题应用了,接下来需要继续排查下具体的原因。


2、定位原因
  根据上面定位到的应用和日志,分析发现 ForkJoinPool.commonPoll-worker 线程日志有明显增高:

   review 日志相关的代码逻辑,发现内部有较多地方使用 CompletableFuture 并发处理业务请求,且使用了默认线程池 ForkJoinPool。(CompletableFuture 是java 8 JUC库新增的主要工具,同传统的Future相比,其支持流式计算、函数式编程、完成通知、自定义异常处理等很多新的特性)
   问题代码截图:

这里调用了 CompletableFuture.supplyAsync 方法,查看方法源码:

这里使用了 asyncPool,继续查看定义,发现默认会使用 ForkJoinPool 线程池:

   CompletableFuture.supplyAsync 方法有两个,我们出问题的代码使用了第一个方法,就会使用默认的线程池,而另一个方法的第二个参数支持自定义线程池。由于这个应用业务的复杂性还有很多代码都使用到了CompletableFuture,并且都使用默认的线程池。

这里已经确认了问题原因:
1、应用多处使用了 CompletableFuture.supplyAsync 方法,但是都使用的是默认线程池,所以当多个业务并发过大时会引起线程阻塞,由于入口是dubbo请求过来,进而也会导致dubbo线程出现阻塞;
2、代码在调用 CompletableFuture.allOf(…).get() 方法时,入参没有传超时时间,也就是说如果线程产生阻塞会一直等待,直到有结果返回,这里再次把问题放大了。


编写测试代码,模拟线上场景运行相关业务代码,确实能够复现线程池出现堆积的情况:

 

二、解决方案
1、对问题代码 CompletableFuture 的线程池进行了自定义,不在使用默认的线程池,避免影响其他业务;

2、对 CompletableFuture.allOf(…).get() 方法增加了超时时间参数,如果在一定时间内没有返回结果,则抛出超时异常,避免长时间占用系统资源,优化后代码;

CompletableFuture.allOf(futureList.toArray(new CompletableFuture[]{})).get(timeOut,TimeUnit.MILLISECONDS);
1
3、举一反三,我们统一梳理了应用中使用到 CompletableFuture 的地方,根据业务使用场景 自定义了线程池和超时时间参数设置。这样能够保证一个业务发生性能瓶颈时不会影响其他业务或者一定范围缩小问题的影响面;


三、总结
1、常用的开源框架大多都非常的成熟,但是业务代码开发同学如果使用姿势不当,也会引起比较大的问题。
2、在集群部署环境下如果出现线程池队列占满的情况,及时依次重启服务(清空队列)也是一种能够快速恢复问题的方法,但是问题的根因还是需要定位到并且彻底解决的。
3、学会举一反三,一个地方出现了问题,那么检查一下系统其他地方是否也有相同的问题。问题不暴露不代表它不存在,举一反三及时排掉“未爆炸的雷”,也是对系统稳定性的一种保障。

————————————————
版权声明:本文为CSDN博主「smatiger」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wsmalltiger/article/details/124236189