记生产环境 rabbitmq 部分客户端 channel 持续积压消息不进行ack

线上mq消息积压,原因竟然是因为…

0. 服务配置

  • rabbitmq 集群(普通集群模式)
  • 消费者 三台 消费线程各消费者 10
  • 消费者配置 使用 spring-amqp|auto-ack 模式

1. 故障发现

近日有同学发现一个业务队列存在上千个 unacked 消息,并且有持续上涨的趋势。

2. 故障表现

队列下其中两个客户端的各一个 channel 分别阻塞几百条数据,并且在持续累加,重启应用后队列 unacked 消息全部进入 ready 状态等待重消费,但是重启后客户端依然有 channel 重新开始堆积并且在趋势上涨。

3. 问题排查

排查思路

  • 检查 mq 控制台是否是队列创建问题
  • 消费者阻塞是否有规律可循(未ack数据是否有共同特征、阻塞客户端配置是否有问题)
  • 客户端代码是否有问题、应用是否有jvm级别故障

4. 问题定位

经过一番筛查,问题定位到了代码部分,队列消费代码并非刚上线,而是在前一日服务重启后出现的这个问题,重新 review 代码后发现消费者有使用 CountDownLatch 等待多线程消费结果,并且采用线程池处理的代码并没有把 CountDownLatch#countDown 调用放到 finally 中执行,并且提交到线程池的任务也没有 try catch 方法包裹,到此怀疑是消费线程阻塞到了 CountDownLatch#await 处,异步任务处理时由于偶现异常代码并未执行到 CountDownLatch#countDown 处,再者由于异步任务未捕获异常导致错误直接抛到 jvm 日志无发记录错误。为了验证这个问题,我们又dump了阻塞服务的栈信息,发现确实有消费者线程阻塞到 CountDownLatch#await 处,问题定位结束。

5. 解决方案

从任务处下手添加 catch 记录日志,并将 CountDownLatch#await 放到 finally 中执行。重启应用再次观察,并未出现 unacked 消息,观察日志也并未出现新添加的 error 日志。

6.问题拓展

  • 同一个 channel 为何会阻塞那么多数据?

    线上生产环境采用推模式,rabbitmq 通过 channel 推送消息到客户端,客户端采用 LinkedBlockingQueue 做缓存,一个 channel 对应一个消费者线程,当消费者线程阻塞时 LinkedBlockingQueue 作为中转一直在预存消息,所以会出现很多 unacked 消息。

  • 为什么仅有部分一两个 channel 出现堆积?

    线上添加错误日志后实际并未出现错误打印,怀疑之前异常可能是由于重启后第一次请求 rpc 偶现调用失败,猜测暂无法复现,后续需观察日志。

总结

  • 谨慎使用线程同步,谨防线程死锁,务必保证线程不会 hang死。
  • 自建线程池做好错误兜底,不要将异常抛给jvm。