这类问题,如果应用不是在容器中运行的(K8S,docker),那排查起来相对简单,无非就是先用top定位哪个java服务的进程的CPU占用较高,再用top -Hp {pid}命令来查看具体哪些线程的CPU占用较高,最后通过jstack命令打印服务的线程堆栈信息,再将占用过高的线程的PID转换成16进制到线程堆栈文件中去搜索,找到对应的高CPU占用的线程就行了。
但如果应用是通过容器启动的,那以上路线就行不通了。原因主要是容器内打印出的线程堆栈信息中的线程ID(NID)和通过宿主机本地top -Hp {pid}命令打印出来的线程ID并不一致,这样就无法准确定位到高CPU占用的线程了。那么如何排查容器内应用的高CPU占用的线程呢?最近看到某篇文章中提到的一种方式倒是让我受到一些启发。
我们知道一个线程的状态一共有六种:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED. 但其中只有处于RUNNABLE的线程才会占用CPU资源,这个很好理解,只有运行中的线程才会占用资源。但现实当中,并非所有处于RUNNABLE状态的线程都会占用实际的资源,举个简单的例子:
"qtp1786040872-10651" #10651 prio=5 os_prio=0 tid= nid=0x299d runnable [0x00007f9565d04000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269)
at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:93)
at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
- locked <0x00000000edf64238> (a sun.nio.ch.Util$3)
- locked <0x00000000edf64248> (a java.util.Collections$UnmodifiableSet)
- locked <0x00000000edf641f0> (a sun.nio.ch.EPollSelectorImpl)
at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:101)
at org.eclipse.jetty.io.ManagedSelector.nioSelect(ManagedSelector.java:149)
at org.eclipse.jetty.io.ManagedSelector.select(ManagedSelector.java:156)
at org.eclipse.jetty.io.ManagedSelector$SelectorProducer.select(ManagedSelector.java:572)
at org.eclipse.jetty.io.ManagedSelector$SelectorProducer.produce(ManagedSelector.java:509)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.produceTask(EatWhatYouKill.java:360)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:184)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:171)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:129)
at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:375)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806)
at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938)
at java.lang.Thread.run(Thread.java:748)
这个线程虽然处于RUNNABLE状态,但明显在等待网络IO,其实并没有占用什么CPU资源。从这个思路出发,我们很容易就能找出那些其实处于伪RUNNABLE状态的线程,并把它们从问题排查的列表中删除掉,那么剩下的线程,很有可能就是你在寻找的高资源占用的线程了。
人工这样一个一个线程的排查,在线程数量比较多的时候还是有点费事的,这里推荐一个网站,可以快速自动的识别出类似这样真正的高消耗的线程:https://fastthread.io/ 上文所述的理论也是原止于此,原文链接如下:THREAD DUMP ANALYSIS PATTERN – ATHLETE