Linux performance monitoring: load, vmstat, iostat and sar

Linux performance monitoring is how you answer “why is the server slow?” with numbers instead of guesses. A handful of tools that ship with every distribution, uptime, vmstat, iostat, mpstat, pidstat, sar and ss, tell you within five minutes whether the box is short of CPU, waiting on its disk, starved by the hypervisor or perfectly fine.

Lesson 22 taught you to find the processes eating memory and CPU. Here you look at the whole server: what load average counts, how to compare it with the CPU count, what each slice of CPU time means (us, sy, wa, st), how to read disk latency in iostat, and how to turn on sar so you can see what happened at 3 a.m. You’ll create real load with stress-ng on rocky01, watch every tool react, and save a baseline you can compare against on a bad day.

Where this lesson fits

Lesson 53 of 82 in Linux Administration · Phase 3: Enterprise infrastructure and storage · Before this: Lesson 52, major OS upgrades · Next: Lesson 54, join Linux to Active Directory

TimeYou needBy the end you can
About 75 minutesrocky01 (2 vCPU, 2 GB RAM) with an SSH session as student; ubu01 works the same, differences are notedRead load against CPU count, tell a CPU-bound server from a disk-bound one, find the process behind each, and pull last night’s numbers from sar

What it is, in one line

Performance monitoring means measuring how busy each resource is (CPU, memory, disk, network), how long work waits for it, and keeping a history so you can compare today with a normal day.

In Simple Words

The railway booking office

Picture a small railway booking office with two counters. At 10 a.m. there are two people being served and three waiting in the queue. A few more are standing aside with their forms half filled, waiting for the clerk’s printer to finish a long job before anyone can move.

If the station master only counts heads, he sees seven people and says “we need more counters”. But the two clerks may be sitting idle, staring at the slow printer, and a third counter would change nothing. You have to look at what the clerks are doing, not only at the crowd.

Load average is the head count, the counters are CPUs, and the printer is the disk.

Engineer's View

What the kernel counts and which tool shows it

Load average, from /proc/loadavg, is the average number of tasks that are either running or ready to run (state R) plus tasks in uninterruptible sleep (state D, nearly always waiting for disk or NFS). The three numbers are exponentially weighted averages over 1, 5 and 15 minutes. Because D-state tasks count, a server with a stuck disk can show a load of 20 with its CPUs almost idle.

CPU time is split into slices, and every tool (top, vmstat, mpstat, sar) shows the same ones under slightly different names:

Slicetop / vmstatWhat it meansWhen it is high
userusRunning program codeYour app is busy; find it with pidstat or top
systemsyKernel work for programs: system calls, network, file systemLots of small I/O, packets or context switches
niceniUser code at a lowered priorityBatch jobs you reniced (Lesson 20)
idleidNothing to doFine, unless the server is still slow
iowaitwaIdle, but some task is waiting for I/ODisk or NFS is the bottleneck, not the CPU
irq / softirqhi / siInterrupt handlingVery high packet rates or a noisy device
stealstThe hypervisor ran someone else on your CPUThe VM host is overloaded (noisy neighbour)

The tools in this lesson come from three packages. procps-ng gives you uptime, top, vmstat and free. sysstat gives iostat, mpstat, pidstat and sar. iproute2 gives ss and ip -s link.

QuestionFirst toolLook at
Is the CPU the limit?uptime, vmstat 1Load against nproc, the r column, us + sy, id near 0
Is the disk the limit?vmstat 1, iostat -xz 1b column, wa, r_await / w_await, aqu-sz
Is memory the limit?free -h, vmstat 1available, si and so (Lesson 22)
Is the hypervisor the limit?vmstat 1, mpstat 1st / %steal above a few percent
Which process?pidstat 1, pidstat -d 1%CPU, %wait, kB_wr/s per PID
What happened last night?sarThe same numbers, every 10 minutes, kept for weeks

How it works, step by step

A slow-server ticket always goes through the same short path, and the diagram below shows it as five commands.

1Load average counts runnable and waiting tasks

Every few seconds the kernel counts tasks in state R (running or queued for a CPU) and state D (blocked on I/O), and folds that count into three moving averages. A 1-minute value of 2.00 means that, on average over the last minute, two tasks wanted a CPU or were stuck waiting on the disk.

2Compare it with the number of CPUs

Load means nothing until you divide it by the CPU count from nproc. On rocky01 with 2 vCPUs, a load of 2 means both are fully used; 4 means tasks queue for half their time. On a 64-core database server, a load of 20 is a quiet afternoon.

Then look at how the CPU time is spent, because high load with a high wa column is a disk problem, not a CPU problem.

Linux performance monitoring: five commands to read a slow server

3Split the CPU time

