You know what happened at three in the morning: fifteen db_timeouts with the 200 connections exhausted. What you do not know is why. And now Marta adds fuel to the fire: "Several of the sales team say the website is slow in the mornings, around nine. Is it something at our end?". "It is slow" is not a piece of data, it is a feeling; turning it into a number, finding the guilty resource and proving that the change worked is precisely the job of this lesson. You are going to learn a reproducible method instead of a collection of loose commands, to genuinely interpret what uptime, free or iostat say, and to establish a baseline for srv-tramontana without which no measurement means anything.

Contents

  1. Monitoring, diagnosing and the USE method
  2. CPU: load average, vmstat, mpstat and steal
  3. Memory: what "used" means, swap and the OOM killer
  4. Disk: iostat -xz, latency versus throughput
  5. Network: sar -n DEV, ss -s and retransmissions
  6. Overview tools and history with sar
  7. Continuous metrics and the four golden signals
  8. A procedure for "the server is slow"
  9. Tuning within an administrator's reach
  10. The srv-tramontana baseline
  11. Tramontana case: "the website is slow in the mornings"

  1. Monitoring, diagnosing and the USE method

These are two different activities and they are constantly confused:

  • Monitoring is knowing how the system is doing continuously and without anybody watching: metrics every fifteen seconds, stored, with alerts. It answers "is it all right?" before the customer calls.
  • Diagnosing is finding out why it is going badly at a particular moment, with interactive tools and a hypothesis that is confirmed or discarded.

A human staring at top is not monitoring: it is manual, occasional diagnosis with no memory. You need both, and the first makes the second possible, because a measurement with nothing to compare it against says nothing: a load of 3.5 may be normal or catastrophic, and you only know which if you know yesterday's.

The USE method (from Brendan Gregg) gives the order in which to look. For each resource — CPU, memory, disk, network — three things are checked:

Dimension Question On CPU On disk
Utilisation What proportion of the time is it busy? %us + %sy %util from iostat
Saturation How much work is queued? The r column of vmstat aqu-sz, await
Errors Are there recorded faults? Machine check exceptions I/O errors in dmesg

Its practical value is that it stops you wasting time: in five minutes you go through the four resources with three questions each and come out knowing which is the bottleneck.

  1. CPU: load average, vmstat, mpstat and steal

$ uptime ; nproc
 09:14:22 up 6 days,  2:31,  2 users,  load average: 3.42, 2.10, 1.08
2

The three numbers are the average of processes in the runnable (R) or uninterruptible-wait (D) state over 1, 5 and 15 minutes. Two rules change everything. First: you have to normalise by the number of cores; with nproc = 2, a load of 3.42 is 1.71 processes per core and the system is saturated, whereas the same 3.42 on a 16-core machine would be 21% occupancy, that is, nothing. Second: the D state counts, and that is the nuance almost nobody knows, because on Linux the load includes processes blocked waiting for disk; that is why you can see a load of 8 with the CPU at 5%.

The trend is informative too: 3.42 2.10 1.08 is a load that is rising (the problem is starting now); 1.08 2.10 3.42 is one that is already resolving itself.

vmstat 1, column by column

$ vmstat 1 5
 r  b   swpd    free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 4  2      0  198432  91240 1842104    0    0    12    48  412  980 18  4 22 56  0
 5  3      0  196108  91240 1843320    0    0     8 14620  902 2140 21  6  9 64  0
 4  3      0  195884  91240 1843644    0    0     4 15108  918 2205 19  5  8 68  0
Column Meaning When it is a worry
r Processes ready to run, waiting for CPU If it sustainedly exceeds the number of cores
b Processes blocked on I/O (state D) Any sustained value > 0 points at the disk
si / so KiB/s going into and out of swap Any sustained value is bad: thrashing
bi / bo Blocks read/written per second Context for the disk
us / sy / id % of CPU in user space, kernel and idle High sy: system calls or networking
wa % waiting for I/O High with low id: the disk is the bottleneck
st % stolen by the hypervisor (steal) > 5% sustained: the problem is not in your VM

