一次线程池使用不当产生的线上问题排查思路

寻找BUG

国庆中秋最后一天,一个电话声音打断了我的美梦,有个项目健康检查告警了。赶紧穿上衣服打开电脑来排查问题。

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工具分析

我们可以通过JVisualVM工具来查看内存与线程总数,线程存活时间等,这个工具就在jdk安装路径的bin目录中

通过观察,线程一直在增长,不会被回收掉,到这里,我们已经确定,方法执行结束后,相关线程确实不会被回收,其实是核心线程数据处于阻塞状态,不会释放。

解决方案

1、局部线程池不太优雅,大可以使用一个全局线程池来做

2、如果你的业务就是需要用到局部线程池,为了防止线程泄露,有以下的方式

第一种我们了解到可以设置allowCoreThreadTimeOut,来让线程池回收空闲达到指定时间的核心线程。修改代码红色部分:

启动项目接着观察JvisualVM,线程总数是上下波动的。每个线程一共存活了大概5秒,和期望相符,最后会被GC回收,如下图:

第二种方法,是把核心线程数设置为0,大家都知道线程池的工作流程,超过核心线程数的线程,超过一定的时间会自动释放,咱们代码里是5秒,代码修改如下

启动项目,继续观察JvisualVM,线程总数也是固定在一个范围内,不是一直增长的,每个线程一共存活了大概5秒,和期望相符,最后会被GC回收,如下图。

总结与思考

1 、这种线上问题监控非常必要项目中一定要有监控

2、遇到问题不要慌,第一步摘流量,后续慢慢排查

3、分析监控信息是否有内存、cpu、负载过高

4、打印堆栈日志对代码进行分析。根据日志分析代码定位问题。

5、修复问题后在测试环境测试后进行灰度上线。


注意:使用线程的时候还是要多思考,用不好就会出现线上事故。

展开阅读全文

页面更新:2024-02-17

标签:线程   前缀   不当   思路   总数   核心   代码   发现   方法   项目   数据   日志

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top