vmstat 1 prints one line a second. The r column is tasks wanting a CPU, b is tasks blocked in D state, and the last columns are us, sy, id, wa, st (and gu, guest time, on new procps).

If us + sy is near 100 and r is bigger than your CPU count, you’re CPU-bound. If id + wa is high and b is above zero, the CPUs are waiting for storage.

4Read disk latency, not only “busy”

iostat -xz 1 shows each disk once a second (-x for extended columns, -z to hide idle devices). The numbers that matter are r_await and w_await (milliseconds per read or write, including queue time) and aqu-sz (average requests waiting). %util is the share of time the device had at least one request, which is honest for a single spinning disk but can read 100% on an SSD, NVMe or SAN LUN that is still far from its limit, because those serve many requests in parallel.

5Keep a history with sar

Live tools only show now. sysstat’s collector runs every 10 minutes from a systemd timer, writes a binary file per day, and sar reads it back later: CPU, load, memory, disks, network, all for the same moments. The picture below shows how those pieces fit, and why sar is empty on a server where nobody switched collection on.

Linux performance monitoring: how sysstat collects data for sar

6Write down what normal looks like

“Load 6” is only scary if this server normally runs at 1. Save a baseline on a quiet day and on a busy day: CPU count, a minute of vmstat, iostat for the main disks, the sar averages and the socket count from ss -s. When the ticket comes in, you compare against your own numbers instead of rules of thumb from the internet.

Lesson 73 goes deeper into the high-load ticket (D-state hunting, NFS hangs). Lesson 61 puts the same metrics into Zabbix, so you get an alert before users notice.

Hands-on lab: load rocky01 and read every tool

Before you run any command, double-check it

Read every command twice before you press Enter. Check the target: the server name, the user, the disk or device name, the path and the sizes. These commands are for learning and were tested in our lab, not on your system. At work, run the read-only commands first, follow your change process, make sure a backup exists, and ask a second person to check anything that changes or deletes data. You run them at your own risk.

Everything runs on rocky01 as student, in ~/lab53. stress-ng creates the load and stops by itself after two or three minutes, so nothing keeps running if you walk away. Take a snapshot first (VBoxManage snapshot rocky01 take before-l53 on the laptop) and start with mkdir -p ~/lab53 && cd ~/lab53.

1Count the CPUs and read the load

Run this:

nproc
lscpu | grep -E '^(CPU\(s\)|Model name|Thread|Hypervisor vendor|Virtualization type)'
uptime
cat /proc/loadavg

You should see something like this:

2
CPU(s):                                  2
Model name:                              Intel(R) Core(TM) i5-10210U CPU @ 1.60GHz
Thread(s) per core:                      1
Hypervisor vendor:                       KVM
Virtualization type:                     full
 11:02:14 up  1:48,  2 users,  load average: 0.03, 0.06, 0.02
0.03 0.06 0.02 1/164 2741

rocky01 has 2 vCPUs, one thread each, and lscpu confirms it’s a VM (VirtualBox presents the KVM paravirtual interface to Linux guests, so the vendor line says KVM). The load is close to zero. In /proc/loadavg the fourth field, 1/164, is running tasks over all tasks, and the last number is the most recent PID handed out.

✅ Checkpoint: You know rocky01’s CPU count, so you know what load number means “fully busy” (2).

2Install sysstat and stress-ng, and switch on collection

Run this:

sudo dnf install -y sysstat stress-ng iotop
sudo systemctl enable --now sysstat
systemctl list-timers 'sysstat*'

You should see something like this (the dnf part is cut):

Complete!
Created symlink '/etc/systemd/system/multi-user.target.wants/sysstat.service' → '/usr/lib/systemd/system/sysstat.service'.
Created symlink '/etc/systemd/system/sysstat.service.wants/sysstat-collect.timer' → '/usr/lib/systemd/system/sysstat-collect.timer'.
Created symlink '/etc/systemd/system/sysstat.service.wants/sysstat-summary.timer' → '/usr/lib/systemd/system/sysstat-summary.timer'.
NEXT                            LEFT LAST PASSED UNIT                  ACTIVATES
Tue 2026-10-27 11:00:00 IST     1min -         - sysstat-collect.timer sysstat-collect.service
Wed 2026-10-28 00:07:00 IST      13h -         - sysstat-summary.timer sysstat-summary.service

2 timers listed.

On Rocky Linux 10 all three packages come from the normal BaseOS and AppStream repositories; the iotop you get is iotop-c, the C rewrite, under the old name. Installing sysstat is not enough on the RHEL family: nothing is recorded until you enable sysstat.service, which pulls in the 10-minute collect timer and the nightly summary timer. Daily files go to /var/log/sa/saDD (DD is the day of the month).

