📜 目录
- 😮 一天上涨五十个线程?
- 🔍 初步分析
- 🚀 复现方式
- 🐢 二分法确定问题
- 🗒️ 总结
😮 一天上涨五十个线程?
上次提到,经过初步修复后,又继续观察了几天线程变化情况,发现每天仍然会有 50 个线程的泄露
虽然对比之前已经好了太多,但是现在的情况仍然难以接受,问题没有彻底解决,因此又开始了新一轮的排查与修复
🔍 初步分析
确认存在问题以后,首先要确认泄露的线程是哪些
每隔一段时间就搜集一下线程数据 ( 方式参考 一个 Jenkins 实例启动了两万多个线程? ),通过对比前后两次线程详情,确认了泄露的线程如下
这种线程是最让人反感的,没有任何调用方的提示,压根找不到在哪里发生了泄露,让人垂头丧气
但是看线程变化趋势,即使是在 Jenkins 静息状态(没有流水线执行)下,线程也会增长,看起来泄露和执行流水线关系不大
联系到在 上篇文章 中提到的 kubernetes-client/java 存在的线程泄露的问题,怀疑这次是否又是类似的问题?
浏览了 controller 相关的代码。发现在 controller 启动时,会启动一个 informer ,这个 informer 也会启动一个线程,public SharedInformerFactory() 在初始化 SharedInformerFactory
时会通过 Executors.newCachedThreadPool()
新建一个线程池
因为暂时没找到哪里会关闭这个 informer,所以怀疑是这里的问题,通过将 informer 代码的调用注释掉,发现线程仍然会继续增长,明显不是这里造成的问题,后续才发现 informer 的关闭是在 ControllerManager shutdown()
中完成的
线索断掉了,单纯的分析代码可能难有进展,那就分析下线程变化,看看能够有什么进展
🚀 复现方式
耐心的分析线程变化,发现线程泄露也是周期性的,每次会新增两个线程,那可能和上一个 TIMED_WAITTING
线程泄露的触发条件一样
因此使用一些手段重启了 controller manager ,发现线程的泄露数目会增加的很快,之前是周期性的增长,这次在重启 controller manager 之后,可以明显看到类似线程的泄露现象
这就算是找到了稳定的复现方式了,问题有解决的希望了
🐢 二分法确定问题
线程堆栈信息中没有提供指向性明确的信息,因为目前只是确认了问题代码范围,就只能用原始的二分法来确定问题代码了
首先将 controller manager 的调用代码注释掉,编译插件部署后,发现问题果然不在复现了,因此基本能够确认问题就在 controller manager 这里
后续将 controller 分为两组,一部分启动,一部分不启动,编译部署,通过复现方式确认线程变化后,可以找到在哪些 controller 中存在问题
找到对应的 controller 后,再将 controller 的代码分为上下两部分,注释一部分,编译复现确认,再注释一部分,编译复现确认,最终终于发现了问题代码
问题就出现在下面这段代码中,在某些 controller 启动时,会调用这里的 pollWithNoInitialDelay
函数,这个函数启动时就会初始化一个线程,用来完成任务,但是这里缺少关闭这个线程的逻辑
目前已经知道,每小时 controller manger 会重启一次,也就是意味着这里每天会重启 24 次,看 usage 也能知道有两个地方在调用这个函数,所以次重启会泄露两个线程,计算下来正好是 48 个线程,正好与之前问题复现中提到的 50 个线程匹配
看起来或许这个就是最后一个窟窿了
问题足够简单,修复也很简单,只需要在 finally 处关闭这个线程即可,这样无论该函数调度多少次,都不会再发生线程泄露了
try {
while (System.currentTimeMillis() < dueDate) {
if (result.get()) {
future.cancel(true);
return true;
}
}
} catch (Exception e) {
return result.get();
} finally {
# 将线程关闭
executorService.shutdown();
}
后续连续观察了三天线程变化,并且在这期间跑了几次集成测试,通过在 Jenkins 上调度了很多流水线模拟正常使用情况
发现在流水线执行过程中,线程会有变化起伏,但是都没有超过 250 个,在流水线执行结束后,最终都会稳定回归到 170 左右
后续在 Jenkins 静息状态下观察了一周,发现线程一直在 170 上下,没有出现线程泄露的情况,至此确认问题解决,所有的窟窿都被补上了
🗒️ 总结
在 Java 中启动线程时,一定要考虑关闭线程,我们应该要考虑照顾到这样一个场景,也就是 这段代码如果被反复调用一万次,是否会出现线程泄露
,不能单纯的认为,这个初始化的代码应该只会被调用一次,就不去关闭线程
另外在启动和关闭线程时,考虑 debug 打印一下这两个操作,不仅有助于自己判断对线程的处理是否生效,也对后来可能的排错场景大有裨益
最后对我来说,这次解决问题时最大的提升,就是在我走投无路时,耐心的查看线程变化信息,并且坚信它会提供给我解决问题的信息,最终结果也没让我失望,我觉得这是这次对我来说最重要的变化
希望通过这两篇线程泄露排查的博文,能为各位在解决类似问题时,提供一些解决思路,帮助各位解决问题
有任何问题欢迎评论交流!