凌晨被告警叫醒,发现某台 consumer 服务器 load5 飙到 8.5。打开 top -H 找凶手,CPU 大户竟然是给我们做监控的 micrometer 自己

这次复盘的不是业务 bug、不是慢 SQL、不是慢下游——是 “监控指标” 把自己挂载的进程打到了 60%+ 一个核。让 VM Thread 持续吃 25% CPU 的元凶,是 6 个看着 “人畜无害” 的 Gauge。

本文从告警到根因走完整链路,顺手把国内技术博客很少深入讲的 safepoint 风暴 这个话题讲透。


一、问题现象

1.1 告警

凌晨 10:48,云监控发来一条不起眼的报警:

报警规则:  CPU.load
监控指标:  (Agent)load.5m
报警条件:  连续满足 5 次,平均值 >= 6
当前值:    8.51 个
持续时间:  5 分钟
实例:      consumer-host-A

load5 = 8.5,机器 4 核 —— load 已经超过核数 2 倍,CPU 排队等服务的进程多到溢出。

1.2 现场(top)

SSH 上去,第一个命令永远是 top

$ top -b -n 1 | head -10
top - 10:37:12 up 649 days, 22:43, load average: 2.87, 3.20, 3.85
Tasks: 264 total,   1 running, 263 sleeping
%Cpu(s): 32.8 us,  3.3 sy, 0.0 ni, 63.9 id, 0.0 wa
MiB Mem :   7727.3 total,    137.8 free,   5925.5 used,   1663.9 buff/cache

  PID USER   PR  NI  VIRT  RES  SHR S  %CPU %MEM   TIME+   COMMAND
830757 app   20   0  11.1g 4.3g 11688 S 143.8 57.0 1256:33  java

一个 Java 进程 CPU 143.8%(>100% 表示多核累加),占了 1.4 个核 —— 看上去不算夸张,但 load 已经飙到 8。问题不在单核满载,而在有大量任务在排队等 CPU

1.3 业务也在恶化

打开 Prometheus 看 XXL-Job 任务的 TP90 对比一周前:

(
  avg by (exported_job) (avg_over_time(xxx_xxl_duration_seconds{quantile="0.9"}[1d]))
  /
  avg by (exported_job) (avg_over_time(xxx_xxl_duration_seconds{quantile="0.9"}[1d] offset 7d))
  - 1
) > 0.1
任务 TP90 涨幅 TP90 绝对值 当日执行次数
taskA +164% 0.78 s 8,625
taskB +106% 0.39 s 29,449
taskC +70% 0.97 s 34,193
taskD +56% 0.26 s 719

Grafana 大盘上能直观看到从 5 月 15 日起耗时整体抬升:

在这里插入图片描述

几个任务一周内 TP90 翻倍。CPU 高、load 高、业务慢——根因到底是哪一件,还是另有元凶?


二、排查现场

2.1 top -H 看进程内部线程

$ top -H -b -n 1 -p 830757
%Cpu(s): 39.5 us,  9.2 sy

  PID USER  ...  %CPU  COMMAND
1129021 app    37.0  java
1127842 app    22.1  java
1129114 app    20.3  java
...
Threads: 3904 total,  6 running

3904 个线程 —— 一个普通 Tomcat 应用线程数也就几百,这个数字明显异常。

记下几个最高 CPU 的 TID(1129021、1127842、1129114),转 hex,准备 jstack

$ printf '%x\n' 1129021
113a3d

2.2 jstack 抓栈 —— 但抓不准

$ jstack 830757 | grep 'nid=0x113a3d' -A 10
"http-nio-7002-exec-9" #1311 daemon prio=5 nid=0x113a3d waiting on condition
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        ...
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)

WAITING?刚才 top 里它明明 37% CPU 啊?

第二次、第三次 jstack 同样:还是 WAITING。

真相是:高 CPU 线程瞬时切换很快,jstack 那一刻它可能正好回 take() 阻塞,等下一个任务。要抓住"现行犯",必须短时间内连续多轮采样 + 比对

2.3 hot-stack.sh:多轮采样的"现行犯"抓捕术

写了个小脚本,并行跑 top -Hjstack 多轮,找出多次出现在 top 高 CPU 列表的线程,然后从对应轮的 jstack 里挑一个 RUNNABLE/BLOCKED 的栈:

