引言

最近遇到了一个 ActiveMQ 消费端的问题:在没有消息时,日志频繁打印,每秒打印2000多条空消息,导致日志文件迅速膨胀,甚至影响系统性能。经过一番排查,最终定位到问题根源并成功解决。本文将详细记录问题的排查过程、原因分析以及解决方案,希望能为遇到类似问题的同学提供参考。


背景

最近优化了一个 ActiveMQ 消费端应用消费速度慢的问题,原先采用 Spring 的@Scheduled定时每秒调用ActiveMQMessageConsumer.receive(2000)拉取消息并同步处理,简化后的代码如下:

@Scheduled(cron = "1/0 * * * * ?")
public void consumer(){
new Thread(()->{
try{
logger.info("ActiveMQClient-->receive begin queue_name = {}", QUEUE_NAME);
ActiveMQMessage msg = (ActiveMQMessage )activeMQMessageConsumer.receive(2000);
if(null != msg){
processMsg(msg);//同步处理消息并手动确认
}
} catch(Exception e){
logger.error("ActiveMQClient-->receive error:", e);
}
}).start();
}

可以看到在调用receive之前打印了一条日志。当队列无消息时,上述代码中的日志每秒打印一次(定时每秒启动一个线程),日志文件每天最多2-3个。

当业务量激增时,以上每秒消费一条消息的方式远远满足不了业务需求,且会造成 ActiveMQ 服务端消息积压,故做了以下优化,简化后的代码如下:

ExecutorService THREAD_POOL = Eecutors.newFiexdThreadPool(1)
@PostConstruct
public void startConsumer(){
THREAD_POOL.submit(()->{
while(true){
try{
logger.info("ActiveMQClient-->receive begin queue_name = {}", QUEUE_NAME);
ActiveMQMessage msg = (ActiveMQMessage )activeMQMessageConsumer.receive(2000);
if(null != msg){
processMsg(msg);
}
} catch(Exception e){
logger.error("ActiveMQClient-->receive error:", e);
}
}
});
}

上述改造采用单线程(为了保持消息消费的有序性)循环执行消息拉取和处理逻辑,相比原先定时任务1秒消费一条消息消费能力有明显提升,另外当队列无消息时receive方法会阻塞两秒,也不会造成线程空转。上述改造部署后,特意观察了无消息时的日志打印频率,确实为2秒一次,日志量和之前相差无几。准备愉快的上线了。

问题描述

上线前夕,有其他小伙伴在测试环境通过日志排查问题时,发现当天早上日志文件数量就达到了130+,根本不知道该看哪个日志文件。于是我打开其中一个文件统计了一下每秒打印ActiveMQClient-->receive begin queue_name高达2000多条(队列无消息时),貌似队列无消息时receive(2000)阻塞两秒失效了,导致线程在空转,一直拉取消息,导致日志量暴增!


排查过程

1. 初步分析

  • 怀疑 receive(2000) 方法的超时设置失效,导致立即返回 null
  • 检查代码和配置文件,确认 receive(2000) 的超时时间为 2 秒。

2. 进一步排查

对此我感到一头雾水,为啥超时时间会失效呢?我明明记得当时在测试环境特意观察了日志,无消息时确实是每两秒打印一次。

  • 重启大法:于是我重启消费端程序,观察了一会儿日志,也是每两秒打印一次,这下更懵逼了!

  • 观察日志:没办法,只能继续找问题了。我查看服务器日志,文件太多了,每天都是一百多个压缩文件。我随便找了某天的第一个文件和最后一个文件,打开最后一个文件从文件末尾看,日志频繁打印。打开第一个文件,文件开头从零点开始打印,也是非常频繁,然后我按时间查找中午十点多的,发现日志又正常每两秒打印一次(没消息时),太奇怪了。

  • 查找日志变化拐点:然后我想看看是从什么时候开始变得频繁,果然有了新发现。在那天的第一个文件里,从22:01:07起日志几乎一秒打印2000多条。在此之前紧挨着有几条ActiveMQ的日志,如下图:

貌似ActiveMQ关闭某个线程池,从关闭后日志就变得频繁。根据日志发现等待了10秒,最终停止了线程池。

