ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

【Linux】CPU 100% 怎么排查?——top、pidstat、jstack 到线程定位实战 【Linux】CPU 100% 怎么排查——top、pidstat、jstack 到线程定位实战线上机器 CPU 突然打满时最忌讳的动作是直接重启。重启当然可能让告警消失但现场也一起没了到底是某个业务循环、GC 线程、锁竞争、日志风暴还是宿主机 steal 时间异常事后只能靠猜。更稳的处理方式是先把故障缩小到“哪台机器、哪个进程、哪个线程、哪段代码”再决定限流、摘实例、回滚或修代码。本文用一个 Java 小程序复现“单个线程持续消耗 CPU”的现场把top、pidstat、jstack、jcmd和/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搜索nid0x...。这里最容易卡住的是线程 ID 对不上Linux 工具显示的通常是十进制 TID而 Java 线程 dump 里的nid常写成十六进制所以中间必须转换。toptop-Hp进程PID pidstat-t-p进程PID1printf%x\n线程TID jcmd 进程PID Thread.print|grep-A30nid0x十六进制TID/proc/pid/stat也能解释为什么这些工具能算 CPU。man-pages 里说明了进程的utime和stime前者是进程在用户态被调度运行的时间后者是内核态运行时间单位是 clock ticks。工具通过两次采样之间这些计数的差值结合时间间隔和 CPU 核数得到你看到的 CPU 百分比。所以排查 CPU 不能只看一次快照至少要连续采样几秒确认它是持续高还是瞬时尖刺。3. 用 Java demo 复现一个高 CPU 线程下面这个程序启动两个线程busy-spin-worker持续做一个无意义计算模拟业务死循环或热路径计算blocking-wait-worker只是睡眠模拟存在但不消耗 CPU 的线程。程序会打印进程 ID、Java 线程 ID 和十六进制形式方便理解“线程定位”这件事。publicclassCpuHotThreadDemo{staticvolatilebooleanrunningtrue;publicstaticvoidmain(String[]args)throwsException{ThreadbusynewThread(()-{longn0;while(running){nSystem.nanoTime()%7;}System.out.println(n);},busy-spin-worker);ThreadwaitingnewThread(()-{while(running){try{Thread.sleep(200);}catch(InterruptedExceptionignored){return;}}},blocking-wait-worker);busy.start();waiting.start();Thread.sleep(1500);System.out.println(processIdProcessHandle.current().pid());for(Threadt: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());}}runningfalse;busy.join();waiting.join();}}本地编译和运行命令javac CpuHotThreadDemo.javajavaCpuHotThreadDemo本次实际输出如下processId31388 threadNameblocking-wait-worker javaThreadId30 hex1e stateTIMED_WAITING threadNamebusy-spin-worker javaThreadId29 hex1d stateRUNNABLE 71903767这个输出不是为了证明某台机器 CPU 一定会到 100%而是证明两类线程的差别忙循环线程处于可运行状态会不断消耗时间片睡眠线程存在于进程中但大部分时间不消耗 CPU。真实 Linux 机器上用top -Hp找到高 CPU TID 后再转十六进制去 dump 里搜索就能看到类似busy-spin-worker这样的线程名和栈顶方法。业务循环GC 线程锁竞争系统态或 iowaitCPU 告警确认机器与时间窗口top 找高 CPU 进程 PIDtop -Hp PID 或 pidstat -t 找线程 TIDprintf %x TID 转十六进制jstack 或 jcmd Thread.print 搜索 nid栈顶在做什么修复循环条件或算法检查内存分配与 GC 日志检查同步块与线程池转向内核、磁盘、网络排查4. jstack 里重点看什么拿到线程栈后先看线程名、nid、线程状态和栈顶几行。RUNNABLE不一定等于有问题但如果同一个线程连续几次 dump 都停在同一个业务方法、同一个正则、同一个序列化循环或同一段集合遍历上就要重点怀疑。对于 CPU 问题单次 dump 的价值有限连续抓三次更可靠如果三次都指向同一段代码可信度就高很多如果每次都在不同位置可能是正常高吞吐计算也可能需要采样 profiler。foriin123;dojcmd 进程PID Thread.printthread-$i.txtsleep5doneJDK 21 文档中仍保留jstack命令说明jcmd的Thread.print也能输出线程栈。实际生产环境里我更倾向优先用jcmd因为它覆盖的诊断命令更多后续还能继续看 GC、VM flags、类加载等信息。不过很多老环境、老脚本仍然使用jstack文章里把两个都保留是为了让读者能匹配自己的 JDK 版本和权限条件。代码jstack/jcmdprintftop -H代码jstack/jcmdprintftop -H记录十进制线程 TID转成十六进制例如 12345 - 3039在线程 dump 中搜索 nid0x3039查看线程名、状态和栈顶方法结合发布、日志和监控确认原因5. 五类常见误判第一把 CPU 高等同于死循环。死循环很常见但不是唯一答案。GC 线程频繁运行也会让 CPU 高这时业务线程栈不一定明显反而要看 GC 日志、对象分配速率和堆使用曲线。第二把 iowait 高当成 CPU 不够。iowait 更应该联想到磁盘、网络盘、数据库、消息队列或远端调用。盲目扩 CPU 可能没有任何效果。第三只看进程不看线程。一个 Java 进程有几百个线程很正常真正烧 CPU 的可能只有一个定时任务、一个消费线程或一个异常重试线程。第四只抓一次栈就下结论。线程刚好经过某个方法不代表它一直卡在那里。连续采样和时间窗口比单点截图更有价值。第五重启后再排查。重启会抹掉线程状态、临时文件、连接状态和故障上下文。除非业务已经无法承受否则至少先留一份top、pidstat、线程 dump、GC 日志和应用日志。6. 什么时候要上 profiler如果top -H jstack能直接定位到具体方法比如某个 while 循环、某个正则或某个 JSON 解析就不需要一上来就火焰图。Profiler 更适合两类情况第一CPU 被很多线程平均消耗单个线程不突出第二线程栈变化很快dump 只能看到碎片。Linux 上可以考虑perfJava 服务也可以用 async-profiler 这类采样工具。采样时要注意权限、符号、容器 PID namespace以及线上开销。火焰图的价值在于看“时间花在哪里”不是替代基础排查。基础命令先确定进程和线程profiler 再回答函数层面的比例。如果基础范围没缩小直接采样整个机器结果可能混进系统服务、旁路任务和其他容器阅读成本很高。7. 生产止血策略定位期间业务还在跑止血要和取证同步。对无状态服务可以先摘掉一台实例保留现场再让其他实例接流量如果所有实例一起高 CPU要优先看最近发布、配置变更、流量入口和外部依赖。限流、降级、关闭非核心任务、暂停异常定时任务、回滚版本都比盲目扩容更可靠。扩容能买时间但如果问题是死循环或异常重试新实例也会很快被打满。如果定位到具体代码修复思路通常有几类给循环加退出条件给重试加退避和上限替换灾难性正则减少大对象序列化拆分大集合遍历降低日志同步输出隔离定时任务线程池。每一次修复都要补监控不然下次 CPU 高还是从头猜。8. 排查清单CPU 高发生在哪台机器、哪个时间窗口top中用户态、系统态、iowait、steal 哪个更突出高 CPU 是单进程还是多进程top -Hp或pidstat -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 -Hp14:03:12 抓jcmd Thread.print14: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。排查时先从整机进程列表看起就是为了避免一上来把锅扣给业务代码。
返回列表