【Linux】CPU 100% 怎么排查?——top、pidstat、jstack 到线程定位实战

【Linux】CPU 100% 怎么排查?------top、pidstat、jstack 到线程定位实战

线上机器 CPU 突然打满时,最忌讳的动作是直接重启。重启当然可能让告警消失,但现场也一起没了:到底是某个业务循环、GC 线程、锁竞争、日志风暴,还是宿主机 steal 时间异常,事后只能靠猜。更稳的处理方式是先把故障缩小到"哪台机器、哪个进程、哪个线程、哪段代码",再决定限流、摘实例、回滚或修代码。本文用一个 Java 小程序复现"单个线程持续消耗 CPU"的现场,把 toppidstatjstackjcmd/proc 背后的含义串起来。

1. 先判断 CPU 高在哪里

CPU 100% 只是结果,不是原因。第一步要看它主要高在用户态、系统态、iowait 还是 steal。用户态高通常意味着业务代码、序列化、正则、压缩、加密、JSON 处理、死循环或计算密集任务在跑;系统态高可能来自频繁系统调用、网络包处理、内核锁或容器运行时开销;iowait 高说明任务在等 I/O,并不等于 CPU 正在计算;steal 高则常见于虚拟化环境,表示虚拟 CPU 被宿主机调度走了。

Linux 的 /proc/stat 会暴露 CPU 在不同状态上的时间,单位通常是 USER_HZ。man-pages 对 user、system、idle、iowait、steal 等字段有明确说明,其中 iowait 还特别提示它并不总是可靠,因为多核机器上等待 I/O 的任务不一定运行在某个 CPU 上。这个细节很重要:看到 iowait 高,不要把它当作"CPU 算力不够",它更像是磁盘、网络存储或下游响应慢把任务卡住了。

2. 命令顺序不要乱

生产排查可以按这条线走:top 看整机和进程,top -Hp <pid>pidstat -t -p <pid> 1 看线程,printf '%x\n' <tid> 把线程 ID 转成十六进制,再用 jstack <pid>jcmd <pid> Thread.print 搜索 nid=0x...。这里最容易卡住的是线程 ID 对不上:Linux 工具显示的通常是十进制 TID,而 Java 线程 dump 里的 nid 常写成十六进制,所以中间必须转换。

bash 复制代码
top
top -Hp 进程PID
pidstat -t -p 进程PID 1
printf '%x\n' 线程TID
jcmd 进程PID Thread.print | grep -A 30 'nid=0x十六进制TID'

/proc/<pid>/stat 也能解释为什么这些工具能算 CPU。man-pages 里说明了进程的 utimestime:前者是进程在用户态被调度运行的时间,后者是内核态运行时间,单位是 clock ticks。工具通过两次采样之间这些计数的差值,结合时间间隔和 CPU 核数,得到你看到的 CPU 百分比。所以排查 CPU 不能只看一次快照,至少要连续采样几秒,确认它是持续高还是瞬时尖刺。

3. 用 Java demo 复现一个高 CPU 线程

下面这个程序启动两个线程:busy-spin-worker 持续做一个无意义计算,模拟业务死循环或热路径计算;blocking-wait-worker 只是睡眠,模拟存在但不消耗 CPU 的线程。程序会打印进程 ID、Java 线程 ID 和十六进制形式,方便理解"线程定位"这件事。

java 复制代码
public class CpuHotThreadDemo {
    static volatile boolean running = true;

    public static void main(String[] args) throws Exception {
        Thread busy = new Thread(() -> {
            long n = 0;
            while (running) {
                n += System.nanoTime() % 7;
            }
            System.out.println(n);
        }, "busy-spin-worker");

        Thread waiting = new Thread(() -> {
            while (running) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException ignored) {
                    return;
                }
            }
        }, "blocking-wait-worker");

        busy.start();
        waiting.start();
        Thread.sleep(1500);
        System.out.println("processId=" + ProcessHandle.current().pid());
        for (Thread t : Thread.getAllStackTraces().keySet()) {
            if (t.getName().contains("worker")) {
                System.out.printf("threadName=%s javaThreadId=%d hex=%s state=%s%n",
                    t.getName(), t.getId(), Long.toHexString(t.getId()), t.getState());
            }
        }
        running = false;
        busy.join();
        waiting.join();
    }
}

本地编译和运行命令:

