Java ThreadPoolExecutor CPU占用100%,如何排查死循环问题?
线上有个服务的一个容器突然告警CPU占有率达到100%, 首先为保证系统安全先将有问题的容器下线掉,不让流量再进来。没有请求流量之后,容器中java进程的CPU占有率依然居高不下。然后登上容器用top命令看各个线程的情况,发现占有CPU的是一个线程池创建的线程。这就奇怪了,容器下线之后应该不会有请求进来触发线程池执行任务了呀。接下来用jstack 命令列出各个线程的堆栈,看到该线程池中共有64个线程,其中40个waiting于获取新的任务,24个处于runnable状态。 处于runnable状态的线程堆栈如下
可以看到runable的线程也是在试图获取任务,当并没有线程在真正执行任务。这就很奇怪了,因为线程池中线程去查询是否有任务的操作是非常快和短暂的。线程池中的线程要么获取到任务去执行任务中的代码,要么因为拿不到任务陷入waiting状态等待有任务,只有极少极少的概率会处于于ruannable并试图去获取任务。不可能有能看到40%左右的线程都处于这种状态。后面把java进程的火焰图弄出来看了大概是这个样子
可以看到CPU主要消耗在 LinkedBlocking.poll 方法上,和jstack的信息一致。开始怀疑是不是线程池的配置有问题打导致,线程不断轮询任务队列而没有waiting,后面经过确认我们的线程池配置的maxPoolSize 是64, corePoolSize 是16,当前线程池中的线程数量达到了最大的64。线程池是配置了keepAliveTime 参数为60秒的,这样线程获取不到任务会等待60秒钟,如果还是没有,线程会终结掉直到线程数量减少到corePoolSize。用arthas 看 LinkedBlocking.poll 的调用情况,也没有看到被频繁调用。到这一步就让我们没有思路了,目前只能怀疑 LinkedBlocking.poll 方法有啥bug,导致获取不到任务就会陷入死循环。但是一般像jdk内部的代码不应该有这样明显的bug。目前用到的jdk 版本是 zulu-17。 看看有谁遇到同样的问题,能给点提示和方向。
根据现有的信息只能推测是poll()时需要获取 takeLock。当大量线程同时调用 poll() 时,锁竞争无法避免,这个过程中没有获取到锁的线程都做自旋等待操作导致CPU占用升高(仍然是RUNNABLE状态),这样keepAliveTime也会失效无法回收线程
尝试:
1、把maxPoolSize调小一点,比如从64调到32
2、换个队列类型试一下,换成ArrayBlockingQueue,这样锁竞争开销会小一点,吞吐量可能会下降
3、排查代码,是不是提交了什么死循环任务
4、换JDK
5、看看容器资源分配是不是不够