
ActiveMQ 跑得好好的突然某个节点 CPU 飙到 100% 多核消息堆积业务超时线上告警炸成一片。这种场景做消息队列运维的朋友基本都碰过。问题在于ActiveMQ 高 CPU 这种事很多时候并不是“重启一下就好”的——你重启完现场全没了查了半天没查到根因过几天又来一次。我这些年排查过不少类似的故障最终沉淀下来一套“一键采集现场 事后分析”的诊断脚本这次把整个思路、脚本实现和排查过程完整整理出来新手可以直接照着用老手也可以参考里面的采集维度。这里说清楚这个脚本能干什么把出问题那一瞬间的系统负载、进程线程、JVM 堆和 GC、ActiveMQ 消息积压情况全部抓下来生成一组带时间戳的快照文件然后你拿着这些文件慢慢定位是“业务代码空循环”“GC 停顿”还是“磁盘刷写”造成的 CPU 压力。它解决的核心痛点不是一步到位把 CPU 调好而是解决“出了事以后现场没了无从下手”的问题。算是一个非常适合接手 ActiveMQ 集群、Spring Boot 整合 ActiveMQ 业务系统的后台开发、运维和中台同学的实操工具。1. 高 CPU 问题从何而来——先搞清楚对手再动手先说原理。Java 应用ActiveMQ 本质就是 Java 进程CPU 飙高无非几种情况业务代码里的死循环、对象频繁创建导致 GC 压力大、线程锁竞争严重引起自旋、以及更底层的磁盘 I/O 等待和内核态消耗。你要知道光看“CPU 100%”是没用的CPU 监控只能告诉你“哪台机器出了问题”不能告诉你“为什么出了问题”。真正要定位的是三个层级哪个进程 CPU 高、进程里哪个线程 CPU 高、线程正在执行哪段代码。1.1 最常见的三类元凶我接触到的 ActiveMQ 高 CPU 现场大致可以归成三类。第一类消费者逻辑里的忙等或空转。最常见的写法是消费者里用了while (true)去轮询某个条件条件一直不满足线程就在那疯狂自旋或者消费失败后没有做退避直接进入重试循环一边打日志一边重试CPU 瞬间就被打满。这类问题在 Spring Boot 整合 ActiveMQ 的工程里尤其常见因为很多同学对DefaultMessageListenerContainer的事务和确认机制理解不深导致消息一直接收、处理失败、又马上重试。第二类JVM GC 压力。ActiveMQ 内部有大量的对象创建和消息字节拷贝如果堆分配过小、或者存在内存泄漏比如全局 Map 只放不收就会导致频繁 Full GC。GC 线程是很吃 CPU 的尤其在 G1 并发标记阶段多个后台线程会同时占满多个核表面看起来就是 Java 进程 CPU 飙升但实际执行业务逻辑的线程反而在等待。第三类持久化和磁盘刷写。ActiveMQ 默认的 KahaDB 在持久化消息时要做磁盘同步如果消息量大、磁盘又是机械盘或者 iowait 很高内核态 CPU 会明显走高。这种场景下 top 里能看到sy系统态 CPU占比很高但 jstack 却抓不到明显的业务热点线程因为压力来自文件落盘。三类问题对应完全不同的处置手段改代码、调堆、换存储/调整刷盘策略。所以盲猜没用必须靠现场数据来判断。1.2 为什么“先抓数据再分析”才是正确姿势很多人遇到高 CPU第一反应是重启。我强烈不建议除非服务已经完全不可用。原因是高 CPU 故障的内存现场线程堆栈、堆快照是定位根因的唯一证据一旦重启JVM 进程退出这些数据全部消失。你唯一能留下的只有监控曲线而曲线只能告诉你“它高了”不能告诉你“哪一行代码导致的”。我的习惯是机器上常备一个诊断脚本出问题后 30 秒内完成现场采集然后再决定要不要重启。采集完再快速分析如果实在顶不住要恢复服务重启也能接受因为证据已经留下来了。这就是“先止血、再查因”的运维思路脚本本身就是一个自动化的现场保护工具。顺带说一句有些做 Windows 部署的朋友会问系统进程ntoskrnl一直 CPU 占用高是怎么回事。这里提一句作为类比在 Windows 上经常看到系统内核进程 CPU 高这跟你 Java 应用本身 CPU 高是两码事。ActiveMQ 的持久化写盘、网络收发都会走到系统调用如果磁盘压力大你会看到系统态 CPU 高而 jstack 抓不到热点。所以采集数据时别只盯着 Java 进程us/sy/id/wa这几项 CPU 状态一定要记录后面分析方向会完全不同。2. 诊断脚本的整体设计——“先止血再查因”的实操思路脚本设计的初衷很简单出问题时我不希望在服务器上一个个敲命令时间紧、手忙脚乱还容易漏数据。所以我把整个采集过程做成一键脚本按下回车后自动生成一个以时间戳命名的目录把所有快照文件放进去。2.1 脚本要采集哪些数据我给脚本定义了四个采集维度缺一不可。第一个维度是系统级uptime、top总览、CPU 状态us/sy/wa、内存和磁盘使用率。它用来判断问题到底是单机资源问题还是集群共性问题。第二个维度是进程级和线程级通过top -H -p PID拉出进程内所有线程的 CPU 占用率找出哪个线程在“烧”CPU。再把这个线程的十进制 TID 转成十六进制用jstack PID打印线程堆栈在堆栈文件里搜nid0x...就能定位到本地线程对应的 Java 线程。第三个维度是JVM 内部jstat -gcutil看 GC 使用率变化、jmap -heap看堆配置和分区占用、GC 日志里的停顿时间。这一维度用来判断是不是 GC 引起的 CPU 峰值。第四个维度是ActiveMQ 业务指标队列/主题的待消费消息数、消费者数量、内存和存储水位。这些数据一般从 ActiveMQ Web 控制台默认 8161 端口或 JMX 拿。脚本里可以提示人工去控制台看或者用 curl 调 Jolokia/REST 接口抓 JSON 回来。积压量是判断“CPU 高是原因还是结果”的关键——如果 CPU 高是因为消费不过来导致的重试风暴积压量会持续增长如果 CPU 高但积压量很低那多半是消息正常但线程在空转。四个维度采集完成后再配合 ActiveMQ 的日志文件data/activemq.log和 GC 日志基本就能组成一个完整的“事故黑匣子”。2.2 命令选型与时间点选择这里有一个关键点单次采集只能看到结果看不到趋势。比如 GC 占用率单独看一次是 10%你没法判断它是在爬升还是在下降。所以脚本里对可变指标要做多时间点采样最常用的是每几秒采一次top -H连续采 3~5 次看 CPU 高的线程是稳定同一个还是来回换。稳定同一个多半是死循环或者锁竞争来回换可能是 GC 线程在轮转。每 1 秒采一次jstat -gcutil连续采 10 次能看出 GC 频率和趋势是平稳、上涨还是即将 OOM。每 3 秒打一次jstack连续打 3 份以上。因为线程状态瞬息万变单份堆栈可能恰好抓到线程在Object.wait给不出有效信号多份对比后热点方法会反复出现定位就准确得多。命令选型上top、jstack、jstat、jmap都是 JDK 自带的命令不需要额外安装这是脚本能快速落地的基础。如果机器上用的是 OpenJDK命令路径一般在$JAVA_HOME/bin下脚本里最好加上自动从ps -ef的启动命令里反推JAVA_HOME的逻辑避免执行的时候提示“找不到命令”。提示jmap -heap和jstack在 JDK 高版本里需要与 JVM 进程相同用户权限执行如果 ActiveMQ 跑在activemq用户下你当前是root或普通用户时可能会报Unable to open socket file这时候要么切到该用户执行要么在脚本里加上sudo -u activemq做封装。3. 脚本核心实现——直接可用的诊断工具这一节给的是实实在在的脚本代码。我会拆成几段讲每段都会说明“为什么这么写”。整体脚本基于 Bash适用于 Linux 服务器ActiveMQ 版本 5.x、JDK 8/11 都验证过。3.1 一键采集系统与 JVM 运行状态先看脚本的主干部分。第一步是自动识别 ActiveMQ 进程 PID。很多服务器上会有多个 Java 进程不能随便pgrep java要结合进程启动参数里的 ActiveMQ 特征来过滤#!/bin/bash # activemq_cpu_diag.sh # 用法: ./activemq_cpu_diag.sh [PID] PID$1 if [ -z $PID ]; then PID$(ps -ef | grep -E activemq|ActiveMQ | grep java | grep -v grep | awk {print $2} | head -1) fi if [ -z $PID ]; then echo 未找到 ActiveMQ Java 进程请手动指定 PID: ./activemq_cpu_diag.sh PID exit 1 fi TS$(date %Y%m%d_%H%M%S) OUTDIR/tmp/activemq_diag_${TS} mkdir -p $OUTDIR echo 采集目录: $OUTDIR这里我说明一下过滤逻辑ActiveMQ 的启动脚本通常会在 Java 命令行里带上activemq相关路径比如-Dactivemq.home/opt/activemq或者是org.apache.activemq.xbean.XBeanBrokerFactory所以grep -E activemq|ActiveMQ能很好地区分它与其他业务 Java 进程。如果你用 Docker 容器部署宿主机的ps可能看不到容器内 JVM那就要改成进容器里执行或者用docker exec 容器名 top -H -b -n 1 -p 1这类方式脚本思路一样只是执行入口不同。接下来采集系统概览和进程 CPU 线程排行# 系统负载与 CPU 状态 { echo system uptime uptime echo echo top overview top -b -n 1 | head -30 } $OUTDIR/01_system_overview.txt # 进程内线程 CPU 排行前 20 echo threads CPU top $OUTDIR/02_threads_top.txt top -H -b -n 1 -p $PID | head -30 $OUTDIR/02_threads_top.txttop -H -b -n 1 -p是 Linux 下的标准写法-H显示线程模式-b批处理模式-n 1只执行一次。执行结果里第一列是线程 TID最后一列是线程名通常是 JVM 内部的线程名或者业务自定义线程名这些信息回看时很有用。3.2 线程 Dump 与 CPU 热点线程定位现在进入整个脚本的精华。你在top -H输出里看到 CPU 占用最高的线程 TID 是一个十进制数但jstack打印的线程号是十六进制nid比如nid0x1f4a。需要先把十进制的 TID 转成十六进制再去搜堆栈。脚本里可以这么做# 抓取 CPU 占用最高的线程 TID TOP_TID$(top -H -b -n 1 -p $PID | tail -n 8 | sort -k9 -rn | head -1 | awk {print $1}) TOP_TID_HEX$(printf %x\n $TOP_TID) echo CPU 最高的线程 TID$TOP_TID, 十六进制 nid0x${TOP_TID_HEX} | tee -a $OUTDIR/hot_thread.txt # 保存线程堆栈 jstack -l $PID $OUTDIR/jstack_1.txt 21注意tail -n 8是因为top -H的输出前 7 行是标题行和表头从第 8 行开始才是线程数据。sort -k9 -rn是按第 9 列%CPU降序排序。这里有个坑不同系统top -H的列位置可能不一样如果你的服务器 CPU 指标不在第 9 列排序就乱了需要根据实际输出调整-k参数。建议第一次跑脚本时先手动执行top -H -b -n 1 -p $PID | head看看表头。为了降低误判概率脚本会对 CPU 最高的几个线程都各做一次转十六进制并多次 jstackfor i in 1 2 3; do jstack -l $PID $OUTDIR/jstack_${i}.txt 21 sleep 3 done # 自动从 jstack 文件里统计高频线程状态 for f in $OUTDIR/jstack_*.txt; do echo $f $OUTDIR/thread_state_summary.txt grep java.lang.Thread.State $f | sort | uniq -c | sort -rn $OUTDIR/thread_state_summary.txt done这个thread_state_summary.txt很有用它把线程状态做了聚合。正常系统里大量线程是TIMED_WAITING或者WAITING但如果出现很多RUNNABLE线程并且 CPU 高就说明有线程在密集计算出现大量BLOCKED说明锁竞争严重。不需要逐行读堆栈先看统计分布能节约大量时间。定位热点线程还有个技巧把 CPU 最高的线程 TID 转成十六进制之后在 jstack 里搜索时要注意大小写统一printf %x输出是小写jstack里nid0x...通常也是小写。搜索命令grep -B2 -A10 nid0x${TOP_TID_HEX} $OUTDIR/jstack_1.txt能看到这个线程的完整 Java 调用栈是哪段业务代码、哪个类、哪一行基本一目了然。3.3 GC 与堆内存数据抓取GC 问题很容易被忽略因为 CPU 监控图里只显示 Java 进程整机飙高你不会自己去想“这是 Full GC 引起的”。所以脚本里必须带自动采集 GC 数据。# 连续采样 GC 使用率每次间隔 1 秒采 10 次 jstat -gcutil $PID 1000 10 $OUTDIR/03_gc_util.txt # 堆内存配置与当前占用 jmap -heap $PID $OUTDIR/04_heap_info.txt 21jstat -gcutil输出的列涉及 S0、S1、E、O、M、CCS、YGC、YGCT、FGC、FGCT、GCT对没接触过的同学解释一下E是 Eden 区使用率O是老年代使用率YGC和FGC是年轻代和 Full GC 次数GCT是累计 GC 耗时。看这份文件时重点关注两件事FGC次数是不是在快速上涨O老年代使用率是不是逼近 90% 以上。如果这两个条件都满足那说明高 CPU 的根源大概率是内存压力而不是业务空转。jmap -heap会打印出堆配置初始堆、最大堆和当前各代容量可以帮助判断是不是堆配置不合理。这里有个经验很多 ActiveMQ 节点堆配得过大比如 -Xmx 设为 8GB却跑在一个只有 4GB 的机器上结果系统频繁换页GC 长时间停顿CPU 飙高。ActiveMQ 本身对堆需求不算特别夸张KahaDB 的索引和消息缓存才是大户建议先根据实际消息量反推堆大小不要无脑调大。3.4 消息队列运行指标监控采集系统和 JVM 之外ActiveMQ 的业务侧指标同样关键。脚本没法直接读控制台但可以提示人工去确认也可以顺手用 curl 调一下 API。ActiveMQ 5.x 自带基于 Jolokia 的 REST 接口如果开启了认证默认用户名密码一般是admin/admin# 尝试从 Jolokia 取总消息数可选失败则跳过 BROKER_NAME$(grep -o brokerName[^]* $ACTIVEMQ_HOME/conf/activemq.xml | head -1 | cut -d -f2) curl -s -u admin:admin \ http://127.0.0.1:8161/api/jolokia/read/org.apache.activemq:typeBroker,brokerName${BROKER_NAME}/TotalMessageCount \ | tee $OUTDIR/05_broker_total_msg.json如果接口不通也没关系——让你自己打开http://服务器IP:8161/admin控制台切到 Queues/Topics 页面把积压数和消费者数截图或者记下来就行。积压数判断逻辑前面说过了这里补充一个更准确的经验对比“CPU 高”和“入库速率/出库速率”。如果队列入库速率远大于出库速率CPU 高是被消息洪峰冲高的如果出入库都正常但 CPU 仍然高那问题就在代码本身与积压无关。4. 一次真实的高 CPU 事件排查全过程记录脚本写得再好不拿来实战等于零。我分享一次真实事件看看这些文件是怎么配合使用的。4.1 现象确认与第一轮采集那天下午监控告警说某个 ActiveMQ 节点 CPU 使用率持续 200%两个核以上消息队列积压从几千涨到几十万下游消费者已大面积超时。我登录服务器后第一件事就是跑脚本确认 PID 后采集目录迅速生成了。当时首先看的是01_system_overview.txtuptime显示 15 分钟负载从 2.1 涨到 8.3top总览里waiowait大约是 18%不算特别高但us用户态 CPU 占了大头说明 JVM 内部真的有线程在大量计算然后看02_threads_top.txt前几名线程全是某个业务线程名MsgConsumer_XXX每个都占了 30% 以上 CPU。这时候基本可以判定不是 GC 问题——因为 GC 相关线程名是G1 Concurrent Mark、G1 Young RemSet Sampling这类出现的是业务线程名方向指向消费者代码。4.2 线程 Dump 怎么读——从线程堆栈定位业务代码我把 CPU 最高的线程 TID 转成十六进制在jstack_1.txt里搜索nid0x...看到了关键调用链MsgConsumer_15 #85 daemon prio5 os_prio0 cpu45602.21ms elapsed11234.33s tid0x00007f nid0x1a5a runnable [0x00007f...] java.lang.Thread.State: RUNNABLE at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:...) at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:...) at com.xxx.order.dao.OrderDao.queryNewOrders(OrderDao.java:47) at com.xxx.order.service.OrderService.process(OrderService.java:122) ...省略若干业务帧... at org.springframework.jms.listener.AbstractMessageListenerContainer.invokeListener(...)线索非常清楚消费者线程在从数据库查询订单而且这是在死循环里反复执行。查了代码之后发现这段逻辑原本是“成功消费消息后返回失败则标记补偿”结果某个异常分支里漏了return导致消息处理完后又重新走到查询逻辑形成了一种伪死循环。因为是同一个消息在同一线程里反复查询数据库消息并没有被正确 ack所以积压里反复出现同样的消息业务数据表一直被查CPU 被彻底打满。这里补充一个读堆栈的经验RUNNABLE状态不一定对应 CPU 消耗。JVM 里线程在等待网络 I/O 时也可能标记为RUNNABLE比如SocketInputStream.read。但只要看到cpu时间戳和堆栈里密集的某项业务操作就能确认它在真干活。4.3 最终定位与解决确认根因后处理就很简单了把漏掉的return补上然后通过 ActiveMQ 控制台把积压的脏消息清掉节点 CPU 立刻回落。整个排查过程从登录到定位不超过 15 分钟脚本生成的采集文件是决定性证据。如果没有这些现场数据只凭监控曲线去猜大概率会先调 JVM 参数、再重启节点折腾一晚上未必能找到那一行代码。通过这个案例我想强调一个概念脚本的价值不只是省事更是让事故处理从“猜”变成“查”。你手里有一份事故发生瞬间的完整快照技术人员在故障复盘时才有话可说、有据可查。5. 常见问题与排查技巧实录这部分是我把这个脚本用了无数次之后积累下来的实际问题按“脚本使用中的坑”和“排查 Java 进程 CPU 的技巧”两类整理。5.1 脚本本身可能遇到的问题问题一自动找 PID 找到的不是真实 JVM 进程。很多 ActiveMQ 安装包用 wrapper 方式启动ps -ef里能看到wrapper-linux-x86-64进程如果过滤条件写得不严格很容易把那个 C 守护进程当成 Java 进程。我的做法是增加过滤条件grep java并且把org.apache.activemq或-Dactivemq.home也放进关键字列表宁可多敲一个 PID也别抓错进程。问题二top -H里线程数量很多看着眼花。不要慌先sort -k9 -rn | head -10只留前几个热点线程剩下的不用管。另外注意PID列的数字在-H模式下是线程 IDTID不是进程 ID拿到后别直接去ps搜要转十六进制去jstack里找。注意同一时间点反复执行jstack时JVM 可能会短暂停顿这在诊断的时候是可接受的。但如果节点已经处于 Full GC 频繁触发状态连续 jstack 会加重停顿建议间隔从 3 秒拉长到 5 秒数量控制在 2~3 份即可。问题三权限导致jstack无法 attach。常见报错是Unable to open socket file: target process not responding or HotSpot VM not loaded。排查顺序是确认 PID 是否正确、确认是否用 JVM 所有者执行、确认 JDK 版本与 JVM 版本是否一致。实际场景里最容易被忽略的是 PATH 里少了jstack工具用 JDK8 启动的 ActiveMQ 环境却装了 JDK17 客户端版本不匹配也会失败。问题四采集完了不能立刻分析。脚本只是第一步分析的时候建议按这个顺序过一遍02_threads_top.txt看线程热点 →jstack_x.txt看热点线程栈 →03_gc_util.txt看 GC 趋势 →01_system_overview.txt排除系统干扰。不要一上来就研究 GC 日志先判断是业务线程还是 GC 线程在消耗 CPU。5.2 高 CPU 排查的独家避坑经验经验一不要只看进程粒度的 CPU。假设进程 4 个核占满可能是 4 个线程各占一核也可能是 1 个线程把 4 个核都吃满。前者是“并发问题”后者往往是“热点代码操作了太多数据”。线程粒度的top -H能一眼区分但线程名相同不代表是同一段代码必须配合线程堆栈。经验二区分同步阻塞和忙等。Java 里Object.wait()也会让线程变成 WAITING但它几乎不耗 CPU而自旋锁会让线程保持在 RUNNABLE 且疯狂消耗 CPU。如果堆栈里多次出现sun.misc.Unsafe.park或java.util.concurrent相关帧而 CPU 又很高说明要么是锁竞争太激烈、要么是 JUC 组件使用不当比如在循环里反复lock.lock()然后立即释放。经验三ActiveMQ 的 JDBC 持久化、KahaDB 同步刷盘会造成内核态 CPU 高。前面强调过如果top里sy占比高而jstack热点线程对应不上业务代码那就要关注磁盘 I/O 和日志。这类问题可以通过调整syncPeriod、maxMemoryUsage等参数缓解但也要结合消息可靠性要求来权衡。经验四配合跟踪消息流程往往能找到真正的触发条件。有一次 CPU 高线程堆栈显示全在HashMap.get看起来是并发容器问题后来跟了一下消息链路才发现某条业务消息包含一个巨大的 JSON解析时反复触发 HashMap 扩容。所以堆栈里的“现象”和消息体里的“诱因”要联合起来看别只盯着代码一点查。6. 脚本后续还能怎么扩展脚本目前的定位是一把“事故即时照相机”但它完全可以扩展成“常态化巡检”。我在线上环境中会把采集命令抽成定时任务每 5 分钟做一次轻量快照只在 CPU 阈值超过警戒时保留文件平时丢弃这样事故发生时至少有一份周期的“背景快照”可对比。更进一步可以把top -H的热点线程自动汇总成 Top10 报告、把jstack的四类线程状态做成简易报表推送到 IM 群。这些后续优化看各自团队需要核心的采集逻辑万变不离其宗。最后分享一个小技巧脚本建议放在/usr/local/bin/下并加执行权限同时把 ActiveMQ 进程的 PID 写入固定文件比如$ACTIVEMQ_HOME/data/activemq.pid脚本里优先读 PID 文件。这样即使多个 Java 进程混跑也不会误抓。我自己在几套环境里就是靠这个小改动把诊断脚本从“偶尔手跑”变成了“出事必跑的工具”几乎每次都能在 30 分钟内交出第一份定位结论。做个可靠的运维或者后台手边有这样一套能随时给现场留证据的工具比临场随机敲命令要踏实得多。