bash 复制代码
javac CpuHotThreadDemo.java
java CpuHotThreadDemo

本次实际输出如下:

text 复制代码
processId=31388
threadName=blocking-wait-worker javaThreadId=30 hex=1e state=TIMED_WAITING
threadName=busy-spin-worker javaThreadId=29 hex=1d state=RUNNABLE
71903767

这个输出不是为了证明某台机器 CPU 一定会到 100%,而是证明两类线程的差别:忙循环线程处于可运行状态,会不断消耗时间片;睡眠线程存在于进程中,但大部分时间不消耗 CPU。真实 Linux 机器上用 top -Hp 找到高 CPU TID 后,再转十六进制去 dump 里搜索,就能看到类似 busy-spin-worker 这样的线程名和栈顶方法。


#mermaid-svg-SLvagmhk5S4qHvpy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SLvagmhk5S4qHvpy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SLvagmhk5S4qHvpy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SLvagmhk5S4qHvpy .error-icon{fill:#552222;}#mermaid-svg-SLvagmhk5S4qHvpy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SLvagmhk5S4qHvpy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SLvagmhk5S4qHvpy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SLvagmhk5S4qHvpy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SLvagmhk5S4qHvpy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SLvagmhk5S4qHvpy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SLvagmhk5S4qHvpy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SLvagmhk5S4qHvpy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SLvagmhk5S4qHvpy .marker.cross{stroke:#333333;}#mermaid-svg-SLvagmhk5S4qHvpy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SLvagmhk5S4qHvpy p{margin:0;}#mermaid-svg-SLvagmhk5S4qHvpy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SLvagmhk5S4qHvpy .cluster-label text{fill:#333;}#mermaid-svg-SLvagmhk5S4qHvpy .cluster-label span{color:#333;}#mermaid-svg-SLvagmhk5S4qHvpy .cluster-label span p{background-color:transparent;}#mermaid-svg-SLvagmhk5S4qHvpy .label text,#mermaid-svg-SLvagmhk5S4qHvpy span{fill:#333;color:#333;}#mermaid-svg-SLvagmhk5S4qHvpy .node rect,#mermaid-svg-SLvagmhk5S4qHvpy .node circle,#mermaid-svg-SLvagmhk5S4qHvpy .node ellipse,#mermaid-svg-SLvagmhk5S4qHvpy .node polygon,#mermaid-svg-SLvagmhk5S4qHvpy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SLvagmhk5S4qHvpy .rough-node .label text,#mermaid-svg-SLvagmhk5S4qHvpy .node .label text,#mermaid-svg-SLvagmhk5S4qHvpy .image-shape .label,#mermaid-svg-SLvagmhk5S4qHvpy .icon-shape .label{text-anchor:middle;}#mermaid-svg-SLvagmhk5S4qHvpy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SLvagmhk5S4qHvpy .rough-node .label,#mermaid-svg-SLvagmhk5S4qHvpy .node .label,#mermaid-svg-SLvagmhk5S4qHvpy .image-shape .label,#mermaid-svg-SLvagmhk5S4qHvpy .icon-shape .label{text-align:center;}#mermaid-svg-SLvagmhk5S4qHvpy .node.clickable{cursor:pointer;}#mermaid-svg-SLvagmhk5S4qHvpy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SLvagmhk5S4qHvpy .arrowheadPath{fill:#333333;}#mermaid-svg-SLvagmhk5S4qHvpy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SLvagmhk5S4qHvpy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SLvagmhk5S4qHvpy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SLvagmhk5S4qHvpy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SLvagmhk5S4qHvpy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SLvagmhk5S4qHvpy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SLvagmhk5S4qHvpy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SLvagmhk5S4qHvpy .cluster text{fill:#333;}#mermaid-svg-SLvagmhk5S4qHvpy .cluster span{color:#333;}#mermaid-svg-SLvagmhk5S4qHvpy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SLvagmhk5S4qHvpy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SLvagmhk5S4qHvpy rect.text{fill:none;stroke-width:0;}#mermaid-svg-SLvagmhk5S4qHvpy .icon-shape,#mermaid-svg-SLvagmhk5S4qHvpy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SLvagmhk5S4qHvpy .icon-shape p,#mermaid-svg-SLvagmhk5S4qHvpy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SLvagmhk5S4qHvpy .icon-shape .label rect,#mermaid-svg-SLvagmhk5S4qHvpy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SLvagmhk5S4qHvpy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SLvagmhk5S4qHvpy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SLvagmhk5S4qHvpy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务循环
GC 线程
锁竞争
系统态或 iowait
CPU 告警
确认机器与时间窗口
top 找高 CPU 进程 PID
top -Hp PID 或 pidstat -t 找线程 TID
printf '%x' TID 转十六进制
jstack 或 jcmd Thread.print 搜索 nid
栈顶在做什么
修复循环条件或算法
检查内存分配与 GC 日志
检查同步块与线程池
转向内核、磁盘、网络排查

