A slow server does not hide its bottleneck: the Linux kernel accounts for every nanosecond of CPU time, every major page fault, every block-layer request and every dropped packet. The difficulty is turning that firehose into a decision in under a minute. This is a drill-down procedure — one-second system snapshot, then per-process attribution, then device-level and memory-level confirmation — using only tools that ship with a normal distribution or one apt install sysstat. All of it reads from /proc and /sys, so it works identically on any Linux instance, whether that is a properly sized VPS with dedicated CPU shares or a cramped container.
Step 1: Establish the Baseline Before Anything Is Wrong
Tuning and diagnosis both fail without a baseline. Capture one while the box is healthy, so a later reading has something to be compared against:
<code>mkdir -p /var/log/baseline
vmstat 1 60 > /var/log/baseline/vmstat.txt
cat /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io \
> /var/log/baseline/psi.txt
sar -u -r -d 1 5 > /var/log/baseline/sar.txt</code>
Two numbers matter most in that baseline: r (run queue) and the PSI avg10 values. A healthy idle box shows r at or below the vCPU count and PSI avg10 under 1.00 for all three resources.
Step 2: The One-Second Snapshot
vmstat 1 remains the densest single command on Linux. Read the columns in a fixed order rather than scanning them:
<code>vmstat 1 5
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st</code>
| Column | Threshold that means “act” | What it implies |
|---|---|---|
r |
sustained > vCPUs | CPU oversubscription inside the guest |
si/so |
any sustained non-zero | Active swapping; memory problem, not disk |
bi/bo |
bo > 50 MB/s with wa high |
Write path saturated; check dirty ratio and device |
in |
> 50k/s on a 4-vCPU box | Interrupt storm — often network at high pps |
cs |
> 100k/s idle-ish | Thread pool too large, or lock contention |
st |
> 2% sustained | Hypervisor steal — guest tuning cannot fix it |
The st row deserves emphasis. Steal time means your vCPU was runnable but the hypervisor scheduled someone else. If the baseline was 0.2% and the current reading is 6%, the instance is being oversubscribed on the host, and every sysctl in the following sections is theatre. Confirm with mpstat -P ALL 1 to see whether steal is spread across all vCPUs or concentrated on one — concentrated steal usually indicates a co-scheduled noisy neighbour or a pinned host thread.
Step 3: Attribute CPU to a Process
top averages over its refresh interval and hides short spikes. pidstat reports true deltas between samples and separates the accounting:
<code>sudo apt install sysstat
pidstat -u -p ALL 1 # user/system CPU per process
pidstat -r -p ALL 1 # page faults, RSS growth
pidstat -d -p ALL 1 # disk read/write per process
pidstat -w -p ALL 1 # voluntary/involuntary context switches</code>
Read %usr and %system separately. A process at 100% %system with a small %usr is spending its time in syscalls — typically small-buffer read/write loops, excessive futex traffic, or a tracing agent. That is tunable or starvable; a process at 100% %usr needs a faster core or fewer threads.
pidstat -w is the underrated one. Involuntary context switches (nvcswch/s) climbing on a process whose %CPU is below 100 means it is being preempted — either the run queue is full or its CPU affinity is competing with another hot thread. Voluntary switches (cswch/s) in the tens of thousands per second usually mean lock contention, not I/O, especially in JVM and Go workloads.
Step 4: Localise Disk Pressure Properly
High wa in vmstat points at storage; iostat -x 1 tells you which layer. The trap is that %util is nearly meaningless on NVMe, where a device can service hundreds of parallel requests and still show high utilisation at trivial latency.
<code>iostat -x 1
# Device r/s w/s rkB/s wkB/s r_await w_await aqu-sz %util</code>
r_await/w_awaitare the numbers that matter. On local NVMe, sub-millisecond is normal; on network-backed block storage, 0.5–2 ms is expected and fine.aqu-szrising while latency stays flat means the queue is absorbing bursts — healthy. Risingaqu-szwith rising latency means the device is the wall.- Compare guest
w/sagainst the provider’s throttle. Many plans cap IOPS; when the cap is hit, latency jumps in a step, not a slope. A step change at a suspiciously round number is the signature.
Cross-check with PSI, which quantifies the time someone was actually stalled rather than how busy the device looked:
<code>cat /proc/pressure/io
# some avg10=12.43 avg60=8.11 avg300=3.02 total=9183421
# full avg10=9.87 avg60=6.44 avg300=2.11 total=7211233</code>
The full line means every runnable task was stalled on I/O. Sustained full avg10 above 5 on a database host is a capacity problem, not a tuning problem.
Step 5: Memory — Beyond “Free RAM”
free -m is the most misread command in the toolbox: low free with a large buff/cache is correct kernel behaviour. What matters is reclaim pressure.
<code>grep -E 'pgscan|pgsteal|allocstall|oom' /proc/vmstat
cat /proc/pressure/memory
grep -E 'AnonHugePages|Shmem|FilePmdMapped' /proc/meminfo
slabtop -o -s c | head -15</code>
Rising pgscan_direct and allocstall mean allocations are blocking on reclaim — the working set no longer fits. A large slabtop dentry or inode cache after a file-heavy workload is normal; a runaway kmalloc-* slab is a kernel leak, usually from a filesystem or network driver, and shows up as steady growth that survives a workload restart.
Then confirm with per-process RSS versus PSS, because RSS double-counts shared pages:
<code>sudo smem -t -k -c "pname uss pss rss" | sort -k3 -h | tail -10</code>
Step 6: The 60-Second Decision
vmstat 1 1— ifstis high, stop; the host is the problem.- If
si/soare non-zero, go to memory and fix the working set or add RAM. - If
r> vCPUs with lowwa, go topidstat -uand find the hog or the thread explosion. - If
wais high, go toiostat -x, compare latency against the baseline, and check PSI iofull. - If all three are clean but the application is slow, the bottleneck has moved outside the kernel: DNS, TLS handshakes, an upstream dependency, or the client’s network.
Instrument the result. A five-line cron job that appends the five decisive numbers every minute gives you the history needed to correlate incidents:
<code>*/1 * * * * /usr/bin/vmstat 1 1 | tail -1 >> /var/log/baseline/minute.log</code>
Rotate it with logrotate at a 7-day retention — roughly 3 MB at that rate — and you have a forensic trail that answers “was it us or the host” without a monitoring stack. If the readings consistently show the instance is the smaller half of the equation, sizing is the fix rather than more tuning: see the provider plan dimensions and CPU allocation model before spending another evening chasing sysctl values. Measurement first, provisioning second, tuning third — in that order, the numbers always win.



Leave a Reply
You must be logged in to post a comment.