The first line of vmstat is the average since boot: always ignore it and look from the second one onwards. In the example, wa at 56–68% with r at 4–5 and bo sky-high shouts that the bottleneck is the disk, not the CPU.

The st deserves a note: in a VM it is time the hypervisor gave to another machine, so an st of 20% means no optimisation of yours will fix anything, because the problem is on the host or with the provider.

And the breakdown by core and by process:

$ mpstat -P ALL 1 1 | tail -3
Average:  CPU  %usr %nice %sys %iowait %steal %idle
Average:    0  19.2   0.0  4.8    58.1    0.0   17.5
Average:    1  17.9   0.0  5.1    54.3    0.0   22.4
$ pidstat -u 1 1 | sort -k8 -rn | head -3
12:31:03  UID  PID  %usr %system  %wait  %CPU  CPU  Command
12:31:03  997 1284  16.00    3.00   1.00 19.00    0  tramontana
12:31:03 1000 8842   4.00    9.00  22.00 13.00    1  backup_tramonta

mpstat -P ALL reveals a very common case: the overall average looks reasonable but a single core is at 100% because the application does not parallelise. pidstat breaks the consumption down by process, and its %wait column — time spent waiting to be given CPU — is pure gold for spotting contention.

  1. Memory: what "used" means, swap and the OOM killer

$ free -h
               total        used        free      shared  buff/cache   available
Mem:           3.8Gi       1.1Gi       208Mi        18Mi       2.5Gi       2.4Gi
Swap:          2.0Gi          0B       2.0Gi

"Free: 208 Mi" is not a problem. It is exactly the opposite: free RAM is wasted RAM, and the kernel uses it as page cache so as not to read from disk again. The columns that matter:

used is process memory without cache or buffers; buff/cache is page cache and metadata, which the kernel releases instantly if it needs to; and available is the figure to look at: how much memory a new application can obtain without swapping. With 2.4 GiB available out of 3.8, srv-tramontana has plenty; the alarm would be available falling towards zero, not free.

$ grep -E 'MemTotal|MemAvailable|Dirty' /proc/meminfo   # the raw detail; vmstat -s summarises
$ ps -eo pid,user,rss,vsz,comm --sort=-rss | head -3
   PID USER       RSS      VSZ COMMAND
  1284 svc-tram 219848  1284932 tramontana
  1102 postgres  98204   412088 postgres

Three figures that get confused every day: VSZ is the whole reserved address space — shared libraries and memory requested but never touched included — almost always enormous and irrelevant; RSS is the pages actually in RAM, useful but it counts each shared library in full in every process, so adding up the RSS values gives far more than the total RAM; and PSS divides each shared page between the processes using it, and is the only one that adds up properly (you see it with smem -k or in /proc/PID/smaps_rollup).

Swap and thrashing

Having swap in use is not bad in itself: the kernel offloads pages nobody touches. What is serious is the constant traffic, which shows in si/so in vmstat. If those columns are sustainedly active, the system spends more time moving pages than working: that is thrashing, and it is felt as brutal slowness with the CPU almost idle.

The OOM killer

When there is no memory and no swap left, the kernel picks a victim and kills it. Knowing it has acted is essential, because the symptom that reaches you is "the service restarted on its own":

$ sudo journalctl -k --since yesterday --grep -i 'out of memory'
Aug 17 04:41:07 srv-tramontana kernel: Out of memory: Killed process 1284 (tramontana)
    total-vm:1284932kB, anon-rss:2894120kB, oom_score_adj:0

It chooses by oom_score, which rewards killing the process that frees the most memory, weighted by oom_score_adj (from −1000 to 1000). It is queried in /proc/PID/oom_score and adjusted on the fly with sudo choom -p 1284 -n -500. Although in a systemd service the right thing is to declare it in the unit (OOMScoreAdjust=-500) and, above all, to set MemoryMax as you saw in 05-05: that way the OOM acts inside the service's cgroup and does not take the database down with it.

  1. Disk: iostat -xz, latency versus throughput