4. jstack 里重点看什么

拿到线程栈后,先看线程名、nid、线程状态和栈顶几行。RUNNABLE 不一定等于有问题,但如果同一个线程连续几次 dump 都停在同一个业务方法、同一个正则、同一个序列化循环或同一段集合遍历上,就要重点怀疑。对于 CPU 问题,单次 dump 的价值有限,连续抓三次更可靠:如果三次都指向同一段代码,可信度就高很多;如果每次都在不同位置,可能是正常高吞吐计算,也可能需要采样 profiler。

bash 复制代码
for i in 1 2 3; do
  jcmd 进程PID Thread.print > thread-$i.txt
  sleep 5
done

JDK 21 文档中仍保留 jstack 命令说明,jcmdThread.print 也能输出线程栈。实际生产环境里,我更倾向优先用 jcmd,因为它覆盖的诊断命令更多,后续还能继续看 GC、VM flags、类加载等信息。不过很多老环境、老脚本仍然使用 jstack,文章里把两个都保留,是为了让读者能匹配自己的 JDK 版本和权限条件。
代码 jstack/jcmd printf top -H 代码 jstack/jcmd printf top -H #mermaid-svg-jeYBxj7szimcK3zD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jeYBxj7szimcK3zD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jeYBxj7szimcK3zD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jeYBxj7szimcK3zD .error-icon{fill:#552222;}#mermaid-svg-jeYBxj7szimcK3zD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jeYBxj7szimcK3zD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jeYBxj7szimcK3zD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jeYBxj7szimcK3zD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jeYBxj7szimcK3zD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jeYBxj7szimcK3zD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jeYBxj7szimcK3zD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jeYBxj7szimcK3zD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jeYBxj7szimcK3zD .marker.cross{stroke:#333333;}#mermaid-svg-jeYBxj7szimcK3zD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jeYBxj7szimcK3zD p{margin:0;}#mermaid-svg-jeYBxj7szimcK3zD .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jeYBxj7szimcK3zD text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-jeYBxj7szimcK3zD .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jeYBxj7szimcK3zD .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-jeYBxj7szimcK3zD .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-jeYBxj7szimcK3zD .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-jeYBxj7szimcK3zD #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-jeYBxj7szimcK3zD .sequenceNumber{fill:white;}#mermaid-svg-jeYBxj7szimcK3zD #sequencenumber{fill:#333;}#mermaid-svg-jeYBxj7szimcK3zD #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-jeYBxj7szimcK3zD .messageText{fill:#333;stroke:none;}#mermaid-svg-jeYBxj7szimcK3zD .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jeYBxj7szimcK3zD .labelText,#mermaid-svg-jeYBxj7szimcK3zD .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-jeYBxj7szimcK3zD .loopText,#mermaid-svg-jeYBxj7szimcK3zD .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-jeYBxj7szimcK3zD .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jeYBxj7szimcK3zD .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-jeYBxj7szimcK3zD .noteText,#mermaid-svg-jeYBxj7szimcK3zD .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-jeYBxj7szimcK3zD .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jeYBxj7szimcK3zD .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jeYBxj7szimcK3zD .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jeYBxj7szimcK3zD .actorPopupMenu{position:absolute;}#mermaid-svg-jeYBxj7szimcK3zD .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-jeYBxj7szimcK3zD .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jeYBxj7szimcK3zD .actor-man circle,#mermaid-svg-jeYBxj7szimcK3zD line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-jeYBxj7szimcK3zD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 记录十进制线程 TID转成十六进制,例如 12345 ->> 3039在线程 dump 中搜索 nid=0x3039查看线程名、状态和栈顶方法结合发布、日志和监控确认原因

5. 五类常见误判

