SysOps

Linux CPU at 100%: the Shortest Path from top to the Thread Stack

"Restart fixes everything" is the most expensive option when CPU is pegged. This guide gives a reusable path: classify the load, pin the thread, capture what it is executing, then act — with dedicated tools for Java, Python and Go.

By 巧匠 Team·8 min read·Updated 2026-08-30

Step 1: user, system, or iowait

Read the CPU line at the top of `top` first: high us means application code is burning CPU (business logic, hot loops, GC); high sy means kernel overhead (syscall storms, lock contention, context switching); high wa is not CPU business at all — slow disk IO inflates load. The directions are totally different, and the wrong tool wastes an afternoon.

Compare load average against core count: `nproc` prints cores; only load persistently above that means real queuing.

top        # 看 %Cpu(s) 行的 us / sy / wa
nproc      # 核数,用于对照 load average

Step 2: pin the process, then drill to threads

After `top` (press P to sort by CPU) identifies the hot process, most people stop there. But the hotspot is often just a few threads inside one process — `top -H -p <pid>` shows a per-thread view; note the thread IDs burning CPU.

An even handier form is `pidstat -t -p <pid> 1`, which prints per-thread CPU usage once per second without an interactive terminal.

top -H -p <pid>          # 该进程的线程级视图
pidstat -t -p <pid> 1    # 逐秒采样各线程 CPU

Step 3: capture what that thread is executing

Java: convert the thread ID to hexadecimal and search `jstack <pid>` output for nid=0x<hex>; the stack frames appear directly. Capture two or three times — frames stuck at the top are the hotspot. Python: `py-spy dump --pid` gives the call stack without code changes or restarts. Go: grab a CPU profile if pprof is wired in. Universal fallback: `perf top -p <pid>` for kernel-side hot symbols.

printf '%x\n' <tid>          # 线程号转十六进制
jstack <pid> | grep -A 20 nid=0x<hex>
py-spy dump --pid <pid>     # Python 进程调用栈
perf top -p <pid>           # 通用内核态热点

Match against the usual root causes

① Infinite loops or a while whose exit condition never holds; ② catastrophic regex backtracking (nested quantifiers over long text); ③ frequent Young/Full GC (memory starvation masquerading as a CPU problem); ④ crypto, compression or serialization doing heavy work on the hot path; ⑤ synchronous log flushing at high QPS. Whatever the stack shows usually maps to one of these.

jstat -gcutil <pid> 1000   # 每秒看 GC 频率,判断是否 GC 风暴

Stop the bleeding first, cure later

Bleeding-stop actions for a live incident: drain traffic (remove the node from the load balancer), throttle, or kill the runaway process so the supervisor restarts it. Before killing, capture all evidence — thread stacks, GC logs — otherwise the clues vanish with the restart and the issue becomes "spooky and random".

The cure goes back to code: fix loop conditions, rewrite regexes, add caching, move heavy work off the hot path, size the heap or fix the leak. If the root cause is traffic growth, it is a capacity problem — scaling out is more honest than patching code.

jstack <pid> > /tmp/stack-$(date +%s).txt   # 先留证据
kill <pid>                                   # 再重启(supervisor 会拉起)

One-page recap: the chain

top to classify us/sy/wa → top -H / pidstat -t to pin the thread → jstack / py-spy / perf to capture the stack → match the five root causes → preserve evidence then stop the bleeding → cure in code. Drill this chain and average CPU triage drops under ten minutes.

top → top -H -p <pid> → jstack/py-spy → 定位 → 止血 → 根治