$ iostat -xz 1 2 | tail -4
Device   r/s     rkB/s   w/s     wkB/s   rareq-sz wareq-sz aqu-sz  r_await w_await %util
dm-0     2.00     8.00 148.00 14620.00     4.00    98.78    3.42     1.20   22.85  97.60
sda      1.00     4.00  92.00 15108.00     4.00   164.22    2.98     0.90   31.10  94.20
Column What it measures Rough threshold
%util % of time with at least one request in flight > 90% sustained: saturated (less reliable on SSD/NVMe)
r_await / w_await Average latency per request, in ms > 10 ms on SSD or > 20 ms on a mechanical disk: there is a queue
aqu-sz Average queue depth > 1 sustained: there is saturation

The critical distinction is latency versus throughput: a disk can move 200 MB/s quite happily with a sequential task (high throughput, low latency) and choke on 5 MB/s of small random writes (ridiculous throughput, terrible latency). What hurts the user is the latency, not the throughput.

And who is writing:

$ sudo iotop -b -n 1 -o -P | head -3
  PID  PRIO  USER   DISK READ  DISK WRITE  COMMAND
 8842  idle  operato   0.00 B/s   13.94 M/s  backup_tramontana.sh
$ sudo lsof -p 8842 | grep -E 'REG.*backups' | head -1
backup    8842 operator  4w  REG  253,0  /srv/.../tramontana-2026-08-18.tar.gz.partial

  1. Network: sar -n DEV, ss -s and retransmissions

$ sar -n DEV 1 1 | grep -E 'IFACE|enp0s3'
12:44:01     IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s
12:44:02    enp0s3    412.00    398.00    118.42    902.18
$ ss -s | head -2
Total: 214
TCP:   187 (estab 142, closed 21, orphaned 0, timewait 19)
$ ss -tn state established '( dport = :5432 )' | wc -l
200

That last command is a preview of today's case: two hundred established connections to the database's port, exactly the max_connections in app.conf.

To see the traffic live, nload (per interface) or iftop (per conversation) are immediate.

And the TCP health indicator you have to know about, nstat -az | grep TcpRetransSegs: retransmissions are segments that had to be sent again because no acknowledgement arrived. A small percentage is normal; a rising rate indicates packet loss — a cable, a saturated link or an intermediate device dropping traffic. You already have the complete network diagnosis, with mtr and the layers, from 03-08.

  1. Overview tools and history with sar

Tool What it contributes
htop A navigable top: a tree with F5, filters, interactive nice, per-core bars
atop It stores history every 10 min and lets you "rewind" to the time of the incident
glances / dstat A single panel with threshold alerts / per-second series, ideal for CSV
sysstat / sar The memory of performance: historical metrics every 10 minutes

The point most often overlooked: today's snapshot is useless without yesterday's. If Marta complains at 09:10 about something that happened at 09:00, top no longer says anything; sar does:

sudo apt install sysstat
sudo sed -i.bak-$(date +%F) 's/^ENABLED="false"/ENABLED="true"/' /etc/default/sysstat
sudo systemctl enable --now sysstat sysstat-collect.timer
sar -u -s 08:50:00 -e 09:30:00   # CPU in that window TODAY; -q gives the historical load
sar -r -f /var/log/sysstat/sa17  # memory on the 17th
sar -b -s 04:00:00 -e 05:00:00   # I/O during the nightly backup

Enabling sysstat takes a minute and it is what turns "I think it was slow" into "at 09:04 the %iowait rose to 61%".

  1. Continuous metrics and the four golden signals

For more than one server, or to alert without anybody watching, today's standard is Prometheus (a time-series database that polls the targets), node_exporter (which exposes the system's metrics at /metrics), Grafana (dashboards) and Alertmanager (notifications). We are not setting it up here: it is the subject of Module 8. What you should take away is what to measure, the four golden signals:

Signal What it measures In Tramontana
Latency The duration of a request in percentiles (p50, p95, p99), separating the ones that fail ms in access.log
Traffic / Errors Requests per second / failure rate Lines per minute and the 23 error codes in the log
Saturation How full the most limited resource is active_connections / max_connections