第一,把 CPU 高等同于死循环。死循环很常见,但不是唯一答案。GC 线程频繁运行也会让 CPU 高,这时业务线程栈不一定明显,反而要看 GC 日志、对象分配速率和堆使用曲线。

第二,把 iowait 高当成 CPU 不够。iowait 更应该联想到磁盘、网络盘、数据库、消息队列或远端调用。盲目扩 CPU 可能没有任何效果。

第三,只看进程,不看线程。一个 Java 进程有几百个线程很正常,真正烧 CPU 的可能只有一个定时任务、一个消费线程或一个异常重试线程。

第四,只抓一次栈就下结论。线程刚好经过某个方法,不代表它一直卡在那里。连续采样和时间窗口比单点截图更有价值。

第五,重启后再排查。重启会抹掉线程状态、临时文件、连接状态和故障上下文。除非业务已经无法承受,否则至少先留一份 toppidstat、线程 dump、GC 日志和应用日志。

6. 什么时候要上 profiler

如果 top -H + jstack 能直接定位到具体方法,比如某个 while 循环、某个正则或某个 JSON 解析,就不需要一上来就火焰图。Profiler 更适合两类情况:第一,CPU 被很多线程平均消耗,单个线程不突出;第二,线程栈变化很快,dump 只能看到碎片。Linux 上可以考虑 perf,Java 服务也可以用 async-profiler 这类采样工具。采样时要注意权限、符号、容器 PID namespace,以及线上开销。

火焰图的价值在于看"时间花在哪里",不是替代基础排查。基础命令先确定进程和线程,profiler 再回答函数层面的比例。如果基础范围没缩小,直接采样整个机器,结果可能混进系统服务、旁路任务和其他容器,阅读成本很高。

7. 生产止血策略

定位期间业务还在跑,止血要和取证同步。对无状态服务,可以先摘掉一台实例保留现场,再让其他实例接流量;如果所有实例一起高 CPU,要优先看最近发布、配置变更、流量入口和外部依赖。限流、降级、关闭非核心任务、暂停异常定时任务、回滚版本,都比盲目扩容更可靠。扩容能买时间,但如果问题是死循环或异常重试,新实例也会很快被打满。

如果定位到具体代码,修复思路通常有几类:给循环加退出条件,给重试加退避和上限,替换灾难性正则,减少大对象序列化,拆分大集合遍历,降低日志同步输出,隔离定时任务线程池。每一次修复都要补监控,不然下次 CPU 高还是从头猜。

8. 排查清单

  • CPU 高发生在哪台机器、哪个时间窗口?
  • top 中用户态、系统态、iowait、steal 哪个更突出?
  • 高 CPU 是单进程还是多进程?
  • top -Hppidstat -t 中是否有单个线程特别高?
  • 线程 TID 是否已经转成十六进制并在 dump 中搜到?
  • 是否连续抓取三次线程栈,结果是否稳定?
  • 最近是否有发布、配置变更、流量突增或定时任务启动?
  • GC 日志、应用日志、慢查询日志是否与 CPU 曲线同一时间异常?
  • 止血动作是否保留至少一台实例的现场?

CPU 排查最重要的不是背多少命令,而是每条命令都回答一个问题:谁在消耗 CPU,消耗发生在线程还是内核,栈顶方法是否稳定,能不能和发布时间、日志、流量入口对上。沿着这条线走,CPU 100% 就不再是一个笼统告警,而是一条可以追到代码行的证据链。

9. 把命令输出翻译成排查判断

很多同学在排查 CPU 时会把命令当成"固定流程"执行:贴一遍 top、贴一遍 jstack,然后仍然不知道怎么下结论。更实用的方式是给每个输出字段配一个判断问题。top 里的 %us 高,问题是业务代码为什么一直拿到时间片;%sy 高,问题是应用是否在频繁调用内核,例如大量小包网络收发、频繁创建线程、疯狂写日志或容器网络栈异常;%wa 高,问题是请求是不是被磁盘、网络盘、数据库或远端接口拖住;load average 高但 CPU 不高,问题可能是大量任务在不可中断 I/O 或队列里等待。

pidstat -t 的价值是把进程拆成线程。如果一个线程长期接近 100%,它通常是单线程热点;如果十几个线程都在 20% 到 40% 之间,要看它们是不是同一类线程池;如果 GC 线程持续靠前,就要结合 GC 日志看对象分配和堆压力。线程维度出来后,jstack 才有明确目标。否则一个几百线程的 dump 翻起来很痛苦,也容易被一些看起来吓人的 WAITING 线程带偏。