于是我根据日志位置找到对应源码,这段代码定义了一个名为awaitTermination的静态方法,用于等待线程池的终止。这个方法的主要目的是确保在关闭线程池之前,所有提交给线程池的任务都已完成执行。参数executorService是要等待终止的线程池,shutdownAwaitTermination是等待线程池终止的最大时间,以毫秒为单位。

找到调用awaitTermination的地方,即ThreadPoolUtils#doShutdown,它先调用了线程池的shutdown方法,然后调用awaitTermination等待线程终止,根据日志可以看到等待了10秒线程都没终止,最后强行调用shutdownNow方法,然后输出了Shutdown of ExecutorService:.....日志,对应上图中最后一条日志。

然后继续向上找调用ThreadPoolUtils#doShutdown的地方最终找到是在AbstractInactivityMonitor#stopMonitorThreads。由于调用这个方法的地方非常多,无法准确找到是在哪调用的。这条线索中断。

3. 灵机一动,发现突破口

正当我们有头绪时,突然想到去看看ERROR日志文件,看了一下ERROR日志文件比Info文件更多!于是我解压第一个ERROR日志文件,打开后根据Info日志中线程池关闭的时间22:00:07去搜索,果然发现了猫腻。

几乎相同时间,AbstractInactivityMonitor的246行抛出了InactivityIOException:Channel was inactive for too (>30000) long异常,即“频道长时间处于非活动状态”

在后续的ERROR日志中,全部都是IllegalStateException:The Consumer is closed异常,表明客户端和ActiveMQ服务端的连接已经断开!



这也就解释了为什么receive(2000)阻塞两秒失效,while循环调用receive拉消息,由于连接已断开,方法立即报错,又不停地拉可不就一直打日志嘛!

为什么连接被断开了?

问了一波AI,给出的回答如下:

  • 心跳检测默认是开启的,所以第一个被排除。
  • 日志中没有看到重连,被排除。
  • 第三种也不符合。

又陷入了僵局~

马上就要上线了,必须赶快排查出根本原因!

这时候原来负责这块儿的同事突然想到测试环境MQ服务端每天晚上十点停机!

???

挖了个渠~好坑

生产环境MQ服务端不会停机

解决方案

解决方法很简单,在原代码逻辑的try-catchcatch模块增加代码使线程休眠1秒。

ExecutorService THREAD_POOL = Eecutors.newFiexdThreadPool(1)
@PostConstruct
public void startConsumer(){
THREAD_POOL.submit(()->{
while(true){
try{
logger.info("ActiveMQClient-->receive begin queue_name = {}", QUEUE_NAME);
ActiveMQMessage msg = (ActiveMQMessage )activeMQMessageConsumer.receive(2000);
if(null != msg){
processMsg(msg);
}
} catch(Exception e){
logger.error("ActiveMQClient-->receive error:", e);
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
logger.error("ActiveMQClient-->sleep error after receive error :", ex);
}
}
}
});
}

回过头看,原来的代码确实存在隐患(抛异常后会立马进入下次while循环),幸亏在测试环境发现了。

复盘

现象

  • 消费端使用 while 循环调用 receive(2000) 方法拉取消息。
  • 当没有消息时,日志应每 2 秒打印一次ActiveMQClient-->receive begin queue_name”。
  • 实际运行时,日志每秒打印高达 2000 次,导致日志刷屏,日志量暴涨。

原因

  • 日志分析:发现Info日志中出现ActiveMQ InvativityMonitor Worker关闭了某个线程池,且ERROR日志出现InactivityIOException::Channel was inactive for too (>30000) long异常,之后频繁出现 IllegalStateException: The Consumer is closed 异常。异常日志的打印时间与Info日志开始频繁打印的时间吻合。
  • 连接状态:最终确认连接因服务端关闭,ActiveMQ Client端 InactivityMonitor 检测到不活跃而断开。
  • 消费者状态:连接断开后,消费者失效,receive() 立即返回 null,线程还在不停while循环调用receive,进而导致日志刷屏,日志量暴涨。

根本原因

  • 服务端关闭:服务端每晚关闭,导致连接中断。
  • 消费者失效:连接断开后,消费者继续调用 receive() 导致异常。
  • 日志刷屏while 循环频繁调用失效的 receive(),日志频繁打印。

探索ActiveMQ的断连机制

发现 AbstractInactivityMonitor

  • 在日志中发现了 AbstractInactivityMonitor 关键字,进一步查看源码,了解到其作用。
  • 源码分析AbstractInactivityMonitor 是 ActiveMQ 中用于监控连接活跃性的核心组件,其 readCheckerTask 机制用于定期检查连接状态。