#!/bin/bash
# hot-stack.sh —— 抓 Java 进程当前 CPU 热点线程的栈
# 用法:./hot-stack.sh <PID> [采样轮数=5] [间隔秒=0.3]
set -u

PID="${1:-}"
ROUNDS="${2:-5}"
SLEEP="${3:-0.3}"

[ -z "$PID" ] && { echo "用法: $0 <PID> [轮数=5] [间隔=0.3s]"; exit 1; }

TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT

echo "采样 $ROUNDS 轮 × ${SLEEP}s..."
for i in $(seq 1 "$ROUNDS"); do
    top -H -b -n 1 -p "$PID" > "$TMP/top.$i" 2>/dev/null &
    jstack "$PID"           > "$TMP/stack.$i" 2>/dev/null &
    wait
    sleep "$SLEEP"
done

# 每轮 top 提取 (TID, %CPU)
for i in $(seq 1 "$ROUNDS"); do
    awk '/^ *[0-9]+ / && $9+0 > 1.0 {print $1, $9}' "$TMP/top.$i" > "$TMP/cpu.$i"
done

# 累计 TID 出现次数
for i in $(seq 1 "$ROUNDS"); do
    awk '{print $1}' "$TMP/cpu.$i" | head -10
done | sort | uniq -c | sort -rn > "$TMP/hot.txt"

[ ! -s "$TMP/hot.txt" ] && { echo "没抓到 CPU > 1% 的线程"; exit 0; }

echo ""
echo "热点 TID($ROUNDS 轮里出现次数排序):"
echo "============================================================"
printf "%-6s %-10s %-10s\n" "命中" "TID" "nid(hex)"
head -10 "$TMP/hot.txt" | while read -r count tid; do
    printf "%-6s %-10s 0x%s\n" "$count" "$tid" "$(printf '%x' "$tid")"
done

TOP5=$(awk '{print $2}' "$TMP/hot.txt" | head -5)

extract_stack() {
    awk -v nid="nid=0x$1 " '
        $0 ~ nid {p=1; print; next}
        p && /^$/ {p=0}
        p {print}
    ' "$2"
}

get_cpu_history() {
    local tid="$1" hist="" cpu
    for i in $(seq 1 "$ROUNDS"); do
        cpu=$(awk -v t="$tid" '$1 == t {print $2; exit}' "$TMP/cpu.$i")
        [ -z "$cpu" ] && cpu="-" || cpu="${cpu}%"
        hist="${hist}  ${cpu}"
    done
    echo "$hist"
}

for TID in $TOP5; do
    HEX=$(printf '%x' "$TID")
    CPU_HIST=$(get_cpu_history "$TID")
    BLOCK=""; FOUND=""

    for i in $(seq 1 "$ROUNDS"); do
        CUR=$(extract_stack "$HEX" "$TMP/stack.$i")
        if [ -n "$CUR" ] && echo "$CUR" | grep -q 'RUNNABLE\|BLOCKED'; then
            BLOCK="$CUR"; FOUND="$i"; break
        fi
    done

    if [ -n "$BLOCK" ]; then
        echo ""
        echo "============================================================"
        echo "  TID=$TID  nid=0x$HEX"
        echo "  各轮 CPU:${CPU_HIST}"
        echo "============================================================"
        echo "$BLOCK"
        echo ""
        echo "[来自第 $FOUND 轮 dump,状态: 活跃]"
    else
        LAST=$(extract_stack "$HEX" "$TMP/stack.$ROUNDS")
        if [ -n "$LAST" ]; then
            NAME=$(echo "$LAST" | head -1 | sed -E 's/^"([^"]+)".*/\1/')
            echo "[跳过-非活跃] TID=$TID  nid=0x$HEX  \"$NAME\"  各轮 CPU:${CPU_HIST}"
        fi
    fi
done

跑起来:

$ ./hot-stack.sh 830757
采样 5 轮 × 0.3s...

热点 TID(5 轮里出现次数排序):
============================================================
命中    TID        nid(hex)
5      1127834    0x11359a
3      1129030    0x113a46
2      1130020    0x113e24
...

============================================================
  TID=1129030  nid=0x113a46
  各轮 CPU:  -  -  58.3%  69.2%  69.6%