And a rule about alerts: an alert that goes off every day stops being read. Alert on symptoms that hurt the user (p95 latency, error rate), not on every CPU spike. It is the same "silence if all is well" discipline as in your scripts.

  1. A procedure for "the server is slow"

This is the numbered procedure you can apply as it stands, in this order, without skipping steps:

  1. Translate the complaint into a measurable number. Which operation, at what time, how long does it take now and how long before? Without this you cannot know whether you have fixed anything.
  2. A 60-second snapshot. uptime, vmstat 1 5, free -h, iostat -xz 1 3, ss -s, systemctl --failed. With six commands you know whether the problem is CPU, memory, disk or network.
  3. Compare with the baseline. Is it abnormal, or has it always been like that and what has changed is the expectation?
  4. Identify the resource with USE and choose one, explicitly ruling out the others.
  5. Locate the culprit within that resource with pidstat, iotop, ss or top -H: a name and a PID.
  6. Formulate a falsifiable hypothesis. "The 04:20 backup saturates the disk and that is why the queries time out" can be confirmed or ruled out; "it is slow" cannot.
  7. Review what changed: journalctl --since, /var/log/apt/history.log, the git log of your scripts, the HISTORY. 80% of new problems come from a recent change.
  8. Measure before touching anything and save the figures in a dated file.
  9. Change ONE single thing and document what and why.
  10. Measure afterwards, under the same conditions, and compare. If it does not improve, revert before trying something else.
  11. Write the result down in the HISTORY, even — especially — if the change did not help.

Steps 8, 9 and 10 are non-negotiable. Changing three parameters at once and seeing an improvement teaches you nothing: you do not know which one helped, and you will drag two useless settings around for ever.

  1. Tuning within an administrator's reach

Kernel parameters via sysctl are the subject of 07-03 and profiling with perf and eBPF is 07-02. What is in your hands today:

Open file limits. A server with many connections exhausts the per-process limit and rejects requests with Too many open files:

$ ulimit -n ; cat /proc/1284/limits | grep 'open files'
1024
Max open files            1024                 4096                 files

In a systemd service you do not touch /etc/security/limits.conf — which applies only to login sessions via PAM — but the unit:

[Service]
LimitNOFILE=65535

The service's resource limits: MemoryMax and CPUQuota from 05-05, which as well as protecting are useful for measuring, because systemd-cgtop shows whether the service reaches its ceiling. And noatime in fstab (05-04), which avoids one write for every read and is noticeable on a server with many small files.

Shallow thread profiling: top -H -p 1284 breaks the consumption down thread by thread when a multithreaded process is loaded and you do not know which part. perf top would give the breakdown by function, but that is 07-02.

The application's own parameters, which usually pay off more than any system tuning: in Tramontana, max_connections and query_timeout in /etc/tramontana/app.conf.

  1. The srv-tramontana baseline

A baseline is a snapshot of normal behaviour, taken when everything is fine, to compare against when it is not. It is kept alongside the reference one from Module 1:

#!/usr/bin/env bash
# /home/operator/scripts/baseline.sh — a dated performance snapshot
set -euo pipefail
dest="/srv/tramontana/backups/baseline-$(date +%F-%H%M).txt"
{
    echo "== $(date --iso-8601=seconds) $(hostname) =="
    uptime; nproc; free -h
    vmstat 1 5 | tail -4
    iostat -xz 1 2 | tail -6
    ss -s; df -h /; df -i / | tail -1
} > "$dest"

Normal values measured on srv-tramontana on a Tuesday at 11:00, with the application running:

Metric Normal value Attention threshold
Load average (2 vCPU) 0.15 – 0.40 > 2.0 sustained
CPU id / wa 85 – 95% / 0 – 3% < 40% / > 20%
Available memory 2.3 – 2.5 GiB < 400 MiB
Swap si/so 0 Any sustained value
w_await on dm-0 0.8 – 2.5 ms > 20 ms
Connections to 5432 / disk / 30 – 60 / 30% > 150 / > 85%

That table, and not intuition, is what turns a measurement into a diagnosis.

  1. Tramontana case: "the website is slow in the mornings"

