欢迎大家关注公众号【离心计划】,更多高质量的教程系列,一起离开地球表面,逃离舒适圈
| 背景
事情是这样的,手上的一个项目使用了线程池来处理大量的异步任务,起初的目的是为了并行处理减少耗时而已,但是前天流量由于其他同事的离线job陡增了数十倍,我们先不讨论这个规范性问题。由于流量尖峰,服务出现了超时,引发上游服务拿不到结果,而前端对他们服务端的超时配置不合理,直接引发了白页,听起来很离谱
,大致的问题请求流程如下:
| 排查
出现超时但是服务的CPU占用却不高,没有触发HPA(动态扩容),我们查看了集群某台机器的线程情况大量线程TimeWait,并且定位到了问题代码,首先我们看我们的线程池定义(示例)
ThreadPoolExecutor tpe = new ThreadPoolExecutor(coreNum,coreNum*2,
100L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),namedThreadFactory);
问题如下:
-
队列长度没限制
-
拒绝策略走到最好添加日志或者告警
然后最关键的地方,这个项目里面存在这样的父子任务关系:
/**
* 父任务,里面异步执行子任务
*/
static class FatherTaskTwo implements Callable<String> {
@Override
public String call() throws Exception {
SonTask sonTask = new SonTask();
Future<String> future = tpe.submit(sonTask);
return future.get();
}
}
/**
* 子任务
*/
static class SonTask implements Callable<String> {
@Override
public String call() throws Exception {
//处理一些业务逻辑
return null;
}
}
public static void main(String[] args) throws Exception {
FatherTaskTwo fatherTaskTwo = new FatherTaskTwo();
Future<String> future = tpe.submit(fatherTaskTwo);
//主线程添加超时时间get阻塞
future.get(1000L, TimeUnit.MILLISECONDS);
}
这里有几个问题:
1. 父任务get子任务的结果没有设置超时时间
2. 父子任务共用了一个线程池
这会导致什么问题呢?就是今天的关键:死锁
我们假设最大有四个线程同时处理,我们正常的情况可能像这样:
ok,为什么这是正常情况,可以想象下子任务1和子任务2会被很快执行完,那么子任务3和4就会马上被执行,但是父任务3和父任务4是要等到子任务3和子任务4执行完才会返回结果的,因为父子有依赖关系,并且父任务中get子任务是 没有设置超时时间,但是这种情况还好,因为子任务34在子任务12执行完后可以拿到线程资源去执行掉,进而把父任务34也消化掉,但是如果是下面这样的情况:
那么就出现了四个父任务都无法消化掉,原因在于:
-
父任务都依赖自己的子任务
-
父任务没有超时时间,会一直阻塞,打破不了死锁的条件
这就形成了死锁,由于阻塞挂起线程所以CPU也不会由于线程池的问题而升高
| 结论
虽然整条事件链路有许多不合理的地方,比如服务之间的降级、Online/Offline的边界划分等等,但是我们主要关注应用内部的问题,主要是错误使用线程池导致的死锁,那么我们的解决方案在不同角度有大致几种:
-
线程池隔离,有依赖关系的线程在不同池内处理
-
一定要设置get超时时间
-
非必要不要拆这么多层级任务,尽量扁平化