Java ThreadPoolExecutor CPU占用100%,如何排查死循环问题?

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。 看看有谁遇到同样的问题,能给点提示和方向。

阅读 379
1 个回答

根据现有的信息只能推测是poll()时需要获取 takeLock。当大量线程同时调用 poll() 时,锁竞争无法避免,这个过程中没有获取到锁的线程都做自旋等待操作导致CPU占用升高(仍然是RUNNABLE状态),这样keepAliveTime也会失效无法回收线程

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    final E x;
    final int c;
    long nanos = unit.toNanos(timeout);
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
            if (nanos <= 0L)
                return null;
            nanos = notEmpty.awaitNanos(nanos);
        }
        x = dequeue();
        c = count.getAndDecrement();
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

尝试:
1、把maxPoolSize调小一点,比如从64调到32
2、换个队列类型试一下,换成ArrayBlockingQueue,这样锁竞争开销会小一点,吞吐量可能会下降
3、排查代码,是不是提交了什么死循环任务
4、换JDK
5、看看容器资源分配是不是不够

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题