外观
CPU 100% 定位
从"哪台机器"到"哪个线程"到"哪行代码",四步。Java 和 C/C++/Go 的第四步不同,前三步一样。
一、确认是真高还是假高
bash
uptime
top -b -n 1 | head -8text
23:01:11 up 1 day, 4:41, load average: 9.12, 8.88, 8.20
%Cpu(s): 92.4 us, 5.1 sy, 0.0 ni, 1.8 id, 0.6 wa, 0.0 hi, 0.2 si, 0.0 stus 92.4% + id 1.8% = 真·CPU 打满。如果 us 不高而 wa 高,那是 IO 问题,见 性能四件套。
容器里的 top 不准
容器内 top 默认看到的是宿主机的 CPU 使用率。容器有 CPU limit 时,top 显示 400% 也可能只是被限流。
bash
docker stats --no-stream
cat /sys/fs/cgroup/cpu.stat # nr_throttled / throttled_usec二、找到进程
bash
top -b -n 1 -o %CPU | head -15
ps -eo pid,ppid,user,stat,%cpu,%mem,etime,cmd --sort=-%cpu | head -10text
PID USER PR NI VIRT RES %CPU %MEM TIME+ COMMAND
28441 app 20 0 4214320 1.9g 398.2 12.4 412:18 java
29112 mysql 20 0 2143212 1.2g 82.1 7.4 121:02 mysqld398.2% 表示用了约 4 个核。记下 PID。
三、找到线程
bash
top -Hp 28441 -b -n 1 | head -20text
PID USER PR NI VIRT RES %CPU %MEM TIME+ COMMAND
28458 app 20 0 4214320 1.9g 98.4 12.4 102:11 java
28461 app 20 0 4214320 1.9g 97.2 12.4 98:42 java
28472 app 20 0 4214320 1.9g 12.1 12.4 2:14 java三个线程各占约 1 核 → 是多线程都在忙,不是单个死循环。
-H 显示线程(每个线程在 Linux 里也是 task)。再拿线程 ID 转十六进制:
bash
printf '%x\n' 28458text
6f2a四、第四步:分语言
Java
bash
jstack -l 28441 > /tmp/jstack.txt
grep -A 30 'nid=0x6f2a' /tmp/jstack.txttext
"http-nio-8080-exec-12" #42 daemon prio=5 os_prio=0 cpu=10211.42ms elapsed=4182.11s tid=0x00007f2c1e9a2000 nid=0x6f2a runnable
java.lang.Thread.State: RUNNABLE
at java.util.regex.Pattern$Loop.match(Pattern.java:4785)
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658)
at java.util.regex.Pattern$Branch.match(Pattern.java:4604)
at java.util.regex.Matcher.match(Matcher.java:1446)
at com.example.validator.OrderValidator.validate(OrderValidator.java:88)nid=0x6f2a 与线程 ID 对上,栈顶是 Pattern 的递归匹配 —— 正则回溯爆炸。
多个线程都热时,反复采样几次(间隔 5 秒),看哪一帧反复出现:
bash
for i in $(seq 1 5); do
jstack -l 28441 | grep -A 8 'nid=0x6f2a' | head -6
echo '---'
sleep 5
done工具替代:
bash
# Arthas(见 03 章)
thread -n 3
# async-profiler,火焰图最直观
./profiler.sh -d 30 -f /tmp/cpu.html 28441C / C++ / Go / Rust
bash
perf top -p 28441text
42.11% [.] std::__detail::_Hash_bytes
18.42% [.] MyService::lookup
9.02% [.] mallocperf top 直接给出函数级热度,比看线程更细。
bash
perf record -F 99 -p 28441 -g -- sleep 30
perf report-g 采调用栈,perf report 里能一路展开到调用方。
Go 程序更简单:
bash
curl http://127.0.0.1:6060/debug/pprof/profile?seconds=30 -o cpu.pprof
go tool pprof -http=:8081 cpu.pprof没有调试符号
bash
gdb -p 28441 -batch -ex 'thread apply all bt' 2>/dev/null | head -60生产上可能需要先 yum install gdb,且会短暂暂停进程。
五、常见根因
| 现象 | 根因 | 下一步 |
|---|---|---|
| 几个线程均匀吃满 | 正常计算密集 / 线程数过多 | 看业务量是否突增 |
| 单线程 100% | 死循环、正则回溯、死锁自旋 | 看栈顶 |
| 周期性飙高 | GC(Java)、定时任务、批量跑批 | 看 GC 日志 / crontab |
sy 特别高 | 系统调用多(网络、锁) | strace -c、perf top -g |
| 线程数暴涨 | 线程池被打满、每请求起线程 | jstack 数线程 |
| CPU 不高但很慢 | 不是 CPU 问题 | 看 wa、内存、网络 |
正则回溯
java
// 危险:嵌套量词
Pattern.compile("(a+)+b").matcher("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").matches();输入里一旦没有 b,回溯次数指数级增长。改成非回溯写法((?>...) 原子组或换算法)。
死锁自旋
bash
jstack -l 28441 | grep -A 20 'Found one Java-level deadlock'自旋锁(Thread.onSpinWait、AtomicLong 高竞争)也会表现为 CPU 高但吞吐不变。
GC 引起的 CPU 高
bash
jstat -gcutil 28441 1000 10text
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 62.10 99.20 98.42 94.10 91.20 18244 412.33 128 642.11 1054.44O 98%、FGC 128 次、FGCT 642 秒 → 堆太小或内存泄漏,一直在 Full GC。
六、一次完整案例
bash
# 1. 谁
top -b -n 1 -o %CPU | head -8
# -> java 398%
# 2. 哪个线程
top -Hp 28441 -b -n 1 -o %CPU | head -5
# -> 28458, 28461 各 98%
# 3. 转十六进制
printf '%x\n' 28458
# -> 6f2a
# 4. 栈
jstack 28441 | grep -A 15 'nid=0x6f2a'
# -> Pattern$Loop.match ...
# 5. 定位代码
# OrderValidator.java:88 的 pattern.matcher(input).matches()从发现到定位到行号,通常三分钟内。
七、踩过的坑
| 现象 | 根因 | 处置 |
|---|---|---|
top -Hp 的 PID 和 jstack 的 nid 对不上 | 没转十六进制 | printf '%x\n' PID |
| jstack 输出里找不到 nid | 线程已结束 | 多采几次 |
| 容器内 top 显示宿主机 CPU | 容器共享 /proc | 用 docker stats 或 cgroup 文件 |
perf 报 Permission denied | perf_event_paranoid | sysctl -w kernel.perf_event_paranoid=1 |
| CPU 高但没热点 | 采样太短 / 被限流 | 延长采样,看 cpu.stat 的 throttled |
| 一 jstack 就恢复 | 典型的"观测改变状态" | 多采几次,或上 async-profiler |
st 高 | 云主机被抢占 | CPU 使用率是"想用但没给",不是代码问题 |