线上非业务问题排查

  常见的线上问题基本都是业务代码导致的问题,例如某个空指针或者是代码编写存在漏洞。这里记录一下网上看到的容器服务线程数飙升导致的问题

  

一、监控数据

首先看下监控

公司采用Prometheus监控,有较为完善的监控指标,因运维同学说的是线程数过多,那就只列出和线程相关的监控,即存活线程数、RUNNABLE线程数、WAITTING线程数

2.1 存活线程数监控图

存活线程数监控图.png

2.2 RUNNABLE线程数监控图

RUNNABLE线程数监控图.png

2.3 WAITTING线程数监控图

WATTING线程数监控图.png

2.4 7天时间跨度图

7天时间跨度线程数.png

从以上数据可以看到,JVM线程的数量确实在不断的增加,而且大部分都处于WATTING状态


二、猜想

如运维同学所说,JVM的线程数确实很多,那么导致线程数过多的原因有哪些?可以先头脑风暴一下~

  1. 此时服务QPS比较高,导致JVM创建了过多的线程数来处理请求(特别是没有使用线程池,而是直接使用new Thread()构造方法来创建线程的情况)
  2. 服务里某个线程池设置的corePoolSize过于庞大
  3. 服务里创建了过多的线程池

对于猜想2,需要了解下Java线程池提交任务的机制,如下代码所示:

 
public void execute(Runnable command) {
    ...为了更清晰,这里省略了一些代码...
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
 }

 

Java线程池在提交任务时,若线程池中的线程数小于corePoolSize的时候,就会不断地创建新线程来执行任务

对于猜想2和猜想3类似,本质上都是核心线程数设置过多,只不过猜想3是靠线程池的数量堆积起来的核心线程数过多


四、验证

4.1 验证猜想一:服务QPS比较高,导致JVM创建了过多的线程数来处理请求

微服务都有相关的调用量监控,由监控可知,在该时间段内QPS并没有多大的波动,因此可以排除猜想一

4.2 验证猜想二 & 猜想三

之所以将猜想二和猜想三放在一起,是因为通过一个工具即可验证,那就是Arthas

Arthas是阿里提供的Java应用诊断利器,其集成了许多的功能,方便实用

通过Arthas的thread命令可以查看到当前JVM所有的线程。(注意:执行该命令前记得把节点的流量摘掉,以防止对线上业务造成影响)

如下,该命令输出以下数据,我们重点关注NAME数据,即线程名称

IDNAMEGROUPPRIORITYSTATE%CPUTIMEINTERRUPTEDDAEMON
线程ID 线程名称 线程的分类 线程的优先级 线程的状态 线程所占的CPU -- 是否被打断 是否为守护线程

为什么关注NAME数据,这里需要了解下Java线程池创建线程时的命名规则:

在创建线程池时,有个ThreadFactory参数,其为线程创建的工厂,可以在里面指定线程创建时的命令规则,如果不传的话,默认采用的是 java.util.concurrent.Executors.DefaultThreadFactory#DefaultThreadFactory

默认的线程工厂创建线程时的命名规则代码如下:

    DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();

            // 此处是核心
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

 

可以看到,线程名称的命名规则是:pool-poolNumber-thread-threadNum,解释如下

线程命名规则.png

根据此名称的规则可知,若poolNumber数过多,则可证明是线程池数量过多导致的线程数过多。

若threadNum过大,则可证明是某线程池内的线程数过多

在机器上执行 arthas thread后的数据如下图,可知是线程池创建过多导致的JVM线程数过多 arthas的thread命令图.png


五、寻找问题源

在原因确定之后,下面需要确定问题代码在什么地方?

有个思路是我们拿到线程的堆栈,从堆栈里面得知线程执行的业务代码,再根据业务线代码的类以及行数就可以知道线程池创建的地方

Arthas同样提供了查看线程堆栈的功能,很遗憾,在里面没有业务代码。如下图:

arthas线程图.png

于是只能换种思路,根据线程池的创建方式,全局搜索线程池创建处的代码。

线程池的创建一般有三种方式:

  1. 利用JDK自带的工厂类Executors,如:Executors.newFixedThreadPool(1);

  2. 利用线程池的构造函数:如:

 
   private static ThreadPoolExecutor pool = new ThreadPoolExecutor(corePoolSize,
            maximumPoolSize,
            keepAliveTime,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(queueSize),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy());

 

  1. 利用三方工具类创建的线程池,如Guava的MoreExecutors

通过搜索,发现了问题代码:

 
     public void method() {
             // 。。。此处省略了一些代码
            final ExecutorService executorService = Executors.newFixedThreadPool(30);
            for(SubTask sub : mutiTaskReult.getSubTasks()){
                executorService.submit(new ExportAccountFlowRunnable(mutiTaskReult.getMainTask(), sub, logStr));
            }
            // 。。。此处省略了一些代码
        }

 

可见,代码里在方法级的作用域里创建了线程池,从而导致JVM线程数不断地增加。

大家见到这里可能有疑问:这个线程池在方法执行完成后便没有引用了,为什么没有被回收?线程为什么没有被释放?

这个答案可以在ThreadPoolExecutor的注释里得到答案,即:

Finalization A pool that is no longer referenced in a program AND has no remaining threads will be shutdown automatically. If you would like to ensure that unreferenced pools are reclaimed even if users forget to call shutdown, then you must arrange that unused threads eventually die, by setting appropriate keep-alive times, using a lower bound of zero core threads and/or setting allowCoreThreadTimeOut(boolean).

线程池如果不被引用,且没有剩余线程的时候才会被自动关闭。

如果想在没有调用shutdown的时候,线程池也会被关闭回收,那么你必须要保证线程池里面的线程最终都要“死”掉。可以如下的两种方式来设置:

  1. corePoolSize设置为0,且设置一个合适的keep-alive时间

  2. allowCoreThreadTimeOut(boolean) 设置为true,允许核心线程也会被超时回收

我们再往深处想一想,线程池没有被回收的原因只能是被GC ROOTS TRACING了,那么引用线程池的GC ROOTS是什么?

结合MAT对内存的分析,可以发现作为GC ROOTS的Threadtarget属性持有了Worker的引用,而Worker作为内部类同样持有了ThreadPoolExecutor的引用,于是形成了Thread->Worker->ThreadPoolExecutor这样一条隐蔽的关系,具体如下图所示

引用图.png

MAT分析的数据如下所示:

  1. thread的target属性持有了worker的引用

thread引用worker.png

  1. worker的this属性持有了ThreadPoolExecutor的引用

worker引用ThreadPoolExecutor.png


六、总结

本文阐述了一个由JVM线程数过多的问题引起的思考、分析与解决的过程。通过利用Arthas、MAT以及对线程池源码的阅读来达到解决问题的目的

 
posted @ 2023-06-25 17:06  重生之我是java程序员  阅读(19)  评论(0编辑  收藏  举报