On ubu01: sudo apt install sysstat stress-ng iotop-c. Ubuntu 26.04 turns collection on when the package is installed (ENABLED="true" in /etc/default/sysstat) and keeps the files in /var/log/sysstat/. The iotop command there is iotop-c.

✅ Checkpoint: systemctl is-enabled sysstat prints enabled and the collect timer shows a NEXT time within 10 minutes.

3Take a resting baseline with vmstat

Run this (one line per second, five lines):

vmstat 1 5

You should see something like this:

procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st gu
 1  0      0 1052340   4908 380112    0    0    21    14  118  160  1  0 99  0  0  0
 0  0      0 1052340   4908 380112    0    0     0     0   96  141  0  0 100 0  0  0
 0  0      0 1052340   4908 380112    0    0     0     0  101  150  0  1 99  0  0  0
 0  0      0 1052336   4908 380112    0    0     0    12  110  163  0  0 100 0  0  0
 0  0      0 1052336   4908 380112    0    0     0     0   94  138  0  0 100 0  0  0

Ignore the first line: it’s the average since boot. The rest are live. r is 0 or 1 (nobody waiting for a CPU), b is 0 (nobody stuck on I/O), id is 99 to 100 and st is 0. swpd, si and so are memory and swap, which you read in Lesson 22; bi and bo are blocks read from and written to disk per second; in and cs are interrupts and context switches.

Remember these numbers. A quiet rocky01 does about 100 interrupts and 150 context switches a second, and that is your “normal” for this box.

✅ Checkpoint: Your live vmstat lines show r 0 or 1, b 0, id near 100 and st 0.

Linux performance monitoring lab: vmstat showing a run queue of 4 on rocky01
Example output: vmstat on rocky01 while stress-ng runs four CPU workers on two vCPUs.

4Load the CPUs and watch the run queue

Start four CPU workers (twice the CPU count) for two minutes, wait a minute, then look:

stress-ng --cpu 4 --timeout 120s &
sleep 60
uptime
vmstat 1 4

You should see something like this:

[1] 2803
stress-ng: info:  [2803] setting to a 2 mins run per stressor
stress-ng: info:  [2803] dispatching hogs: 4 cpu
 11:08:44 up  1:54,  2 users,  load average: 2.61, 0.83, 0.33
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st gu
 4  0      0 1041220   4908 380240    0    0    20    14  120  162  2  0 98  0  0  0
 4  0      0 1041220   4908 380240    0    0     0     0 1044  523 100 0  0  0  0  0
 5  0      0 1041220   4908 380240    0    0     0     0 1061  540 99  1  0  0  0  0
 4  0      0 1041220   4908 380240    0    0     0     0 1038  517 100 0  0  0  0  0

The 1-minute load climbed to 2.61 and is still rising towards 4, while the 5 and 15-minute values move slowly, which is how you tell a fresh spike from a long problem. vmstat shows r at 4 (four tasks want a CPU), us at 100 and id at 0: the CPUs are the limit. Now look per CPU and per process:

mpstat -P ALL 1 1 | head -n 6
pidstat -u 1 1 | head -n 8

You should see something like this:

Linux 6.12.0-211.16.1.el10_2.0.1.x86_64 (rocky01) 	10/27/2026 	_x86_64_	(2 CPU)

11:08:52 AM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
11:08:53 AM  all   99.50    0.00    0.50    0.00    0.00    0.00    0.00    0.00    0.00    0.00
11:08:53 AM    0   99.00    0.00    1.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00
11:08:53 AM    1  100.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00

Linux 6.12.0-211.16.1.el10_2.0.1.x86_64 (rocky01) 	10/27/2026 	_x86_64_	(2 CPU)

11:08:58 AM   UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
11:08:59 AM  1000      2805   50.00    0.00    0.00   49.00   50.00     0  stress-ng-cpu
11:08:59 AM  1000      2806   49.00    0.00    0.00   51.00   49.00     1  stress-ng-cpu
11:08:59 AM  1000      2807   50.00    0.00    0.00   50.00   50.00     1  stress-ng-cpu
11:08:59 AM  1000      2808   50.00    1.00    0.00   49.00   51.00     0  stress-ng-cpu

mpstat shows both CPUs at 100% user time, so the load is spread evenly. pidstat is the interesting one: each worker gets about 50% of a CPU, and the %wait column says each spent the other half of its time waiting in the run queue. Four tasks on two CPUs means everything runs at half speed, which on a real server is the moment response times double.

The times say AM and PM because rocky01 uses the en_US.UTF-8 locale. For 24-hour times in sysstat output, run the tool with LC_TIME=C.UTF-8 in front, or export S_TIME_FORMAT=ISO.

