国庆中秋最后一天,一个电话声音打断了我的美梦,有个项目健康检查告警了。赶紧穿上衣服打开电脑来排查问题。
1、登录服务器看服务进程是否存在(利用命令ps -ef|grep 项目名),发现项目进程存活。
2、去日志目录下查看日志,发现是有日志打印的,但是没有了新的业务日志。
3、手动去请求健康检查接口 curl http://127.0.0.1:端口/actuator/health,发现请求不通了。
4、当前服务总共有8个实例,临时方案从负载均衡种摘除这个节点,用作排查使用。
5、登录云服务器查看服务器的监控,CPU、内存、系统负载等指标发现一切正常。
6、top -Hp 进程Id查看监控内容,发现Threads线程总数竟然有8000多个线程,并且
线程总数等于sleeping线程数,在这里就意识到了线程出了问题,怀疑线程创建后没有销毁。
下图不是当时有问题的截图
7、利用命令jstack -pid > jstack.log 转存到文件里,打开文件看到好多WAITING的线程,利用命令grep -c "WAITING" jstack.log 或者 cat jstack.log |grep WAITING|wc -l 统计了一下数量,差不多有8600多个,正好和第5步查询的总的线程数差不太多,在这就能确定是线程使用不当造成的了。排查到这里我不禁发出疑问,线程池到底在做些什么?为啥线程不释放呢?
8、根据线程前缀”preUpdate” 搜索到代码的位置,分析代码项目中线程使用方式,发现有下面一个线程池的内部类代码,看一下下图。
通过看代码,发现了问题的所在,该业务是直播结束后将生成的画笔数据、文档数据等
存入mongodb,由于数据量比较大。就采用了多线程的方式入库。采用了线程池。问题就出现在这.
问题1:方法忠使用线程池,而不是把线程池抽出一个公用的方法,可能当时写程序的人有自己的考虑。因为要抽出公用的方法,就要考虑,使用哪个线程池对列,拒绝策略怎么设计。补偿策略怎么处理?
问题2:采用了方法级别创建线程池,它设置了核心线程数和最大线程数相等,这样就带了一个问题。核心线程数是常驻的不会释放的。所以每次调用方法生成了一批线程,但是永远不会释放,造成了线程数越来越多,线程最终导致资源不可用了。
用Demo模拟这种情况,demo的代码时根据线上代码改的,并且利用阿里巴巴的druid包下的DaemonThreadFactory应用到该线程工厂的线程池,内部产生的线程的名称都有“preUpdate”前缀。模拟代码如下
import com.alibaba.druid.util.DaemonThreadFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.*;
public class ThreadPoolLeakTest {
public static void main(String[] args) {
List metas =new ArrayList<>();
for(int i=0;i<10;i++){
metas.add(UUID.randomUUID().toString());
}
//死循环,因为是main方法执行,防止主线程执行完。
while(true){
try {
//每次启动10个线程,线程的核心线程数10,最大线程数也是10
TaskAddHandler updateHandler = new TaskAddHandler(metas);
updateHandler.execute();
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static class TaskAddHandler {
private ExecutorService pool;
private List> futureList;
private List metas;
//构造方法创建线程池
TaskAddHandler(List metas) {
this.pool = new ThreadPoolExecutor(
metas.size(),
metas.size(),
5,
TimeUnit.SECONDS,
new SynchronousQueue<>(),
//应用到该线程工厂的线程池,内部产生的线程的名称都有“preUpdate”前缀。
new DaemonThreadFactory("preUpdate"));
this.futureList = new ArrayList<>();
this.metas = metas;
}
//任务是否执行完成
boolean isDone() throws Exception {
for (Future future : futureList) {
try {
if (!future.get()) {
return false;
}
} catch (InterruptedException | ExecutionException e) {
throw new Exception(e);
}
}
return true;
}
//执行任务
void execute() {
for (String meta : this.metas) {
this.futureList.add(this.pool.submit(() -> {
try {
//打印数据
System.out.println(">>>"+meta);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}));
}
}
}
}
启动这个main方法项目,调用execute接口。这个时候产生的线程日志中。
我们可以通过JVisualVM工具来查看内存与线程总数,线程存活时间等,这个工具就在jdk安装路径的bin目录中
通过观察,线程一直在增长,不会被回收掉,到这里,我们已经确定,方法执行结束后,相关线程确实不会被回收,其实是核心线程数据处于阻塞状态,不会释放。
1、局部线程池不太优雅,大可以使用一个全局线程池来做
2、如果你的业务就是需要用到局部线程池,为了防止线程泄露,有以下的方式
第一种我们了解到可以设置allowCoreThreadTimeOut,来让线程池回收空闲达到指定时间的核心线程。修改代码红色部分:
启动项目接着观察JvisualVM,线程总数是上下波动的。每个线程一共存活了大概5秒,和期望相符,最后会被GC回收,如下图:
第二种方法,是把核心线程数设置为0,大家都知道线程池的工作流程,超过核心线程数的线程,超过一定的时间会自动释放,咱们代码里是5秒,代码修改如下
启动项目,继续观察JvisualVM,线程总数也是固定在一个范围内,不是一直增长的,每个线程一共存活了大概5秒,和期望相符,最后会被GC回收,如下图。
1 、这种线上问题监控非常必要项目中一定要有监控
2、遇到问题不要慌,第一步摘流量,后续慢慢排查
3、分析监控信息是否有内存、cpu、负载过高
4、打印堆栈日志对代码进行分析。根据日志分析代码定位问题。
5、修复问题后在测试环境测试后进行灰度上线。
注意:使用线程的时候还是要多思考,用不好就会出现线上事故。
页面更新:2024-02-17
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号