记一次线上使用线程池不当引起的线程卡死问题

2023年 9月 12日 96.4k 0

1. 引言

Hi,你好,我是有清

在金融系统中,业务人员可以通过页面点击来修改当前产品的保费利率,然后触发一系列的业务逻辑

近几天,业务人员反馈偶尔修改产品的保费利率页面会卡死,但是刷新页面后重试,往往都能成功....

红线要紧,下文中出现的代码,均为伪代码

2. 排查过程

2.1. 接口问题排查

在业务人员反馈问题发生的时间点前后,查看该接口的日志,的确存在大量超时的情况

最后的日志都停在了 countDownLatch 的 await 前

尾部宣传图 (2).png

这边结合上方伪代码,顺便科普一下 counrDownLatch 的用法

  • 初始化 countDownLatch,传入初始化参数,比如传入 3
  • 在 Task 任务中,使用 countDownLatch.countDown() 对 3 进行 -- 操作
  • 当 3 被子线程减到 0 的情况下,countDownLatch.await() 会自动被唤醒往下走,如果没有加超时时间线程会被持续夯住,如果加了超时时间,在时间到达之后则会进行异常抛出

那这个情况下,很显然 main 线程被阻塞住了,无法继续往下走,而且有意思的是 Task 中的日志均没有打印

那么即子线程中的任务都没有执行,没有进行 countDownLatch.countDown() 操作,导致可怜的 main 线程一直苦苦等待

2.2. 子线程任务未执行

那么为何子线程中的任务没有执行呢?

开始经典老八股了,线程池的工作流程是什么样的呢?请默写并背诵

  • 新任务进来,开启线程执行,直到核心线程数满载
  • 任务继续进来,核心线程数满载,将任务放到队列中,直到任务队列满载
  • 任务继续进来,任务队列满载,开启最大线程数执行,直到最大线程数满载
  • 任务继续进来,任务根据具体的拒绝策略,进行对应的处理

根据这个套路,我们的子任务未执行,可能的情况只有两个

  • 任务还在队列里,还没轮到他执行
  • 任务被抛弃了,根本没有执行到

但其实我们重写了拒绝策略,一旦任务被抛弃,我们会主动抛出出我们的业务异常,但是对日志进行关键字搜索,并没有发现该异常

那么真相只有一个,任务还在队列里,还没轮到他执行

但是很可惜,我们没有直接对线程池的可视化界面,但是我们可以借助 Arthas 的 watch 去佐证我们的判断,再对比正确机器和错误机器的线程队列大小之后,我们的推测是正确

那么,为什么子线程的任务不执行呢?我们继续看代码

2.3. 子线程代码排查

先看一波代码

carbon

在子线程中,竟然又开了新的子线程去执行新任务

Main 线程开启 2 个 task 子线程,这 2 个子线程又开启 2 个 subTask 子线程

一图胜千言

image.png

当主线程进来的时候,任务 1 开始执行,什么时候执行完成呢?必须等等 任务1-1 和 任务 1-2执行完成后,才能继续执行,但是这些任务队列还没满,有没有新的线程去处理这些任务,就导致线程池中任务一直无法被执行

概括一下就是:主线程等待 任务1,任务1 等待任务 1-1,1-2,但是1-1、1-2 无人执行,任务1 卡住,任务1卡住,任务 2 卡住,任务 1、2 卡住,主线程卡住

完整伪代码如下,感兴趣的同学可以看下

public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 3, 2, TimeUnit.MINUTES, new LinkedBlockingQueue(7));
List futures = Lists.newArrayList();
int taskCount = 2;
CountDownLatch countDownLatch = new CountDownLatch(taskCount);
int i = 1;
while (i < taskCount) {
Task task = new Task(countDownLatch, threadPoolExecutor, String.valueOf(i));
Future res = threadPoolExecutor.submit(task);
futures.add(res);
i += 1;
}
try {
countDownLatch.await();
} catch (Exception e) {

}
}

static class Task implements Callable {

private CountDownLatch countDownLatch;
private ThreadPoolExecutor gocExecutors;
private String taskId;

public Task(CountDownLatch countDownLatch, ThreadPoolExecutor threadPoolExecutor, String taskId) {
this.countDownLatch = countDownLatch;
this.gocExecutors = threadPoolExecutor;
this.taskId = taskId;
}

@Override
public String call() throws Exception {

Thread.sleep(300);

int taskCounts = 2;

CountDownLatch cl = new CountDownLatch(taskCounts);

List futures = Lists.newArrayList();

for (int i = 1; i

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论