✅ Checkpoint: vmstat showed r 4 and id 0, and pidstat showed four stress-ng-cpu workers with about 50 %CPU and 50 %wait each.

5Load the disk and watch iowait

Wait until the CPU test has ended (about two minutes), then start two disk writers that sync every write:

stress-ng --hdd 2 --hdd-opts dsync --hdd-bytes 256M --temp-path ~/lab53 --timeout 180s &
sleep 45
uptime
vmstat 1 4

You should see something like this:

[1] 2851
 11:15:31 up  2:01,  2 users,  load average: 1.62, 0.71, 0.49
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st gu
 0  2      0 1032904   4908 384508    0    0    20   160  121  165  2  0 98  0  0  0
 0  2      0 1032904   4908 384508    0    0     0  3464 2210 3105  1  5 44 50  0  0
 1  2      0 1032904   4908 384508    0    0     0  3452 2198 3087  0  6 43 51  0  0
 0  2      0 1032904   4908 384508    0    0     0  3480 2231 3122  1  5 44 50  0  0

A different picture: r is 0, b is 2, us is about 1 and wa is 50. The CPUs are half idle and half “waiting for I/O”, and the two blocked writers are pushing the load average up anyway. bo shows about 3.4 MB/s of writes, small in size, but every write waits for the disk to confirm it. Now ask the disk:

iostat -xz 1 2

Read the second report. You should see something like this:

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           0.50    0.00    5.53   50.25    0.00   43.72

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   wrqm/s  %wrqm w_await wareq-sz     d/s     dkB/s   drqm/s  %drqm d_await dareq-sz     f/s f_await  aqu-sz  %util
dm-0             0.00      0.00     0.00   0.00    0.00     0.00  866.00   3464.00     0.00   0.00    9.21     4.00    0.00      0.00     0.00   0.00    0.00     0.00    0.00    0.00    7.98  99.60
sda              0.00      0.00     0.00   0.00    0.00     0.00  866.00   3464.00     0.00   0.00    4.83     4.00    0.00      0.00     0.00   0.00    0.00     0.00  842.00    2.14    5.99  99.60

dm-0 is the rl-root logical volume and sda is the virtual disk under it. Each write is 4 KiB (wareq-sz), about 866 a second, and each waits 4.8 ms on sda and 9.2 ms through the LVM layer (w_await), with 6 to 8 requests queued (aqu-sz). The f/s column is flushes, the dsync option forcing the disk to commit. %util is 99.6: on a single VirtualBox disk that does mean “full”, but on SSD, NVMe or SAN storage trust await and aqu-sz before %util.

Find who is writing:

pidstat -d 1 1 | head -n 6
sudo iotop -b -o -n 2 -d 1 | tail -n 4

You should see something like this:

Linux 6.12.0-211.16.1.el10_2.0.1.x86_64 (rocky01) 	10/27/2026 	_x86_64_	(2 CPU)

11:15:44 AM   UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
11:15:45 AM  1000      2853      0.00   1736.00      0.00       0  stress-ng-hdd
11:15:45 AM  1000      2854      0.00   1728.00      0.00       0  stress-ng-hdd

Warning: task_delayacct is 0, enable by: echo 1 > /proc/sys/kernel/task_delayacct
  Total DISK READ:    0.00 B/s |   Total DISK WRITE:    3.38 M/s
Current DISK READ:    0.00 B/s | Current DISK WRITE:    3.40 M/s
   TID PRIO     USER   DISK READ  DISK WRITE SWAPIN     IO COMMAND
  2853 be/4 student      0.00 B/s    1.70 M/s 0.00 % 0.00 % stress-ng-hdd [run]
  2854 be/4 student      0.00 B/s    1.69 M/s 0.00 % 0.00 % stress-ng-hdd [run]

pidstat -d shows kB written per second per process, and iotop shows the same per thread with a total line. Both point at the two stress-ng-hdd workers. iodelay (pidstat) and the SWAPIN and IO columns (iotop) stay at 0 because the kernel’s delay accounting is off by default; sudo sysctl kernel.task_delayacct=1 turns it on until the next reboot if you need those columns.

✅ Checkpoint: vmstat showed b 2 and wa near 50 while us stayed low, iostat showed w_await and aqu-sz above zero for sda and dm-0, and pidstat -d named the stress-ng-hdd workers.

6Check the network and the sockets

After the disk test ends, run this:

ss -s
sar -n DEV 1 1 | head -n 5
ip -s link show enp0s3 | sed -n '3,6p'

You should see something like this (the ip lines are left out here):

Total: 168
TCP:   7 (estab 2, closed 0, orphaned 0, timewait 0)