readCheckerTask

readCheckerTaskSchedulerTimerTask实例,通过Java中的Timer定时器周期性的执行任务,默认30秒执行一次。readCheckerTask具体执行的任务如下:

  • 实现原理

    1. 获取当前时间:使用System.currentTimeMillis()获取当前时间。
    2. 计算时间差:计算当前时间与上次运行时间之间的差值。
    3. 检查上次运行时间:如果上次运行时间不为0,则记录自上次读取检查以来经过的时间。
    4. 允许读取检查:如果自上次读取检查以来经过的时间小于90%,则放弃当前的读取检查。
    5. 执行读取检查:如果时间足够,则执行readCheck()
    6. 更新上次运行时间:无论是否执行了读取检查,都会更新lastRunTime为当前时间。
  • 作用

    这段代码确保读取检查不会过于频繁地执行,从而避免资源浪费或潜在的性能问题。当判断通过时执行readCheck()

readCheck()

源码如下:

  • 实现原理

    1. 获取接收计数器:获取当前和上一次的接收计数器值,并更新上一次的接收计数器值为当前的接收计数器值。
    2. 检查是否正在接收:如果当前正在接收消息或者接收计数器值发生了变化,则跳过读取检查。
    3. 检查是否需要抛出异常:如果commandReceived为false即没有接收到命令,且monitorStarted为true即监控已经开始,并且异步任务线程池(ASYNC_TASKS)没有关闭,则抛出InactivityIOException异常。异常处理通过异步任务执行,以避免阻塞当前线程。如果异步任务被拒绝执行,并且异步任务没有关闭,则记录错误并重新抛出异常。
    4. 重置标志:最后,重置commandReceived标志,表示没有接收到命令。
  • 作用:定期检查连接状态,确保连接活跃。

  • 工作机制

    • 定时任务:默认每隔 readCheckTime(默认30秒) 时间执行一次。
    • 活跃性检查:每次执行时,检查自上次执行检查以来的时间间隔。如果超过 readCheckTime的90%且当前没有在接收消息和命令,则认为连接不活跃。
    • 处理不活跃连接:关闭连接等相关资源(如定时器、线程池等)并触发 InactivityIOException
  • 与服务端关闭的关系

    • 如果服务端主动关闭连接,客户端与服务端之间的心跳检测中断,readCheck中的inReceive.get() || currentCounter != previousCounter将为false,从而会触发 InactivityIOException
    • 如果服务端未关闭,但客户端长时间无数据传输,connectCheckerTask 也会关闭连接。

总结

  1. 问题根源
  • 服务端关闭导致连接中断,消费者失效,receive() 立即返回 null,while仍循环调用,导致日志刷屏。
  1. 解决核心
  • 休眠一秒:快速减少日志频率。
  1. 最终效果
  • 日志频率显著降低,系统稳定性提升。
  • 生产环境运行平稳,问题彻底解决。

经验分享

  1. 日志分析:遇到问题时,优先分析日志,定位异常类型和时间点。
  2. 连接管理:ActiveMQ 的连接和消费者状态需要仔细管理,避免资源泄漏。
  3. 快速解决:在紧急情况下,优先采用简单有效的方案(如休眠一秒),再逐步优化。

互动话题

你是否也遇到过类似的问题?欢迎在评论区分享你的经验和解决方案!如果本文对你有帮助,请点赞、转发支持!


关注公众号,获取更多技术干货!

