wiki / linux
reading a slow system
A triage order for a Linux box that feels slow, from the first sixty seconds down to perf and bpftrace.
updated · 5 min read
“Slow” means some resource is saturated or something is waiting. Start wide, find the resource, then narrow down to the process and the code path. The tools get sharper and more expensive as you go down.
the first minute
The opening list comes from Brendan Gregg and the Netflix performance team, in Linux Performance Analysis in 60,000 Milliseconds. Run them in order:
uptime
dmesg | tail
vmstat 1
mpstat -P ALL 1
pidstat 1
iostat -xz 1
free -m
sar -n DEV 1
sar -n TCP,ETCP 1
top
What each one is for:
uptimegives 1, 5 and 15 minute load averages. On Linux, load counts tasks waiting for disk I/O (state D) as well as runnable tasks (state R), so a high load doesn’t prove the CPUs are busy. A 1-minute value well above the 15-minute value means load is rising.dmesg | tailcatches the loud failures: OOM kills, dropped packets, disk errors.vmstat 1:ris runnable tasks; if it stays above the CPU count, the CPUs are saturated.bis tasks blocked on I/O. Non-zerosi/someans the box is swapping. Thenus,sy,id,wa(waiting on I/O) andst(time stolen by the hypervisor). A steadywapoints at the disk.mpstat -P ALL 1shows each CPU. One core pinned while the rest idle is a single-threaded bottleneck.pidstat 1is a rolling per-process view.-dadds I/O,-rpage faults and memory,-wcontext switches,-tthreads.iostat -xz 1, where-zhides idle devices.r_awaitandw_awaitare milliseconds per request, queue time included, which is the latency the application feels.aqu-szis the average queue length (older sysstat calls itavgqu-sz).%utilnear 100% means saturation only for devices that serve requests one at a time. SSDs and RAID arrays work in parallel, so trustawaitover%utilthere.free -m: read theavailablecolumn. It estimates memory a new program can get without swapping, page cache included.sar -n DEV 1shows interface throughput, which you compare against link speed.sar -n TCP,ETCP 1shows connection rates and retransmits.topto confirm what the others suggested.
pressure stall information
If /proc/pressure/ exists, the kernel tracks how much time tasks spend stalled on CPU, memory and I/O. It answers the question the tools above circle around: is anything waiting, and how much?
cat /proc/pressure/memory
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0
some is the share of time at least one task was stalled on that resource. full is the share of time every non-idle task was stalled at once, which is when throughput is really lost. avg10, avg60 and avg300 are percentages over 10 s, 60 s and 5 min windows, and total is the absolute stall time. CPU full has no meaning at the system level and reads zero. With cgroup v2, each cgroup has its own cpu.pressure, memory.pressure and io.pressure, which tells you which service is the one waiting.
one process: strace
Once you have a suspect PID, see what it asks the kernel for.
strace -c -p 1234 # Ctrl-C after a few seconds
strace -f -T -e trace=openat,read,write -p 1234
-c prints calls, errors and time per syscall. That time is CPU time in the kernel, not wall-clock time, so a process blocked in futex or poll can look cheap here; add -w to summarize wall-clock time instead. -T prints the time spent inside each call, -f follows child processes and threads, and -e trace= narrows the set.
strace stops the target on every syscall it traces, so it can slow it down a lot. Keep runs short on a box that serves traffic.
CPU: perf
When a process burns CPU, sample its stacks.
perf top -g # live view with call graphs
perf record -F 99 -a -g -- sleep 30 # sample every CPU at 99 Hz for 30 s
perf report --stdio
99 Hz rather than 100 avoids sampling in lockstep with work that runs on a 100 Hz timer. -a records every CPU; use -p PID for one process. Call graphs default to frame pointers, and code built without them gives broken stacks. Switch the unwinder:
perf record -F 99 --call-graph dwarf -p 1234 -- sleep 30
Function names showing up as hex addresses mean symbols are missing; install the debug package for that binary. For a picture instead of a text report, Gregg’s FlameGraph scripts turn perf script output into an SVG:
perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > cpu.svg
bpftrace one-liners
bpftrace attaches small programs to kernel and user events with far less overhead than strace. These come from the bpftrace one-liner tutorial and need root.
# syscalls per process
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
# files being opened, and by whom
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args.filename)); }'
# vfs_read latency per process, as a histogram in nanoseconds
bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; } kretprobe:vfs_read /@start[tid]/ { @ns[comm] = hist(nsecs - @start[tid]); delete(@start, tid); }'
# block I/O request sizes
bpftrace -e 'tracepoint:block:block_rq_issue { @ = hist(args.bytes); }'
# on-CPU kernel stacks at 99 Hz
bpftrace -e 'profile:hz:99 { @[kstack] = count(); }'
Older releases spell tracepoint arguments args->filename. If one of these fails to parse, check the version and the syntax first.
when nothing looks busy
CPUs idle, disks quiet, and the app is still slow: the time is going into waiting. Lock contention, a DNS lookup, a remote call, a sleep in a retry loop. Look at where tasks leave the CPU:
bpftrace -e 'tracepoint:sched:sched_switch { @[kstack] = count(); }'
Then go back to strace -T on the process and find the call that blocks.
sources
- Linux Performance Analysis in 60,000 Milliseconds, Netflix Technology Blog
- PSI: Pressure Stall Information, Linux kernel docs
- bpftrace one-liner tutorial
- FlameGraph, Brendan Gregg
- vmstat(8), iostat(1), pidstat(1), free(1), strace(1), perf-record(1)