问题暴露
运维反馈线上服务线程数飙高,达到2w个线程
首次排查
根据经验, 我们认为在一个普通的web服务中 ,涉及线程的地方有 tomat请求线程 , 自定义线程池 , rpc调用远程服务http请求线程 几个地方, 所以我们首先排查了代码中,和 httpClient有关的位置 , 发现 使用 hutool的 Request包发送http请求 ,有编译提示:
我们对比了该任务的运行时间窗口,扫描数据范围 和 线程数增高的数量,发现大致吻合 ,初步确定由该位置引起 , 决定当晚停止该段代码射击的跑批任务后观察线程数
次日,我们观察线程数没有明显减少,感觉应该不是这个问题引起;
再次排查
发现问题没有那么简单,还是得从线程快照排查
使用jps命令,找到java进程:
jps
使用 jstack 快照进程线程信息
jstack 21 > thread_dump21.txt
使用 ps -ef -L ,列出所有线程
ps -ef -L
抽查几个疑似没有释放的线程号 , 使用在线工具转成16进制,例如 : 134102 --> 1ff83 , 在16进制前拼接 0x , 就是快照文件中的线程号 0x1ff83 ,使用命令在快照中查看该线程堆栈信息
grep -C20 "0x1fbfb" thread_dump21.txt
由图可知, 大量线程是 timewheel 时间轮类创建的 , 此时需要看代码中如何使用的该类
发现 ,代码中在循环里使用了 timerTask 方法
而 timerTask 中,又有新建时间轮的操作
时间轮的原理可以简单了解下, 大致就是有个调度线程 来延迟处理任务 , 如果每次都实例化该时间轮 , 则会新建时间轮的调度线程 ,此处在loop中新建,确实会引起线程泄露的问题
解决问题
时间轮不能每次都实例化 ,应该使用一个静态的实例,使用固定的线程去调度; 调度的执行逻辑可以再新启一个定长线程池 ,去进行任务的执行.
这样 任务的调度,和执行分开,并且处理的线程数都在可控范围内
例子 :
.....
// 静态线程池
private static final HashedWheelTimer TRANSFER_QUERY_HASHED_WHEEL_TIMER = new HashedWheelTimer(new NamedThreadFactory("pool-xxx-thread"),30, TimeUnit.SECONDS,20);
public static void main (String[] a) {
....
// 处理任务
for(Task task : taskList){
TRANSFER_QUERY_HASHED_WHEEL_TIMER.newTimeout(timeout -> executorService.execute(task), delayTime, delayTimeUnit);
}
....
}
.....
评论区