Step 1 — Translate the complaint. We ask and we measure: the availability search, which normally answers in 180 ms, takes between 4 and 9 seconds between 08:50 and 09:15. That is a number already.

Steps 2-3 — A snapshot and a comparison with the baseline. As the incident is recurring, we measure at 09:00 the next day:

$ uptime
 09:04:11 up 6 days, 22:20,  2 users,  load average: 3.42, 2.10, 1.08
$ vmstat 1 3 | tail -1
 5  3      0  195884  91240 1843644    0    0     4 15108  918 2205 19  5  8 68  0

A load of 3.42 with 2 vCPU (baseline: 0.15–0.40) and wa at 68% (baseline: 0–3%) with id at 8%. It is not CPU that is missing: it is disk. Memory and swap, normal. Resource identified: I/O.

Steps 4-5 — The culprit.

$ iostat -xz 1 2 | tail -2
Device   r/s   rkB/s    w/s    wkB/s  aqu-sz  w_await  %util
dm-0    2.00    8.00 148.00 14620.00    3.42    22.85   97.60
$ sudo iotop -b -n 1 -o | tail -1
 8842  idle operator   13.94 M/s  backup_tramontana.sh
$ journalctl -u tramontana-backup.service --since "2 days ago" -o short-iso | grep -E 'start|completed'
2026-08-17T04:20:06+0200 backup[8811]: backup start (version=3.2.1)
2026-08-17T06:02:44+0200 backup[8811]: backup completed in 6158s
2026-08-18T04:20:07+0200 backup[8842]: backup start (version=3.2.1)

A w_await of 22.85 ms against the baseline's 0.8–2.5, %util at 97.6%, and the one doing the writing is the backup, which in theory is launched at 04:20: the one on the 17th took 1 h 42 min and the one on the 18th had still not finished at 09:04. The data set has grown and the backup overlaps with the peak hour.

Step 6 — A falsifiable hypothesis. "The backup saturates the disk's I/O until after 09:00; that lengthens the database queries, which hit the 30 s query_timeout and exhaust the 200 connections of max_connections, which is what causes the db_timeouts in the logs."

Checking the second half:

$ ss -tn state established '( dport = :5432 )' | wc -l   # 200: the exact ceiling
$ journalctl -u tramontana.service --since "09:00" -p err --no-pager | wc -l
11

Exactly two hundred connections — the ceiling — and eleven errors in four minutes. Hypothesis confirmed: the same mechanism that caused the 03:00 db_timeouts in the previous lesson.

Steps 7-9 — What changed, measure and change ONE thing. The HISTORY says it: the backup volume was migrated on the 18th and the data set has grown. The root cause is the duration of the backup, not the connections. The minimal, reversible change:

### /etc/systemd/system/tramontana-backup.service.d/override.conf
[Service]
# The backup must finish before the peak hour (08:45); if not, it aborts and warns.
RuntimeMaxSec=3h
IOWeight=10
$ sudo systemctl edit tramontana-backup.timer   # OnCalendar=*-*-* 02:30:00
$ sudo systemctl daemon-reload && systemctl list-timers tramontana-backup.timer | tail -1
Wed 2026-08-19 02:30:00 CEST 14h left  tramontana-backup.timer

Step 10 — Measure afterwards, at the same time and with the same commands.

Metric at 09:04 Before After
Load average / CPU wa 3.42 / 68% 0.38 / 2%
w_await dm-0 22.85 ms 1.40 ms
Connections to 5432 200 (the ceiling) 47
db_timeout errors (09:00–09:15) 11 0
Search latency 4–9 s 190 ms

Step 11 — Document. And the report for Marta, with the usual structure: "The morning slowness was caused by the backup, which had grown to take more than four hours and was still writing during peak hours; by saturating the disk, the queries timed out and the 200 database connections were exhausted. We have brought the backup forward to 02:30 and given it a 3-hour limit and lower I/O priority. Measured at 09:04: the search is back to 190 ms and there are no errors. What this protects: it stops the backup interfering with working hours. What it does not protect: if the data set keeps growing, we will hit this again; we need to move to incremental backups, and we are tackling that right now. Nor does it solve the fact that 200 connections is a tight limit if the number of users grows."