============================================================
"http-nio-7002-exec-9" #1311 daemon  nid=0x113a46 runnable
   java.lang.Thread.State: RUNNABLE
        at sun.management.ThreadImpl.getThreadInfo1(Native Method)
        at sun.management.ThreadImpl.getThreadInfo(ThreadImpl.java:178)
        at io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics.getThreadStateCount(JvmThreadMetrics.java:84)
        at io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics.lambda$bindTo$0(JvmThreadMetrics.java:74)
        at io.micrometer.core.instrument.internal.DefaultGauge.value(DefaultGauge.java:40)
        at io.micrometer.prometheus.PrometheusMeterRegistry.lambda$newGauge$3(PrometheusMeterRegistry.java:235)
        at io.micrometer.prometheus.MicrometerCollector.collect(MicrometerCollector.java:69)
        at io.prometheus.client.exporter.common.TextFormat.write004(TextFormat.java:22)
        at org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint.scrape(...)

第一只现行犯

ThreadMXBean.getThreadInfo1 (Native)
  ← micrometer JvmThreadMetrics.getThreadStateCount
  ← PrometheusMeterRegistry.newGauge
  ← TextFormat.write004
  ← PrometheusScrapeEndpoint.scrape  (= /actuator/prometheus)

这条 http-nio 线程正在处理 Prometheus 拉 metrics 的请求,结果它自己跑到 60% CPU。

2.4 另一只大户:VM Thread

[跳过-非活跃] TID=1127834  nid=0x11359a  "VM Thread"
              各轮 CPU:  20.0%  29.2%  20.8%  23.1%  21.7%

VM Thread 是 JVM 的内部协调线程,5 轮采样每轮都吃 20-29% CPU

VM Thread 持续高 CPU 通常只有三个原因:

  1. GC
  2. safepoint 同步
  3. JIT 编译

2.5 洗清 GC 嫌疑

jstat -gcutil 看 GC:

$ jstat -gcutil 830757 1000 10
  S0     S1     E      O      M     CCS    YGC    YGCT   FGC   FGCT    GCT
  0.00 100.00  26.39  60.79  91.22  86.80 23016 405.429   0   0.000 405.429
  0.00 100.00  34.72  60.79  91.22  86.80 23016 405.429   0   0.000 405.429
  ... 10 秒里 YGC 仅涨 2
  • YGC 每 5 秒一次,单次 17ms(约 0.3% CPU)—— 解释不了 VM Thread 25%
  • Old 区稳定 60.6~60.8% → 无内存泄漏
  • FGC = 0

GC 排除。剩下的只能是 safepoint 同步了。

2.6 算一下 Prometheus 抓取频率

$ grep '/actuator/prometheus' logs/access.log | awk '{print $4}' | uniq -c
   1 10:52:46
   1 10:52:47   ← 注意紧挨着
   1 10:52:56
   1 10:53:02
   1 10:53:06
   1 10:53:16
   1 10:53:17   ← 又紧挨着
   1 10:53:26
   ...

30 秒内出现 6 次访问。间隔模式 6, 4, 10, 1, 9 重复 —— 平均 5 秒一次,比通常的 15 秒一次高 3 倍。

看抓取方 IP:

$ grep '/actuator/prometheus' logs/access.log | awk '{print $1}' | sort | uniq -c
   4567 10.0.1.10
   3245 10.0.1.20

两个 Prometheus 实例都在抓(一个 15s 间隔、一个 10s 间隔,叠加起来约 6s 一次)。


三、分析原因:什么是 safepoint 风暴

到这一步,直接元凶已经清楚:

Prometheus 抓 /actuator/prometheus → 触发 micrometer 的 jvm.threads.states gauge → 内部调 ThreadMXBean.getThreadInfo() → 触发 JVM safepoint → 3900 线程被强制对齐 → VM Thread 持续协调。

但要理解"为什么这事这么贵",得先把 safepoint 讲明白。

3.1 什么是 safepoint:物业断电的比喻

safepoint = JVM 内的"集合检查点" —— 所有 Java 应用线程必须暂停在某个安全位置,让 JVM 自己做一些"全局视角的事"。

形象比喻:一栋大楼里有 3900 个员工在各自工位上干活,物业偶尔需要"全楼断电检修"。

为了不让正在用电梯的人摔下来,物业不能直接拉闸 —— 必须先广播"5 秒后停电",等所有人走到楼梯口(= safepoint)就位,才能停电检修,完事后再广播"恢复"。