Transport Total     IP        IPv6
RAW	  1         0         1
UDP	  3         2         1
TCP	  7         4         3
INET	  11        6         5
FRAG	  0         0         0

Linux 6.12.0-211.16.1.el10_2.0.1.x86_64 (rocky01) 	10/27/2026 	_x86_64_	(2 CPU)

11:22:07 AM     IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s   %ifutil
11:22:08 AM        lo      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00
11:22:08 AM    enp0s3      3.00      2.00      0.18      0.31      0.00      0.00      0.00      0.00

ss -s is a quick count of sockets by state. On a web server, thousands of timewait sockets are normal, but a growing number of established connections with flat traffic often means a client or a database pool that never lets go. sar in live mode (an interval and a count) works even without collection, and shows packets and kB per second per interface; %ifutil needs a link speed, which virtual cards often don’t report. In ip -s link, look at the errors and dropped counters: they should be 0 and stay 0.

✅ Checkpoint: ss -s and sar -n DEV ran, and ip -s link shows 0 errors on enp0s3.

7Read the history with sar

Wait until at least 30 minutes have passed since step 2, then run:

sar -u
sar -q | tail -n 5
sar -d -p | grep -E 'DEV|sda'

You should see something like this:

Linux 6.12.0-211.16.1.el10_2.0.1.x86_64 (rocky01) 	10/27/2026 	_x86_64_	(2 CPU)

10:58:31 AM       LINUX RESTART	(2 CPU)

11:00:01 AM     CPU     %user     %nice   %system   %iowait    %steal     %idle
11:10:01 AM     all     21.84      0.00      0.62      0.04      0.00     77.50
11:20:01 AM     all      0.71      0.00      2.95     15.36      0.00     80.98
11:30:01 AM     all      0.55      0.00      0.38      0.03      0.00     99.04
11:40:01 AM     all      0.42      0.00      0.31      0.02      0.00     99.25
Average:        all      5.88      0.00      1.07      3.86      0.00     89.19

11:00:01 AM   runq-sz  plist-sz   ldavg-1   ldavg-5  ldavg-15   blocked
11:10:01 AM         0       166      2.48      1.24      0.49         0
11:20:01 AM         0       165      0.11      0.66      0.55         0
11:30:01 AM         0       163      0.02      0.09      0.28         0
11:40:01 AM         0       163      0.00      0.01      0.14         0

11:00:01 AM       tps     rkB/s     wkB/s     dkB/s   areq-sz    aqu-sz     await     %util DEV
11:10:01 AM      1.84      0.31     11.20      0.00      6.25      0.00      0.62      0.07 sda
11:20:01 AM    260.12      0.05   1041.38      0.00      4.00      1.80      6.91     29.88 sda
11:30:01 AM      1.52      0.00      9.75      0.00      6.41      0.00      0.58      0.05 sda

Without -f, sar reads today’s file. “LINUX RESTART” marks the moment sysstat.service started. The 11:10 line holds your CPU test (21.84% user averaged over ten minutes, though it was 100% for two of them), and the 11:20 line holds the disk test (15.36% iowait, 29.88% util for sda). sar -q gives the load history and the runq and blocked counts at each sample time.

Notice what ten-minute averages do: a two-minute 100% spike becomes 21%. sar tells you when and what kind of trouble, then you use the live tools next time. For another day, sar -u -f /var/log/sa/sa26 reads the 26th; to zoom in, add -s 11:05:00 -e 11:20:00.

✅ Checkpoint: sar -u shows a row with high %user and a later row with high %iowait, matching your two tests.

8Save a baseline for the next bad day

Create the script (paste this whole block):

cat > ~/lab53/baseline.sh <<'EOF'
#!/usr/bin/env bash
# baseline.sh: save a performance snapshot of this server to a text file
set -euo pipefail
out="$HOME/lab53/baseline-$(hostname -s)-$(date +%F-%H%M).txt"
{
  echo "== $(hostname -f)  $(date '+%F %T %Z')  CPUs: $(nproc)"
  echo "== uptime";            uptime
  echo "== vmstat 1 5";        vmstat 1 5
  echo "== mpstat -P ALL 1 1"; mpstat -P ALL 1 1
  echo "== iostat -xz 1 2";    iostat -xz 1 2
  echo "== free -h";           free -h
  echo "== ss -s";             ss -s
  echo "== sar -q (today)";    sar -q || echo "no sar data yet"
} > "$out"
echo "saved $out ($(wc -l < "$out") lines)"
EOF
chmod +x ~/lab53/baseline.sh
bash -n ~/lab53/baseline.sh && ~/lab53/baseline.sh

You should see something like this:

saved /home/student/lab53/baseline-rocky01-2026-10-27-1146.txt (71 lines)

