【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(一)

一、背景

某天,运维同学找来,说服务的线程数过大,需要排查下原因

这个服务是商家的运营平台系统,采用Java语言,Spring框架编写

运维同学找来.png

注:本文是中大型互联网公司遇到的真实案例,其知识点的深度和广度拿来面试都足够,建议认真阅读并且熟悉涉及到的相关知识点。若有任何问题可在评论区指出~

若对你有所帮助的话,记得点个赞,谢谢~

本文是线上问题系列第一篇 -> JVM线程数过多的排查方法论


二、监控数据

首先看下监控

公司采用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以及对线程池源码的阅读来达到解决问题的目的

看完记得点赞记录一下~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值