还有一个小技巧:记录证据时把三类信息放在同一个时间点附近。比如 14:03:10 抓 top -Hp,14:03:12 抓 jcmd Thread.print,14:03:15 保存应用日志片段。后续复盘时,你能把线程、日志和接口流量连起来,而不是拿着不同时间点的材料硬拼故事。

10. Java 服务里最常见的几个代码原因

第一类是循环条件错误。比如消费队列时没有正确处理空队列,异常后立即重试,没有退避;或者分页查询忘记推进游标,导致同一页数据反复处理。这类问题在线程栈里通常表现为同一个业务方法反复出现,日志里也可能伴随大量重复报错。

第二类是算法复杂度被数据量放大。小数据量测试没问题,上线后某个接口对几万条记录做嵌套循环、全量排序或重复正则匹配,CPU 会被正常代码打满。它不是传统意义上的死循环,但结果和死循环一样危险。遇到这种情况,不要只盯异常日志,因为代码可能没有抛异常;更应该看慢接口、入参规模、集合长度和循环次数。

第三类是日志风暴。同步日志、异常堆栈重复打印、请求体完整输出,都可能让 CPU 和 I/O 同时升高。日志风暴经常和异常重试一起出现:一次外部接口失败触发重试,重试又打印完整异常,异常日志再拖慢服务,最终形成放大器。

第四类是锁竞争和线程池配置不合理。严格说,很多锁等待线程不直接烧 CPU,但自旋锁、CAS 重试、过度竞争的并发容器、过小或过大的线程池,都会让上下文切换和用户态计算变多。线程 dump 中如果大量线程停在同一把锁附近,要结合线程状态和上下文切换指标一起看。

第五类是 GC 压力。频繁创建短命对象、大 JSON 转换、大集合复制、缓存击穿后的大量对象构造,都可能让 GC 线程频繁运行。CPU 高的时候,如果业务线程看不出单点热点,GC 日志却显示 Young GC 过密或 Full GC 频繁,就要从内存分配链路入手。

11. 容器和云服务器里的额外坑

现在很多 Java 服务跑在容器里,CPU 排查会多一层视角。宿主机看到的是进程,容器里看到的是 PID namespace;容器被限制了 CPU quota 时,应用内部可能觉得线程不多,实际已经触达配额。Kubernetes 里还要看 request、limit、throttling 指标。如果 CPU throttling 很高,表现可能是接口变慢,但进程本身不一定显示传统意义上的 100%。

云服务器上还要关注 steal 时间。steal 高意味着虚拟 CPU 想运行,但宿主机没有把真实 CPU 时间分给它。此时优化业务代码当然有意义,但故障根因可能在实例规格、宿主机争用或云厂商调度。这个场景下,换机、升规格或迁移节点可能比改代码更直接。

另一个容易忽略的是 sidecar、agent 和日志采集器。线上整机 CPU 高,不一定是业务进程高。监控 agent、日志采集、服务网格代理、病毒扫描或备份任务,都可能在特定时间窗口抢 CPU。排查时先从整机进程列表看起,就是为了避免一上来把锅扣给业务代码。

相关推荐
深念Y1 小时前
开机启动优化记录
linux·开机
!chen1 小时前
客户环境 Nginx 配置流式报表与超时排查
运维·nginx
2601_966799041 小时前
酷嗨米J300:硬件级多通道分发采集设备,为矩阵直播打造独立信号通道
服务器·网络·负载均衡
落樱弥城1 小时前
RHI 设计考量:不同 API 差异分析
服务器·前端·性能优化
Titan20242 小时前
网络基础:传输层协议知识梳理
linux·服务器·网络
一叶龙洲2 小时前
Ubuntu从图标到卸载应用
linux·windows·ubuntu
KIZIFLOW北泽五金2 小时前
液压系统压力损失怎么降?成因分析与减损实操方案
运维·自动化·新媒体运营·产品运营·流量运营·用户运营·内容运营
介拙2 小时前
Zabbix 7.0 从装到告警触发,我踩了这些坑
运维
爱吃提升2 小时前
VMware虚拟机迁移系统磁盘完整教程(VMware Workstation17)
linux·运维·服务器