把"物业" = VM Thread、“员工” = Java 业务线程、“停电检修” = GC / jstack / 拍线程快照……、“楼梯口” = safepoint。

JIT 编译的代码里到处都插入了 "safepoint 检查"指令 —— 在循环回边(每跑一圈检查一次)、方法返回、安全点 poll 页等位置。

3.2 谁会触发 safepoint

谁触发 干啥
GC 标记存活对象、移动对象、修引用
jstack / jmap / jcmd 拍线程栈、拍堆快照
ThreadMXBean.getThreadInfo() ⬅️ 本次主角 拍所有线程的状态/栈/锁
Deoptimization JIT 发现编译错了,回退到解释器
偏向锁撤销、Object.wait/notify(老 JDK) 内部状态机切换
类重定义(Java agent / hotswap) 改字节码

3.3 safepoint 协调全过程

T0: VM Thread:  "我要做 GC,请所有线程到 safepoint"
                ↓ 设置全局 safepoint flag

T1: 线程A:  下次循环回边时看到 flag → 立刻停
    线程B:  正在跑 Math.sqrt → 继续跑几条指令到下一个 check
    线程C:  正在 epollWait(native)→ 内核态算 safepoint 已就位
    ...3900 个线程各自到达检查点

T2: VM Thread 轮询:所有线程都到了吗?→ 等齐
T3: VM Thread:  开始干活(这段叫 STW,Stop-The-World,全线程暂停)
T4: VM Thread:  释放 safepoint flag
T5: 所有线程恢复

总 STW = (T2 - T1) 等齐时间 + (T3 - T2) 干活时间

3.4 为什么线程数多让 safepoint 变贵

回到大楼比喻:

维度 100 人 3900 人
广播"集合" 0 0
每人到楼梯口 几秒 几秒
等齐 = max(每人时间) 几秒 可能几秒,但更可能几十秒(尾延迟)
物业实际干活 1 分钟 1 分钟(不变)

线程多 → 尾延迟(tail latency)拖累 max → 等齐时间被拉长

ThreadMXBean.getThreadInfo() 还多一笔账:它不只要 safepoint,还要遍历所有 3900 个线程的内部数据结构拷贝,本身就是 O(N)。

3.5 micrometer 的 JvmThreadMetrics:6 倍放大器

打开 JvmThreadMetrics 源码(简化版):

public class JvmThreadMetrics implements MeterBinder {
    @Override
    public void bindTo(MeterRegistry registry) {
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();

        // 这四个都是 O(1) 的:
        Gauge.builder("jvm.threads.live",    bean, ThreadMXBean::getThreadCount)
             .register(registry);
        Gauge.builder("jvm.threads.peak",    bean, ThreadMXBean::getPeakThreadCount)
             .register(registry);
        Gauge.builder("jvm.threads.daemon",  bean, ThreadMXBean::getDaemonThreadCount)
             .register(registry);
        Gauge.builder("jvm.threads.started", bean, ThreadMXBean::getTotalStartedThreadCount)
             .register(registry);

        // ⚠️ 这一段是 O(N) × 6(Thread.State 有 6 个枚举值):
        for (Thread.State state : Thread.State.values()) {
            Gauge.builder("jvm.threads.states", bean,
                    b -> Arrays.stream(b.getThreadInfo(b.getAllThreadIds()))
                                .filter(info -> info.getThreadState() == state)
                                .count())
                .tags("state", state.name().toLowerCase())
                .register(registry);
        }
    }
}

注意最后那个 for 循环:为每个 Thread.State 注册一个 gaugeThread.State 有 6 个枚举值(NEW / RUNNABLE / BLOCKED / WAITING / TIMED_WAITING / TERMINATED),所以这里一口气注册了 6 个 gauge。

每个 gauge 在 scrape 时都要跑一次 getThreadInfo(getAllThreadIds()) —— 这是个 O(N) 操作,而且每次都重新拍快照

也就是说,每次 Prometheus 抓一次,要触发 6 次 safepoint + 6 次全线程快照

3.6 算账:风暴是怎么形成的

触发 safepoint 的源(多个并发):
  · YGC                          每 5 秒一次(必要的)
  · micrometer scrape            每 6 秒一次 × 6 个 gauge = 1 秒一次
  · 偶发的 jstack / jcmd         忽略

每次 safepoint:
  · 等齐 3900 线程:几 ms ~ 几十 ms 尾延迟
  · 拍快照:O(N) = O(3900)