The script saves everything you used today into one dated file with the host name in it. Run it on a quiet day and during a busy hour, and keep both; when someone reports “slow”, you run it again and compare side by side. At work this is the same idea as a Zabbix baseline (Lesson 61), only with no server needed.

✅ Checkpoint: A file named like baseline-rocky01-2026-10-27-1146.txt exists in ~/lab53 and contains the vmstat, iostat and sar sections.

Break It, Then Fix It

Take a snapshot of rocky01 first (VBoxManage snapshot rocky01 take before-l53-break). The first trap gives no error message at all, only numbers that lead people to the wrong fix, and the second gives an error that people stop reading after the first line.

1High load, idle CPUs

Create the kind of load a stuck disk or a busy backup gives you:

cd ~/lab53
stress-ng --hdd 4 --hdd-opts dsync --hdd-bytes 256M --temp-path ~/lab53 --timeout 240s &
sleep 90
uptime
top -b -n 1 | head -n 3

You should see something like this:

[1] 3120
 12:03:12 up  2:49,  2 users,  load average: 3.92, 1.86, 0.79
top - 12:03:12 up  2:49,  2 users,  load average: 3.92, 1.86, 0.79
Tasks: 145 total,   0 running, 145 sleeping,   0 stopped,   0 zombie
%Cpu(s):  1.2 us,  6.9 sy,  0.0 ni, 21.5 id, 70.4 wa,  0.0 hi,  0.0 si,  0.0 st

A load of 3.92 on 2 CPUs looks like “the server needs more CPUs”, and that’s the request that reaches the virtualisation team on a bad day. But us is 1.2, there are 0 running tasks, and 70% of the time is iowait. Doubling the vCPUs would change nothing, because none of these tasks is waiting for a CPU.

Fix: confirm it’s I/O, find the D-state tasks and the device, then deal with the source.

vmstat 1 3 | tail -n 2
ps -eo stat,pid,user,cmd | awk '$1 ~ /^D/'
iostat -xz 1 2 | tail -n 3

You should see something like this:

procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st gu
 0  4      0 1029120   4908 388612    0    0     0  3620 2390 3402  1  7 21 71  0  0
D+      3122 student  stress-ng-hdd [run]
D+      3123 student  stress-ng-hdd [run]
D+      3125 student  stress-ng-hdd [run]
sda              0.00      0.00     0.00   0.00    0.00     0.00  905.00   3620.00     0.00   0.00    8.77     4.00    0.00      0.00     0.00   0.00    0.00     0.00  880.00    3.95   11.42  99.80

b is 4, three of the writers are in D state at the moment ps looked (the fourth was between writes), and sda has 11 requests queued with w_await near 9 ms.

The load is made of tasks waiting for storage. At work the “fix” is to find what is writing (pidstat -d, iotop) and move or slow it: a backup that overran into business hours, a log rotation compressing huge files, or a SAN path problem, which Lesson 73 covers in depth. Here, stop the test with kill %1 or wait for the timeout.

✅ Checkpoint: After the writers stop, vmstat 1 3 shows b 0 and wa 0, and the 1-minute load falls within a few minutes.

2Ask sar about a night it never recorded

A manager asks what rocky01 was doing at 3 a.m. yesterday. Run this:

sar -u -f /var/log/sa/sa$(date -d yesterday +%d) -s 02:50:00 -e 03:20:00

You’ll get this error:

Cannot open /var/log/sa/sa26: No such file or directory
Please check if data collecting is enabled

The second line is the useful one. On the RHEL family, installing sysstat doesn’t start collection; on rocky01 you only enabled it in step 2 of today’s lab, so there is no file for yesterday and no way to create one after the fact.

The same message appears on any server where someone ran dnf install sysstat and stopped there. On Ubuntu servers upgraded from older releases, check /etc/default/sysstat: with ENABLED="false" the timers still fire, but every collect run is skipped by the service’s condition, so sar stays empty.

Fix: make collection part of every build, and check it.

sudo systemctl enable --now sysstat
systemctl is-active sysstat-collect.timer
ls -l /var/log/sa/
grep -E '^(HISTORY|SADC_OPTIONS)' /etc/sysconfig/sysstat

You should see something like this:

active
total 112
-rw-r--r--. 1 root root 108416 Oct 27 12:10 sa27
HISTORY=28
SADC_OPTIONS="-S DISK"

Today’s file exists and grows every 10 minutes. HISTORY is how many days are kept (28 here), and -S DISK adds per-disk statistics, which you used with sar -d.

On ubu01 the same settings live in /etc/sysstat/sysstat and the switch in /etc/default/sysstat; sudo dpkg-reconfigure sysstat flips it the supported way. Put the enable command in your build checklist or Ansible role (Lesson 64), because the night you need sar is always before you thought to turn it on.

