SkyWalking高频采集泄漏线程导致CPU满载排查思路

type
status
date
slug
summary
tags
category
icon
password
 

契机

最近在消除线上服务告警,发现Java线上测试服经常CPU满载告警,以前都是重启解决,今天好好研究下,打arthas火焰图发现是SkyWalking-agent的线程采集任务一直在吃cpu,jstack一看发现有1w+线程,故使用JProfiler好好排查了下,终于定位到了线程泄漏的原因。

收集线上信息

notion image

用JProfiler分析hprof

notion image
可以看到此时确实有1w2的存活线程
notion image
右键thread,查看是谁持有的thread,发现都是ThreadPoolExecutor基础线程池
notion image
同样去查看ThreadPoolExecutor的引用,此时发现差不到是谁声明的ThreadPoolExecutor,线索也就断了

分析jstack线程转储

notion image
使用jstack导出的txt,或者JProfiler的线程转储,信息都一样,发现都不能定位到线程/线程池是哪里来的,但是通过比较发现:线程命名太过于规范pool-367-thread-7,并且后缀不超过10,那么代表一定是我们显示的创建的ThreadPoolExecutor,并且制定了线程数=10。随后在代码中搜索线程池创建:ThreadPoolExecutor,newFixedThreadPool。发现确实有很多地方在声明,但是无法定位到具体代码
notion image

用JProfiler直接分析线上程序

notion image
这里需要提前使用命令行链接一下,不然会有known_hosts报错
notion image
此时在线程monitor里面就可以看到新线程创建的堆栈,马赛克就是我的业务代码
notion image

分析代码

notion image
发现是在方法中定义了ThreadPoolExecutor,但是没有调用shutdown方法来正确关闭。导致即使业务方法执行完成后,线程池依然存在,导致线程泄漏。业务中使用全局线程池!

无法被回收原因

无法回收的根本原因
  • 每个Worker线程通过thread -> this(Worker实例)-> outerClass(ThreadPoolExecutor)形成引用闭环
  • 即使外部没有引用,只要工作线程存活,就会保持对线程池的强引用

总结

  • JProfiler可以远程分析线上程序
  • 线程池无法被回收的本质原因是其内部的工作线程(Worker)与线程池实例之间的循环强引用,只有当工作线程完全终止(进入TERMINATED状态)且外部没有其他引用时,GC才能回收线程池实例。
  • 应该使用github的动态全局线程池,后续改造吧

写到最后

notion image
是在往前走就好 bothsavage.github.io
 
notion image
 
尝试使用gocryptfs实现大模型加密部署旧版本NotionNext图片失效最小改动解决思路