VM Thread 持续被"喊集合 → 等齐 → 干活 → 释放"占满
→ VM Thread 持续 25% CPU
→ 业务线程频繁被 STW 切碎,延迟抖动
→ load 飙高

可视化:

正常情况(线程数 < 200):
  safepoint:    |     |     |     |     |
  VM Thread:    短   短   短   短   短     ── CPU 占用低

本次(3900 线程 + 高频触发):
  safepoint:    ||||  ||||  ||||  ||||  ||||
  VM Thread:    长长长 长长长 长长长 长长长  ── CPU 持续 25%

  其他线程:  ┊STW┊┊STW┊┊STW┊┊STW┊┊STW┊
            (被频繁打断,业务延迟抖动)

"风暴"不是一次性爆炸,是持续不断的小爆炸 —— 把 VM Thread 烧热、把业务线程频繁打断。

3.7 完整因果链

1. 业务设计   事件总线让每个事件 Bean 单独建一个 MQ Consumer → 3900 线程
2. 监控设计   jvm.threads.states 是 O(N) × 6 个 gauge
3. 抓取频率   两个 Prometheus 实例叠加 = 6s 一次
4. JVM 行为   每次抓取触发 safepoint,等齐 3900 线程
5. 受害方     VM Thread 25% / 业务线程 STW / Redisson channel PING 超时

哦对,Redisson PING 超时 —— 副产品。Netty EventLoop 在 epoll 时被频繁 STW,导致 pingConnectionInterval 周期内 PING 字节没发出,触发 RedisTimeoutException: Unable to send PING command。这次连 Redis 都"莫名其妙"地超时了。


四、解决方案

4.1 立即止血:减少抓取方

两个 Prometheus 实例其中一个是历史遗留,干掉:

# 删除其中一个 prometheus.yml 的 scrape config
# scrape_configs:
#   - job_name: 'consumer'
#     static_configs:
#       - targets: ['consumer-host-A:7002']

预期效果:抓取频率从 6s 一次降到 10s 一次,safepoint 风暴次数立刻减半

但这只是"少抓",单次抓取的 60%+ CPU 风暴还在

4.2 治本:关 jvm.threads.states 指标

Spring Boot + micrometer 提供了精细化的开关:

# application.yml
management:
  metrics:
    enable:
      # 关闭按状态分桶的线程指标(对应 Prometheus 指标 jvm_threads_states_threads)。
      # 其内部 ThreadMXBean.getThreadInfo(allIds, 0) 是 O(N) 且触发 safepoint,
      # 在大线程数场景下每次 Prometheus scrape 都会让 http-nio 跑到 60%+ CPU、
      # 并间接推高 VM Thread。
      # 仍保留便宜的 jvm.threads.live / peak / daemon / started 指标(都是 O(1))。
      jvm.threads.states: false

关键设计:精确关闭"贵的"那一个,保留便宜的几个 jvm_threads_* 指标。这样你仍然能在 Grafana 上看到线程数总量、峰值、daemon 占比 —— 只是失去了"按 state 分桶"的能力。

⚠️ 不要一刀切关 management.metrics.binders.jvm: false。那会把 jvm.memoryjvm.gc 等所有 JVM 指标都干掉,监控就瞎了。

4.3 长期:降低线程基数

事件总线让每个事件 Bean 单独建一个 MQ Consumer 的设计是根因之一,不解决迟早还会被别的 O(N) 指标拖死。

中长期方向:

  • 评估事件总线是否能让多个事件 Bean 共享同一个 MQ Consumer
  • 提升单 Consumer 的并发度配置,降低 Container 数量
  • consumeThreadMin/Max 从默认 20 调小(如 5),合计就能从 ~1500 降到 ~400

但这是另一个故事了,本文不展开。


五、解决与验证

5.1 上线改造清单

# 改造 效果
1 停掉一个 Prometheus 抓取 抓取频率 6s → 10s
2 配置 jvm.threads.states: false scrape 单次 CPU 风暴归零
3 修复业务侧某个不停止的任务(顺手修) 减少无效压力

5.2 验证命令

部署后这套三件套验证:

# 1. 确认 Prometheus 端点上指标已消失
curl http://127.0.0.1:7002/actuator/prometheus | grep -c '^jvm_threads_states'
# 改前应该是 6 行,改后是 0

