诊断由于忘记调用 CountDownLatch.countdown() 方法引起的服务器卡住(stuck)

正常情况下, 这些 countdown() 方法都不会被忘记调用的, 很多情况都是由于考虑不周全, 对于异常的情况处理不正确引起的. 这和各种连接泄漏(数据库连接泄漏, 外部服务连接池) 是一样的问题, 只不过这是对于关键资源的泄漏.

症状:

偶然看到有个应用里面有一台服务器的 tomcat busy thread 的数量达到了配置的上线(默认是 40), 而其他服务器则分布在在 1~23 之间. 并且这个数字一旦上去之后, 就再也下不来了.

诊断:

首先看了下 CPU使用率, 非常低. 处理的 tps 数量降到了 0. 直接访问监控配置页面, 没有响应. 连续做了 3 个 thread dump, 发现这 40 个线程大概可以分为 2 组, 第一组全部卡在 CountDownLatch.await(CountDownLatch.java:236) 方法上, 有 20 个. 另外 20 个全部卡在: OrchestratorQueue.waitFor(OrchestratorQueue.java:86) 方法上. 这个OrchestratorQueue 是公司内部框架层面的一个线程池, 其实它相当于一个线程池的线程池. 它用一个线程池虚拟出来 N 个虚拟线程池, 这个 N 个虚拟线程池共享实际的一个线程池.

为什么后面 20 个tomcat 线程卡住了?

OrchestratorQueue 的设置是: 实际线程池的线程 core 数量是 100, 所以它是有空余线程的, 从 thread dump 也可以看到. 另外它的最大虚拟线程池的数量是 20, 也就是说它最多允许 20 个线程使用这个实际线程池, 而我们上面看到的前 20 个线程已经使用了 20 个, 并且没有结束请求(退出), 所以后面的 20 个请求只能被卡在这里等待空闲线程池. 所以这里发现的第一个问题是: 明明 Tomcat 设置的是 40 个线程, 可是OrchestratorQueue 却设置了最大 20, 所以导致后面来请求拿不到虚拟的线程池.

于是, 我们尝试把虚拟线程池在运行时设置为最大值为 40, 期望第二组请求能够获得虚拟线程池, 继续走下去. 可是即使我们改成了 40, 也没有看到任何改动. (由于这里的默认的处理端口上线程已经达到最大值, 我们是通过管理端口去改的这个值).

为什么虚拟线程池改大, 没有任何效果

虚拟线程池改成 40 后, 没有任何效果. 于是我们又把 tomcat 的 worker 线程池从 40 改成 41, 很快我们看到 tomcat 的 忙碌线程数升到了 41, 然后忙碌线程数又降到了大概 21, 22 的样子. 为什么会这样呢?
因为之前把虚拟线程数改成 40, 只是在内存改了一个变量的值, 并没有触发虚拟线程池扩容. 真正的扩容是在下次和这个线程有交互的时候, 所以我们把 tomcat 的 worker 线程数改成 41, 就会有新的 请求进来, 触发了虚拟线程池数量的扩容.

为什么第一组 20 个线程卡住了

这20 个线程都卡在 CountDownLatch.await() 方法上, 是因为这个 latch 一直不能 count down 到 0, 所以它只能等待. 为什么不能 countdown 到 0 呢?
进一步看代码, 发现有段代码是这么写的

public Object doSomeTask(CountDownLatch latch, ***) throws SomeException {
    try {
        //some some business logic and can throw exception
    } finally {
        //clean or release
    }
    latch.countdown();
}

所以, 问题出现在: 没有把 latch.countdown() 放到 finally 块里面去. 如果中间有代码抛出了异常, 导致 countdown() 方法没有被执行.

修复:

无论发生什么情况, 最后一定要执行 latch.countdown(). 放到 finally 块就好了.

思考:

  1. 凡是调用 await()方法的地方, 都要思考一下, 是不是需要增加一个 timeout 时间;
  2. 通过观察长期的 tomcat busy thread 的数量可以发现此类问题, 长期来看, 这个一定是只上升, 不下降;
  3. 通过对比 同一个应用的不同 server 直接 tomcat busy thread 的数量, 也可以发现类似的问题, 因为有的维持高位, 有的一直在低位(可能最近重启过);
  4. 通过计算 tps , latency (transaction time) 和 tomcat 线程数, 能发现这里面的偏差. 若某个服务器的平均 tps 是 10, 请求的平均的 latency 是 100ms, 那么平均只需要一个 tomcat 的忙绿线程.

标签: none

添加新评论