侧边栏壁纸
博主头像
buukle博主等级

布壳儿

  • 累计撰写 109 篇文章
  • 累计创建 17 个标签
  • 累计收到 9 条评论

目 录CONTENT

文章目录

记一次服务线程数超高异常排查

zhanglei001
2024-08-27 / 0 评论 / 0 点赞 / 16 阅读 / 2516 字

问题暴露

运维反馈线上服务线程数飙高,达到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);
	}
    ....
}

.....

0

评论区