sudo tee -a /opt/tramontana/HISTORY >/dev/null <<'END'
2026-08-19  Morning performance (operator)
  - Cause: 4h+ backup saturating I/O (w_await 22.85 ms, %util 97.6) past 09:00
  - Effect: queries hitting the 30s timeout -> 200/200 connections -> db_timeout
  - Change: timer to 02:30, RuntimeMaxSec=3h, IOWeight=10 (one single change, measured)
  - Result 09:04: load 0.38 / wa 2% / w_await 1.40ms / 47 connections / 0 errors / 190 ms
  - Pending: incremental backups (the full backup will keep growing)
END

Common Mistakes and Tips

  • Reading the load average without dividing by nproc. A 3.42 means nothing until you know how many cores there are.
  • Panicking about "little free memory". The figure that matters is available; the cache is memory well spent.
  • Trusting the first line of vmstat or iostat. It is the average since boot, not the current moment.
  • Looking at %util on an SSD as if it were a mechanical disk. With parallel queues, an NVMe can show 100% and not be saturated. Look at await and aqu-sz.
  • Diagnosing without a baseline. Without knowing what is normal, any number looks alarming or reassuring depending on your mood. And do not change several things at once: you will not know which one worked, and you will drag useless settings around for years.
  • Having no history. Install sysstat on day one: by the time somebody asks about 09:00, it will be too late to enable it.
  • Confusing a symptom with the cause. The 200 connections were the symptom; the backup was the cause. Raising max_connections would have hidden the problem.
  • Tip: save every measurement in a dated file, alongside the HISTORY. In six months' time, that folder will be worth more than your memory.

Exercises

  1. Reading a snapshot. A server with 4 vCPU shows load average: 7.80, 7.20, 6.90, vmstat with us=6 sy=3 id=4 wa=87 st=0, r=1, b=6, and free -h with 5.1 GiB available out of 8. Which resource is saturated, which one is not, and what are the next two commands?
  2. A service that dies in the small hours. tramontana.service shows up as restarted every night around 03:40 without anybody touching it. Give the commands to confirm whether it was the OOM killer and, if it was, two ways of stopping it taking the database down with it.
  3. Proving an improvement. Design the exact protocol — commands, moments and success criterion — to demonstrate that enabling noatime on /srv/tramontana/backups shortens the nightly backup, in a way that would convince a sceptic.

Solutions

1. The saturated resource is the disk. The proof: wa=87% with id=4% means the CPU is sitting still waiting for I/O, and b=6 is six processes blocked in state D, which are also what inflate the load to 7.80 despite there being only r=1 waiting for CPU. Normalised, 7.80 across 4 cores would be 1.95, but that number is misleading here precisely because it is made up of processes in D, not in R. What is not saturated: the CPU (us+sy = 9%) or the memory (5.1 GiB available out of 8, with no swap traffic). The next two commands:

iostat -xz 1 3          # which device, with what latency (w_await) and what queue (aqu-sz)
sudo iotop -b -n 1 -o   # which specific process is doing that I/O

2.

$ sudo journalctl -k --since "03:00" --until "04:00" --grep -i 'out of memory'
Aug 17 03:41:12 srv-tramontana kernel: Out of memory: Killed process 1284 (tramontana)
$ systemctl show tramontana.service -p NRestarts     # NRestarts=6
$ journalctl -u tramontana.service --since "03:00" | grep 'Main process'
Aug 17 03:41:12 systemd[1]: tramontana.service: Main process exited, code=killed, status=9/KILL

status=9/KILL without anybody having run a kill is the OOM killer's signature, and the kernel's message confirms it. Note the coincidence in timing with the nightly backup: the page cache the backup generates puts pressure on the available memory.

Two ways of protecting the database:

# (a) In tramontana.service: the OOM acts INSIDE the service's cgroup
[Service]
MemoryMax=512M
Restart=on-failure
# (b) In postgresql.service: make it an unlikely victim
[Service]
OOMScoreAdjust=-800