✅ Checkpoint: sar -u (today) returns rows, and systemctl is-active sysstat-collect.timer prints active.

This happened at work

The exam portal that needed less CPU, not more

My team lead tells this one from his time looking after IT for a chain of schools in Bengaluru. The schools ran their online unit tests on a Moodle server, a 4 vCPU VM at a small hosting provider, and for two weeks in a row the portal crawled every weekday between about 9:30 and 11:30, right when thousands of students were writing tests. Teachers were extending exam times by hand and parents were calling the head office.

The first fix was the obvious one: the load average was 9 on 4 CPUs, so the team doubled the PHP-FPM workers. It got worse. He then ran vmstat 1 during the slow window and saw us around 40, id near 0, and an st column of 35 to 40.

More than a third of the CPU time the VM was promised was being given to someone else on the same physical host, and the extra PHP workers only made the run queue longer.

sysstat had been enabled on that server from day one, so sar -u for the previous ten weekdays showed %steal climbing every morning at 9:20 and falling after noon, while weekends were clean. That graph, more than any complaint, got the hosting provider to admit the host was overcommitted and move the VM to a quieter one the same evening.

Afterwards the team set the PHP workers back to the old count, added an alert for steal above 10% for 10 minutes, and kept a monthly baseline file per server. The next time someone said “the portal is slow, add CPUs”, the first question was “what does st say?”

Common mistakes and troubleshooting

MistakeWhat happensWhat to do instead
Reading load average without the CPU countA load of 8 scares people on a 32-core box and is ignored on a 2-vCPU VMDivide by nproc and compare with your baseline
Assuming high load means high CPUMore vCPUs are ordered for a disk or NFS problemCheck wa and the b column in vmstat 1, and look for D-state tasks
Trusting the first line of vmstat or iostatIt’s the average since boot, so a problem right now looks smallUse an interval and read from the second report on
Treating %util 100 as “disk full” on SSD, NVMe or SANYou chase a disk that still has plenty of capacityRead r_await, w_await and aqu-sz, and compare with the storage team’s numbers
Installing sysstat and not enabling it on RHELsar says “Cannot open /var/log/sa/saDD” on the one night you need itRun systemctl enable --now sysstat in every build
Ignoring st on virtual machinesYou tune an app that is being starved by the hypervisorWatch st in vmstat or %steal in sar and alert on it
Judging a spike from 10-minute sar averagesA 2-minute 100% CPU burst shows as 20% and gets dismissedUse sar for “when”, then live tools or a shorter interval to measure

Interview questions

Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.

Fresher level

1. A server shows a load average of 4.00. Is that a problem?

Why they ask this: They want to hear that load only means something compared with the CPU count and with what is normal for that server.

How you might answer: It depends on how many CPUs it has. I’d run nproc: on a 2-vCPU VM, 4 means tasks are waiting about half the time, while on a 16-core server 4 is quiet.

I’d also check whether it’s CPU or I/O, because load counts tasks waiting for the disk as well. vmstat 1 tells me: high us and r above the CPU count is CPU, high wa and b above zero is storage.

2. In top, what do us, sy, wa and st mean?

Why they ask this: A basic check that you can read the most common screen on any Linux server.

How you might answer: us is time running program code, sy is kernel time for those programs, wa is idle time while something waits for I/O, and st is time the hypervisor gave our virtual CPU to another VM.

If us is high I look for the busy process; if wa is high I look at the disks with iostat; if st is high on a VM, the problem is the host and I talk to the virtualisation team.

2–5 years experience

3. Users say the server is slow. Load is 12 on 4 CPUs, but top shows the CPUs 70% idle. Walk me through it.

Why they ask this: They’re testing whether you know load includes D-state tasks and can move from symptom to device to process.

How you might answer: Idle CPUs with high load usually means tasks blocked on I/O. I’d confirm with vmstat 1 (b column and wa), list D-state processes with ps -eo stat,pid,cmd filtered on D, and run iostat -xz 1 to see which device has high await and a long queue.

Then pidstat -d 1 or iotop tells me who is doing the I/O. If nobody local is busy and await is still high, I’d suspect the storage path, an NFS mount or a SAN LUN, and check dmesg and multipath next.

4. A manager asks what the server was doing at 3 a.m. last night. How do you find out?

Why they ask this: They want to see that you know sar and that you know it only works if collection was set up beforehand.

How you might answer: I’d use sar with yesterday’s file and a time window, for example sar -u -f /var/log/sa/sa26 -s 02:30:00 -e 03:30:00, then the same with -q for load, -r for memory, -d for disks and -n DEV for network. On Ubuntu the files are in /var/log/sysstat.