从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密的更多相关文章

  1. zookeeper源码分析之五服务端(集群leader)处理请求流程

    leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...

  2. zookeeper源码分析之四服务端(单机)处理请求流程

    上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...

  3. 鸿蒙内核源码分析(信号消费篇) | 谁让CPU连续四次换栈运行 | 百篇博客分析OpenHarmony源码 | v49.04

    百篇博客系列篇.本篇为: v49.xx 鸿蒙内核源码分析(信号消费篇) | 谁让CPU连续四次换栈运行 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁 ...

  4. HDFS源码分析EditLog之获取编辑日志输入流

    在<HDFS源码分析之EditLogTailer>一文中,我们详细了解了编辑日志跟踪器EditLogTailer的实现,介绍了其内部编辑日志追踪线程EditLogTailerThread的 ...

  5. 4. 源码分析---SOFARPC服务端暴露

    服务端的示例 我们首先贴上我们的服务端的示例: public static void main(String[] args) { ServerConfig serverConfig = new Ser ...

  6. Nacos(二)源码分析Nacos服务端注册示例流程

    上回我们讲解了客户端配置好nacos后,是如何进行注册到服务器的,那我们今天来讲解一下服务器端接收到注册实例请求后会做怎么样的处理. 首先还是把博主画的源码分析图例发一下,让大家对整个流程有一个大概的 ...

  7. Kafka源码分析(三) - Server端 - 消息存储

    系列文章目录 https://zhuanlan.zhihu.com/p/367683572 目录 系列文章目录 一. 业务模型 1.1 概念梳理 1.2 文件分析 1.2.1 数据目录 1.2.2 . ...

  8. Netty源码分析之服务端启动过程

    一.首先来看一段服务端的示例代码: public class NettyTestServer { public void bind(int port) throws Exception{ EventL ...

  9. zookeeper源码分析之一服务端启动过程

    zookeeper简介 zookeeper是为分布式应用提供分布式协作服务的开源软件.它提供了一组简单的原子操作,分布式应用可以基于这些原子操作来实现更高层次的同步服务,配置维护,组管理和命名.zoo ...

  10. TeamTalk源码分析之服务端描述

    TTServer(TeamTalk服务器端)主要包含了以下几种服务器: LoginServer (C++): 登录服务器,分配一个负载小的MsgServer给客户端使用 MsgServer (C++) ...

随机推荐

  1. UWP 读写文件

    List<Pics> pics = new List<Pics>(); for (int i = 0; i < 2000; i++) { pics.Add(new Pic ...

  2. 彻底讲透Spring AOP动态代理,原理源码深度剖析!

    1.AOP:[动态代理]定义 指在程序运行期间动态的将某段代码切入到指定方法指定位置进行运行的编程方式: 2.基于注解aop的开发流程 1.导入aop模块:Spring AOP:(spring-asp ...

  3. XReport通过数据控制控件是否打印

    需求场景:医嘱单在患者出院的时候,需要标记一条红线,表示以下没有医嘱了.数据库中此记录的一个字段属性isRed值来标记这一行. 实现:XReport报表的明细区域增加一个line1对象.然后在明细表格 ...

  4. dart变量类型详解

    1==> 三个单引号的作用 String Str = ''' qijqowjdo 哈哈嘿嘿黑 '''; print(Str); 这样使用三个单引号,输出来换行:方便我们观看而已哈 2==> ...

  5. ulimit命令 控制服务器资源

    命   令:ulimit功   能:控制shell程序的资源语 法:ulimit [-aHS][-c <core文件上限>][-d <数据节区大小>][-f <文件大 小 ...

  6. Sdcb Chats 重磅更新:深度集成 DeepSeek-R1,思维链让 AI 更透明!

    Sdcb Chats 是一个强大且易于部署的 ChatGPT 前端,旨在帮助用户轻松接入和管理各种主流的大语言模型. Sdcb Chats 主要特性: 广泛的大模型支持: 已支持 15 种不同的大语言 ...

  7. 任务调度器Azkaban(Azkaban环境部署)

    文章链接:https://www.cnblogs.com/liugp/p/16273966.html

  8. 本地一键运行大模型神器Ollama + DeepSeek R1尝鲜指南

    本地一键运行大模型神器Ollama + DeepSeek R1尝鲜指南 作为AI领域的弄潮儿,你是否苦恼于云端大模型API的高昂成本?想在本机零门槛体验顶尖开源模型?这篇保姆级教程将带你解锁「Olla ...

  9. 11. Docker 微服务实战(将项目打包生成镜像,在 Docker 当中作为容器实例运行)

    11. Docker 微服务实战(将项目打包生成镜像,在 Docker 当中作为容器实例运行) @ 目录 11. Docker 微服务实战(将项目打包生成镜像,在 Docker 当中作为容器实例运行) ...

  10. .NET Core 托管堆内存泄露/CPU异常的常见思路

    常见的思路 内存泄露 托管内存暴涨大多数原因都是因为对象被GC Root(stack,gchandle,finalizequeue)持有,所以一直无法释放,所以观察的重点都在对象的可疑GC Root ...