Skip to content

CPU 100% 定位 ​

从"哪台机器"到"哪个线程"到"哪行代码",四步。Java 和 C/C++/Go 的第四步不同,前三步一样。

一、确认是真高还是假高 ​

bash
uptime
top -b -n 1 | head -8
text
 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 st

us 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 -10
text
    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 mysqld

398.2% 表示用了约 4 个核。记下 PID。

三、找到线程 ​

bash
top -Hp 28441 -b -n 1 | head -20
text
    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' 28458
text
6f2a

四、第四步:分语言 ​

Java ​

bash
jstack -l 28441 > /tmp/jstack.txt
grep -A 30 'nid=0x6f2a' /tmp/jstack.txt
text
"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 28441

C / C++ / Go / Rust ​

bash
perf top -p 28441
text
  42.11%  [.] std::__detail::_Hash_bytes
  18.42%  [.] MyService::lookup
   9.02%  [.] malloc

perf 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 10
text
  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.44

O 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 deniedperf_event_paranoidsysctl -w kernel.perf_event_paranoid=1
CPU 高但没热点采样太短 / 被限流延长采样,看 cpu.stat 的 throttled
一 jstack 就恢复典型的"观测改变状态"多采几次,或上 async-profiler
st 高云主机被抢占CPU 使用率是"想用但没给",不是代码问题