If sar says “Cannot open” and asks whether collection is enabled, there is no data for that night. I’d look at the journal and the app logs for that window, and enable sysstat on that server and in the build so it doesn’t happen again.

Senior level

5. How would you set up performance baselines and alerts for 200 Linux VMs?

Why they ask this: Seniors should talk about normal per server, the right metrics and avoiding noise, not only tools.

How you might answer: Every VM gets sysstat enabled from the build, plus monitoring like Zabbix collecting CPU split including steal, load per CPU, disk await and queue per device, memory available and swap-in, and network errors. I’d keep two to four weeks of history and compute a baseline per server and per hour of the week, because a batch server at 2 a.m. and a web server at 2 p.m. have different normals.

Alerts would be on sustained deviations, for example steal above 10% for 10 minutes or await three times the baseline for 15 minutes, not on single spikes. Each alert links to a runbook that starts with vmstat, iostat and pidstat, so whoever is on call collects the same evidence.

6. iostat shows %util at 100 on a SAN LUN, but await is 0.4 ms. Is the LUN saturated?

Why they ask this: They’re checking whether you understand what %util measures and why it misleads on parallel devices.

How you might answer: Probably not. %util only says the device had at least one request in flight all the time; it doesn’t know the array behind the LUN can serve dozens at once. With 0.4 ms await, requests aren’t waiting.

I’d look at await and aqu-sz over time and at the throughput and IOPS against what the storage team says the LUN and its paths can do. If await starts climbing while IOPS stay flat, that’s real saturation, and then I’d check the queue depth settings and the multipath paths too.

Quick quiz

Q1. rocky01 has 2 vCPUs and a 1-minute load of 2.00 with us at 100. What does that tell you?

  1. The CPUs are idle
  2. Both CPUs are fully used, with no queue on average
  3. The disk is the bottleneck
  4. The server is swapping
Show answer

B — Load equal to the CPU count with high us means fully used CPUs and, on average, nobody waiting.

Q2. Which kind of task makes load average high while CPUs stay mostly idle?

  1. Zombie processes
  2. Tasks in uninterruptible sleep (D state), usually waiting for I/O
  3. Stopped jobs
  4. Processes with a high nice value
Show answer

B — Linux counts R and D tasks in load, so blocked I/O raises load without using CPU.

Q3. In vmstat 1, which columns point to a disk bottleneck?

  1. si and so
  2. r and us
  3. b and wa
  4. in and cs
Show answer

C — b counts tasks blocked on I/O and wa is CPU time spent idle waiting for I/O.

Q4. On rocky01, sar -u -f /var/log/sa/sa26 says “Cannot open … Please check if data collecting is enabled”. Why?

  1. sar needs root
  2. sysstat.service was never enabled, so no file was written for that day
  3. The file is compressed
  4. The disk is full
Show answer

B — On the RHEL family you must run systemctl enable --now sysstat; past days can’t be recovered.

Q5. What does a high st (steal) value on a VM mean?

  1. The kernel is stealing memory
  2. The hypervisor ran other work on your virtual CPU
  3. A process is stuck
  4. Swap is in use
Show answer

B — Steal is time your vCPU was ready but the host gave the physical CPU to someone else.

Quick recap

  • Load average counts running, queued and D-state tasks; always divide it by nproc and compare with your baseline.
  • vmstat 1 gives the quick split: r and us for CPU, b and wa for disk, si and so for memory, st for the hypervisor.
  • iostat -xz 1 shows latency per device; read r_await, w_await and aqu-sz before %util.
  • mpstat splits by CPU, and pidstat (with -d for disk) names the process and shows how long it waited.
  • Enable sysstat on every RHEL-family server (systemctl enable --now sysstat); Ubuntu 26.04 enables it on install. sar then answers “what happened at 3 a.m.”
  • Save a baseline file on quiet and busy days, so “slow” can be compared with numbers.

Next lesson

Lesson 54: Join Linux to Active Directory → You’ll build a small Samba AD domain controller, ad01, join rocky01 and ubu01 to it with realmd and SSSD, limit who can log in and give an AD group sudo rights.

Further reading: Linux performance monitoring

Want to go deeper into Linux performance monitoring? Read the man7.org pages for vmstat(8) and proc_loadavg(5), the sysstat project site for iostat, mpstat, pidstat and sar, the RHEL 10 documentation on monitoring system performance, and the stress-ng project for its many stressors. You can also see the full plan for this course on the Linux Administration course page.

Scroll to Top

This website uses cookies

We use a few cookies to keep this site working. If you allow it, we also use Google Analytics to count visits so we can improve our lessons. You can accept all, deny the optional ones, or choose what to allow.