The first is the good one, because it contains the problem at its source: when Tramontana overruns its memory, Tramontana will die and systemd will bring it back up, without the kernel choosing a victim across the whole machine. The second is a defensive complement: it biases the choice, but it does not stop the machine running out of memory.

3. The protocol, which is the method from section 8 applied:

# 1. Measure the current state, three nights in a row to capture the variability
journalctl -u tramontana-backup.service --since "3 days ago" -o cat | grep 'completed in'
# -> 6158s, 5904s, 6021s  (median 6021 s); sar -b -f /var/log/sysstat/sa17 for the I/O

# 2. Change ONE single thing, with a prior backup and verification
sudo cp -a /etc/fstab /etc/fstab.bak-$(date +%F)
sudo sed -i 's|\(/srv/tramontana/backups.*defaults\)|\1,noatime|' /etc/fstab
sudo diff -u /etc/fstab.bak-$(date +%F) /etc/fstab
sudo mount -o remount /srv/tramontana/backups
findmnt -no OPTIONS /srv/tramontana/backups    # check that noatime is active

# 3. Measure another three nights, the same commands and the same times
journalctl -u tramontana-backup.service --since "3 days ago" -o cat | grep 'completed in'

The success criterion, fixed before measuring: a reduction of more than 10% in the median duration with no increase in errors in journalctl -u tramontana-backup.service -p err. Three nights on each side because a single measurement cannot tell an improvement from a quiet night. And if the criterion is not met, you revert with the .bak and you write in the HISTORY that it did not help: a documented negative experiment saves somebody repeating it in a year's time.

Conclusion

You no longer depend on somebody "noticing" that the server is slow. You tell monitoring from diagnosing and you apply the USE method — utilisation, saturation, errors — to the four resources. You read the load average normalised by nproc knowing that it includes processes in state D, and you interpret vmstat 1 column by column: r and b, si/so, us/sy/id/wa and the st that gives away the hypervisor. You break the consumption down with mpstat -P ALL and pidstat.

In memory you know that "free" is a misleading figure and that the good one is available, you tell VSZ, RSS and PSS apart, you recognise thrashing in si/so, and you know how to prove the OOM killer acted with journalctl -k and how to stop it taking the neighbour with it using MemoryMax and OOMScoreAdjust. On disk you handle iostat -xz with %util, await and aqu-sz, you are clear about the difference between latency and throughput, and you locate the culprit with iotop and lsof. On network you use sar -n DEV, ss -s and retransmissions. You know htop, atop, glances and dstat, and — most importantly — you have enabled sysstat so as to have the history without which today's snapshot means nothing. You know what the four golden signals are and why an alert that goes off every day stops being read.

You have an eleven-step procedure for "the server is slow", with the three non-negotiable rules: measure before, change one single thing, measure after. You have a baseline for srv-tramontana with written thresholds. And you have solved the case: the morning slowness was the backup saturating the I/O until after 09:00, causing the db_timeouts and the exhaustion of the 200 connections; bringing it forward and capping it took the search back from 4–9 s to 190 ms, with the before-and-after figures written into the HISTORY.

But the report for Marta ended with a loose end that cannot wait: the full backup will keep growing, and the problem with it. And there is something far worse that nobody has checked yet: nobody has ever restored that backup. In Backup and Restore, the module's last lesson, you will see why backups do not exist, only tested restores: you will set an RPO and an RTO for Tramontana, you will apply the 3-2-1 rule and generational retention, you will solve consistency with the LVM snapshots from 05-04, you will use tar --listed-incremental, rsync --link-dest and a modern tool with deduplication and encryption, and you will write the step-by-step restore procedure for the three scenarios that can genuinely happen to you.

Linux Course: From Beginner to System Administrator

Module 1: Introduction to Linux

Module 2: Basic Linux Commands

Module 3: Advanced Command-Line Skills

Module 4: Shell Scripting

Module 5: System Administration

Module 6: Networking and Security

Module 7: Advanced Topics

Module 8: Practical Projects

© Copyright 2026. All rights reserved