# 2. 跑 hot-stack 看 http-nio 是否还吃 60%+
./hot-stack.sh <PID>

# 3. 看 VM Thread 是否同步下来
jstat -gcutil <PID> 1000 10

5.3 实际效果

指标 改前 改后
VM Thread CPU 持续 20~29% < 5%
http-nio scrape CPU 峰值 单次 60~70% 平时 < 5%
load5 6~8(峰值 8.51) 1.5~3
业务 RedisTimeout 频次 ~80 条/天 < 5 条/天
TP90 业务任务(taskA) 0.78 s 0.31 s

CPU / 内存 / 负载三连降——这张是云监控的对比图,改造发生在中午,曲线断崖式下来:

在这里插入图片描述

更细粒度看 Grafana 的核心指标大盘,VM Thread 和 http-nio 都恢复正常:

在这里插入图片描述

业务侧 XXL-Job 任务耗时同步下降:

在这里插入图片描述

业务 TP90 也跟着回落 —— 因为业务线程不再被 safepoint 频繁切片


六、举一反三

6.1 hot-stack.sh:现行犯抓捕术

文章第二节给了完整脚本。核心思想

  1. 单次 jstack 抓不准(高 CPU 线程瞬时切换很快)
  2. 多轮采样 + 命中频次排序,反复出现的才是真凶
  3. 同一线程在多轮 jstack 里,优先选 RUNNABLE / BLOCKED 状态的那一帧栈
  4. 抓不到活跃栈的(VM Thread 等),输出"非活跃"标记,不打整段栈污染输出

6.2 一句话识别 safepoint 风暴

CPU usr 高 + load 飙、VM Thread 持续吃 CPU、GC 频率正常、业务接口延迟抖动 —— 先怀疑 safepoint 风暴

特别在以下场景:

  • 线程数 > 1000
  • 监控接了 micrometer + Prometheus
  • 业务无明显瓶颈

6.3 jvm.threads.states 不是唯一的"O(N) 监控指标"

类似坑还有:

指标 实现 大线程数下表现
jvm.threads.states ThreadMXBean.getThreadInfo(allIds) × 6 ⚠️ 最贵
jvm.gc.pause GC notification callback 通常 O(1)
process.files.open 平台相关 Linux 上 O(打开文件数)
自定义 gauge 里调 Thread.getAllStackTraces() O(N) + safepoint ⚠️ 自定义代码也要警惕

写自定义 metric 的原则:metric 计算应该是 O(1),不能 O(N)。需要遍历的,要么缓存(异步更新),要么干脆不暴露。

6.4 排查 SOP 速查表

下次再遇到 load 飙高但 GC 正常时,照着跑:

步骤 命令 目的
1 top -b -n 1top -H -p $PID 找高 CPU 进程 + 高 CPU 线程数总数
2 ./hot-stack.sh $PID 多轮采样抓真活跃栈
3 jstat -gcutil $PID 1000 10 洗清 GC 嫌疑
4 grep '/actuator/prometheus' access.log | awk '{print $4}' | uniq -c 看 scrape 频率是否异常
5 curl /actuator/prometheus | wc -l 看暴露的 metric 总数(> 5000 也偏多)
6 看 hot-stack 栈帧是否有 micrometer / ThreadImpl.getThreadInfo 命中 → safepoint 风暴

七、总结

凌晨那条告警,揭开的真相是:

3900 个线程 + 每 6 秒一次的 ThreadMXBean.getThreadInfo + 6 个 O(N) gauge = 监控自己把进程压在地上摩擦。

这种"监控反噬"在国内很少被深入复盘,因为它太隐蔽:

  • 栈里看不到业务代码 → 业务同学一脸懵
  • GC 没问题 → SRE 排除一项嫌疑
  • CPU 不算特别高 → 看着像"机器配置不够"
  • 业务接口也慢 → 像下游有问题

唯一的破解办法是:对 VM Thread 持续高 CPU 这个信号有敏感度。它几乎只能来自三件事 —— GC、safepoint、JIT —— 前后者用 jstat-XX:+PrintCompilation 就能排除,剩下的只有 safepoint。

一句话送给程序员们

别让监控成了进程的"勒索软件" —— 每次抓数据都要 3900 个员工集合,这监控不要也罢。


延伸阅读(本系列其他线上问题排查实战):


如果这篇文章帮你下次少跑几个 jstack 命令,那它就值了。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