In Module 5 you learned to measure. The USE method, vmstat, iostat, free, sar and pidstat tell you how much: the CPU is at 80%, the disk has 22 ms of write latency, 300 MB of memory is still available. With that you solved the incident of the website being slow in the mornings, because the counters pointed at I/O and the I/O had an identifiable culprit.
But there is a class of problem the counters are no use for: when they are all green and the service is performing badly anyway. The CPU idle, the disk quiet, memory plentiful, and the application taking ten times longer than it should. At that point the question stops being how much and becomes what exactly is this process doing, and answering it needs tools that do not measure aggregates but observe individual events.
This lesson gives you three of them, in increasing order of sophistication and decreasing order of cost: strace to see the system calls one by one, perf to profile where the cycles really go, and eBPF to instrument the kernel in production with no appreciable penalty. And you are going to run head-on into a consequence of your own work from Module 6.
Contents
- When counters are not enough
- strace: the system calls one by one
- The obstacle you put there yourself: ptrace_scope
- ltrace and the library level
- perf: sampling-based profiling
- Flame graphs
- eBPF: instrumenting the kernel in production
- The bpfcc tools and the question each one answers
- A decision table: which tool for which question
- The Tramontana case: from 400 ms to 40 ms
When counters are not enough
The difference between the two families of tools is the unit of observation:
| Counters (M5) | Tracing and profiling (this lesson) | |
|---|---|---|
| What they observe | Aggregates and averages over an interval | Individual events |
| Examples | vmstat, iostat, free, sar |
strace, perf, bpftrace |
| Cost | Negligible; you leave them running all the time | From negligible to prohibitive |
| They answer | How much, where | What, why |
| Blind spot | Whatever is not in the average | They need a hypothesis first |
That last point is important and it orders the whole lesson: tracing tools do not replace the counters, they continue from them. You use them once you already have a hypothesis you want to confirm or refute. Firing strace at a process without knowing what you are looking for produces a hundred thousand unreadable lines and slows the service down.
The methodology, then, has three steps and the order is not negotiable:
- A measured symptom. "The application takes 400 ms to respond; the baseline says 40 ms." Not "it is slow".
- Hypotheses ruled out with counters. CPU? Disk? Memory? Network? It is free and it eliminates most cases.
- A targeted measurement with the right tool. One specific hypothesis, one tool, one answer.
strace: the system calls one by one
Remember the layers from Module 1: hardware → kernel → system calls → shell and libraries → applications. A system call is the only way a program has of asking the kernel for something: open a file, read from a socket, allocate memory, wait. Everything a process does towards the outside world goes through there.
strace intercepts those calls and prints them. It is literally watching the conversation between the program and the kernel.
$ sudo apt install strace
# The simplest thing: trace a command from the start
$ strace -f ls /opt/tramontana 2>&1 | head -12
execve("/usr/bin/ls", ["ls", "/opt/tramontana"], 0x7ffd1c4a2b18 /* 24 vars */) = 0
brk(NULL) = 0x5f8c1a2f4000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, {st_mode=S_IFREG|0644, st_size=71234, ...}, 0) = 0
mmap(NULL, 71234, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f2a1c4b0000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
...
openat(AT_FDCWD, "/opt/tramontana", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
getdents64(3, 0x5f8c1a2f5b40 /* 5 entries */, 32768) = 160
write(1, "app HISTORY releases shared\n", 31) = 31Every line has the same structure: the call's name, the arguments in brackets, and the returned value after the =. If the value is negative, the error's symbolic name (ENOENT, EACCES, EAGAIN) and its description appear. That third part is usually where the answer is.
Look at the /etc/ld.so.preload line: it returns ENOENT. That is not an error, it is normal — that file rarely exists — and it illustrates something you have to internalise: strace shows an enormous number of expected errors. Knowing which ones are normal is half the craft.
The options that actually get used
| Option | What it does | When |
|---|---|---|
-f |
Follows child processes and threads | Nearly always; without it you lose half of it |
-p PID |
Attaches to a process already running | Diagnosis in production |
-e trace=<list> |
Only the calls listed | Indispensable if you are not to drown |
-c |
Only the statistical summary at the end | The first step, nearly always |
-T |
Adds how long each call took | When you are chasing latency |
-tt |
Timestamp with microseconds | Correlating with logs |
-s N |
Maximum string length (32 by default) | -s 200 to see complete paths and data |
-o file |
Writes to a file instead of stderr | Long sessions |
-y |
Shows the path behind each file descriptor | Very useful with sockets and files |
-k |
Shows the call stack | When you need to know who called |
The -e trace= groups save you memorising names:
| Group | Includes |
|---|---|
%file |
Everything that takes a filename (openat, stat, unlink…) |
%desc |
Operations on descriptors (read, write, close, poll…) |
%network |
socket, connect, accept, send, recv… |
%process |
fork, execve, wait, exit… |
%memory |
mmap, brk, munmap… |
%signal |
Signals |
The statistical summary: always start here
$ sudo strace -f -c -p 4318
strace: Process 4318 attached with 4 threads
^Cstrace: Process 4318 detached
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
89.14 2.417882 8062 300 connect
6.02 0.163291 272 600 sendto
3.11 0.084372 140 602 recvfrom
0.98 0.026573 44 604 epoll_wait
0.41 0.011118 18 617 futex
0.34 0.009229 15 602 12 read
------ ----------- ----------- --------- --------- ----------------
100.00 2.712465 3325 12 totalThis is the first step of any diagnosis with strace, and for a practical reason: it fits on one screen and it immediately says where the time goes. Here 89% of the time is in connect, with 300 calls at 8 ms each. That is an enormous clue: the process is constantly opening new connections and each one costs 8 milliseconds.
The columns: % time is the proportion of the total time inside system calls (not of the process's total time: if the program burns CPU in its own code, it does not show up here); usecs/call is the average per call; errors counts the returns that failed.
The classic case: "it cannot find its configuration file"
The most profitable use of strace, and the one to have memorised. A program fails saying it cannot find a file, and you are certain the file exists:
$ sudo -u svc-tramontana /opt/tramontana/app/tramontana --config /etc/tramontana/app.conf
error: could not load the configuration
$ ls -l /etc/tramontana/app.conf
-rw-r----- 1 root tramontana 341 Aug 18 12:04 /etc/tramontana/app.confThe file is there. The question is what the program is really looking for:
$ sudo -u svc-tramontana strace -f -e trace=openat,newfstatat -s 200 \
/opt/tramontana/app/tramontana --config /etc/tramontana/app.conf 2>&1 \
| grep -E 'ENOENT|EACCES' | grep -v 'lib\|locale\|gconv'
openat(AT_FDCWD, "/etc/tramontana/app.conf.local", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/tramontana/secrets/db_password", O_RDONLY) = -1 EACCES (Permission denied)There it is, and it was not what it looked like. The configuration file opens fine; the .local that does not exist is optional. The real error is the EACCES on the credential file: the program looks for it in /etc/tramontana/secrets/db_password, but since 06-05 the credential is delivered by systemd in $CREDENTIALS_DIRECTORY, and when run by hand that variable does not exist. The program works correctly under systemd and fails when run directly.
That is the general pattern, and it is why it is worth memorising:
# The one line that solves half of all "it cannot find the file" cases
$ strace -f -e trace=%file <command> 2>&1 | grep -E 'ENOENT|EACCES'The cost, stated plainly
strace works through ptrace, which stops the process at every system call, transfers control to the tracer, and resumes it. That means two context switches per call.
| The process's workload | Typical slowdown with strace |
|---|---|
| CPU-intensive, few calls | ×1.2 – ×2 |
| Mixed | ×5 – ×20 |
| I/O-intensive, many calls | ×50 – ×100 |
A process making 50,000 calls a second can become a hundred times slower. The operational consequences:
- Never run
stracewithout-e trace=or-con a production process under load. If you have to, do it with a time limit:timeout 5 strace -f -c -p PID. - A process you are attached to whose
straceyou kill withSIGKILLcan be left stopped. Always exit withCtrl+C, which does a clean detach. - For production,
perf traceand eBPF do the same thing at a cost one or two orders of magnitude lower. That is the reason they exist.
The obstacle you put there yourself: ptrace_scope
Try attaching to the application's process as your own user:
$ pgrep -u svc-tramontana tramontana
4318
$ strace -p 4318
strace: attach: ptrace(PTRACE_SEIZE, 4318): Operation not permittedIt is not a fault. It is the kernel.yama.ptrace_scope = 1 you set in /etc/sysctl.d/60-hardening.conf in 06-06, which that day came with a warning written beside it for precisely this reason:
The four possible values:
| Value | Who can trace whom |
|---|---|
0 |
Any process can trace any other belonging to the same user |
1 |
Only direct descendants (the value you set) |
2 |
Only processes with CAP_SYS_PTRACE (that is, root) |
3 |
Nobody, not even root. Irreversible until you reboot |
And why the measure is correct, which is the part to understand rather than merely work around: with ptrace_scope = 0, a compromised process can read all the memory of any other process belonging to the same user. That includes the database credential you took such trouble to encrypt in 06-05: it is in the clear in the memory of the process using it. An attacker who managed to run code as svc-tramontana in any process at all could extract it from the application's process without touching a single file. The value 1 closes exactly that route.
The two legitimate ways out:
# Way out A: use sudo. CAP_SYS_PTRACE ignores Yama's restriction.
$ sudo strace -f -c -p 4318
# it works# Way out B: lower it temporarily. Only if you need to trace WITHOUT
# privileges, which is rare. And ALWAYS with the way back guaranteed.
$ sudo sysctl -w kernel.yama.ptrace_scope=0
kernel.yama.ptrace_scope = 0
$ strace -f -c -p 4318 ; sudo sysctl -w kernel.yama.ptrace_scope=1
kernel.yama.ptrace_scope = 1For way out B, the disciplined form is a script with a trap, applying what you saw in 04-06, because a Ctrl+C halfway through would leave the system with the protection turned off:
$ cat ~/scripts/trace_temporarily.sh
#!/usr/bin/env bash
# trace_temporarily.sh - Lowers ptrace_scope, runs the trace, and restores it
# ALWAYS, even if interrupted.
# Usage: trace_temporarily.sh <pid>
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/common.sh
source "${SCRIPT_DIR}/lib/common.sh"
readonly LOG_TAG="trace-temporarily"
restore() {
sudo sysctl -q -w kernel.yama.ptrace_scope="$ORIGINAL_VALUE"
log "ptrace_scope restored to $ORIGINAL_VALUE"
}
main() {
local pid="${1:?usage: trace_temporarily.sh <pid>}"
is_number "$pid" || die 64 "the pid must be a number: $pid"
require_command strace
ORIGINAL_VALUE="$(sysctl -n kernel.yama.ptrace_scope)"
readonly ORIGINAL_VALUE
trap restore EXIT INT TERM
log "lowering ptrace_scope temporarily (it was $ORIGINAL_VALUE)"
sudo sysctl -q -w kernel.yama.ptrace_scope=0
timeout 10 strace -f -c -p "$pid" || true
}
main "$@"In practice, way out A — sudo — is the right one 95% of the time. Way out B only makes sense with tools that do not work well under sudo, and on a production server the honest answer is that you do not lower ptrace_scope: you use eBPF, which does not need ptrace at all. That is another argument in favour of the third section of this lesson.
ltrace and the library level
strace sees the boundary between the program and the kernel. There is another boundary one level up: between the program and the shared libraries it uses.
$ sudo apt install ltrace
$ ltrace -e 'malloc+free' ./program 2>&1 | head -5
program->malloc(1024) = 0x5f8c1a2f5000
program->malloc(4096) = 0x5f8c1a2f5410
program->free(0x5f8c1a2f5000) = <void>It is useful for understanding the behaviour of a program of your own — memory leaks, use of a cryptographic library, calls into libcurl — but it has two serious limits: it only sees calls into dynamic libraries (a static binary is opaque), and its cost is even higher than strace's. In practice it gets little use, and on a server almost none. strace for the boundary with the kernel, perf for the inside of the process.
perf: sampling-based profiling
perf is the Linux kernel's own performance tool, and it operates on a radically different model:
Tracing (strace) |
Sampling (perf) |
|
|---|---|---|
| Method | Intercepts every event | Takes a snapshot N times a second |
| Precision | Exact | Statistical, but sufficient |
| Cost | ×10 – ×100 | 1 – 5% |
| Sees the process's own code | No | Yes |
| Suitable for production | No | Yes |
The key difference is the penultimate row. strace cannot tell you anything about a process burning CPU in its own loop, because there are no system calls there. perf can.
On a VM, some hardware counters are unavailable because the hypervisor does not expose them. The software events (task-clock, context-switches, page-faults) always work.
perf stat: the efficiency snapshot
$ sudo perf stat -p 4318 -- sleep 10
Performance counter stats for process id '4318':
1,284.17 msec task-clock # 0.128 CPUs utilized
3,412 context-switches # 2.657 K/sec
48 cpu-migrations # 37.378 /sec
1,204 page-faults # 0.938 K/sec
3,108,442,190 cycles # 2.421 GHz
1,882,104,556 instructions # 0.61 insn per cycle
412,887,204 branches # 321.52 M/sec
18,442,109 branch-misses # 4.47% of all branches
104,882,441 cache-references # 81.68 M/sec
41,204,882 cache-misses # 39.28% of all cache refs
10.002841 seconds time elapsedHow to read this, line by line, because each one answers a different question:
| Metric | What it means | Reference value |
|---|---|---|
CPUs utilized |
The fraction of one core being consumed | 0.128: the process is practically idle |
context-switches |
How many times it left the CPU | High + low CPU = it is waiting for something |
insn per cycle (IPC) |
Instructions per cycle: the real efficiency | >1 good; <0.5 the processor is waiting on memory |
branch-misses |
Failed branch predictions | <5% normal; >10% heavily branching code |
cache-misses |
Accesses that went out to RAM | <10% good; >30% a locality problem |
And the conclusion from this particular measurement: 0.128 CPUs utilized. The process is not working, it is waiting. The 3,412 context switches in 10 seconds confirm it: it goes on and off the CPU constantly because it blocks. This rules out the CPU as the cause of the latency problem, which was the point of step 2 of the methodology.
The IPC of 0.61 and the 39% cache misses are mediocre, but irrelevant here: with the process at 12.8% of one core, improving its CPU efficiency would not change the latency.
perf top and perf record
# Live: which functions are consuming CPU right now, across the whole system
$ sudo perf top --sort comm,dso
Samples: 84K of event 'cpu-clock:pppH', 4000 Hz
18.42% postgres postgres
11.04% tramontana tramontana
8.87% swapper [kernel.kallsyms]
4.12% tramontana libssl.so.3# Record with call stacks (-g) for 30 seconds
$ sudo perf record -F 99 -g -p 4318 -- sleep 30
[ perf record: Woken up 3 times to write data ]
[ perf record: Captured and wrote 1.842 MB perf.data (2841 samples) ]
$ sudo perf report --stdio --sort overhead,symbol | head -14
# Overhead Symbol
64.12% [k] __x64_sys_connect
18.44% [k] tcp_v4_connect
6.02% [.] tramontana_db_connect
3.18% [.] SSL_connect
1.84% [k] finish_task_switch-F 99 fixes the sampling frequency at 99 Hz. It is a convention with a reason: using 100 Hz risks synchronising with periodic system events that also run at 100 Hz, and skewing the sample. A nearby prime number avoids that.
The [k] and [.] markers distinguish kernel space from user space. Here 82% of the CPU time is in connect and tcp_v4_connect, both in the kernel, and tramontana_db_connect appears in user space. Three different tools pointing at the same place: the process spends its life opening connections.
Flame graphs
A perf report with call stacks is hard to read because the information is hierarchical and the output is flat. The flame graph solves that visually, and it has become the standard in the field.
How to read one, which is the thing to learn:
- The horizontal axis is NOT time. It is the alphabetical grouping of the stacks. A block's width is the proportion of samples in which that function was on the stack.
- The vertical axis is the stack depth. At the bottom the entry point, at the top the function that was executing at that instant.
- What you are looking for are wide plateaux, especially near the top: a wide function at the top is a function where the process spends a lot of time actually executing. A wide function at the bottom with lots of thin towers on top of it is only a place things pass through.
$ git clone --depth 1 https://github.com/brendangregg/FlameGraph ~/FlameGraph
$ sudo perf record -F 99 -g -p 4318 -- sleep 30
$ sudo perf script > output.perf
$ ~/FlameGraph/stackcollapse-perf.pl output.perf > output.folded
$ ~/FlameGraph/flamegraph.pl output.folded > flame-tramontana.svgThe result is an interactive SVG: you open it in a browser, and you can click to zoom into a branch and search by function name.
There is a very useful variant almost nobody knows about: the off-CPU flame graph. The normal one shows where CPU is consumed; the off-CPU one shows where the process is blocked waiting. For a latency problem like ours — a process at 12% CPU — the second is far more informative, and it is built with eBPF rather than with perf:
$ sudo offcputime-bpfcc -df -p 4318 30 > off-cpu.folded
$ ~/FlameGraph/flamegraph.pl --title "Off-CPU" --countname us \
off-cpu.folded > flame-waiting.svgAnd perf trace, which deserves a mention because it solves strace's cost problem:
$ sudo perf trace -p 4318 --duration 5 2>&1 | head -6
0.000 ( 8.412 ms): tramontana/4318 connect(fd: 12, uservaddr: 10.0.2.15:5432) = 0
8.441 ( 0.182 ms): tramontana/4318 sendto(fd: 12, buff: 0x7f2a..., len: 96) = 96
8.712 ( 6.204 ms): tramontana/4318 recvfrom(fd: 12, ...) = 412The same information as strace -T, at a much lower cost because it uses the kernel's event infrastructure instead of ptrace — and, incidentally, ptrace_scope does not affect it.
eBPF: instrumenting the kernel in production
eBPF is the most important change in Linux observability of the last decade. The idea: allow programs of your own to be loaded inside the kernel, which run when an event occurs, with three guarantees that make it safe:
- A verifier analyses the program before loading it and rejects anything that could hang the kernel: unbounded loops, arbitrary memory accesses, disallowed calls.
- It is compiled to native code with a JIT, so it runs at kernel speed.
- It cannot block or modify the kernel's flow; only observe and aggregate.
Why that changes everything: previously, to know the latency of each disk operation you had to choose between an aggregate counter (iostat, which gives the average and hides the tail) or tracing everything (strace, prohibitive). With eBPF you can compute the complete histogram inside the kernel and export only the result. The cost is fractions of a percentage point.
And here the second sysctl from 06-06 shows its consequences:
This stops an unprivileged user loading eBPF programs, and it is correct: the BPF subsystem has had privilege escalation vulnerabilities, and its surface is large. The practical consequence is that every tool in this section is run with sudo, which is exactly what you want on a server.
bpftrace: one line, one answer
bpftrace is a one-line language for eBPF, with syntax inspired by awk — which you already know from Module 3: event { action }.
# 1. Count system calls per process over 10 seconds
$ sudo timeout 10 bpftrace -e '
tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
Attaching 1 probe...
@[systemd-journal]: 412
@[postgres]: 8841
@[tramontana]: 33204
# 2. Latency of the connect() calls, as a histogram
$ sudo timeout 30 bpftrace -e '
tracepoint:syscalls:sys_enter_connect { @start[tid] = nsecs; }
tracepoint:syscalls:sys_exit_connect /@start[tid]/ {
@us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
@us:
[1, 2) 4 |@ |
[2, 4) 12 |@@@@ |
[4, 8) 142 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[8, 16) 128 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[16, 32) 14 |@@@@ |
# 3. Which files a particular process opens, live
$ sudo bpftrace -e '
tracepoint:syscalls:sys_enter_openat /comm == "tramontana"/ {
printf("%s -> %s\n", comm, str(args->filename));
}'
# 4. How many bytes each process writes to disk
$ sudo timeout 20 bpftrace -e '
tracepoint:block:block_rq_issue { @bytes[comm] = sum(args->bytes); }'The histogram in example 2 is the kind of information no other tool gives you easily: 300 calls to connect, with the bulk between 4 and 16 microseconds... hold on. That histogram says microseconds, and strace -c said 8 milliseconds per call. The discrepancy is the clue: the connect call into the kernel is fast; the time goes somewhere else in the connection process. We will come back to this in the case study.
Note the mechanics of example 2, because it is the pattern for measuring latency with eBPF: store nsecs in a map indexed by tid on entry, subtract on exit, and aggregate with hist(). The entire calculation happens inside the kernel; the only thing that leaves for user space is the final histogram.
The bpfcc tools and the question each one answers
bpfcc-tools installs about a hundred and fifty ready-written tools. These are the ones that really get used:
| Tool | The question it answers |
|---|---|
execsnoop-bpfcc |
Which processes are being launched? (short-lived processes ps never sees) |
opensnoop-bpfcc |
Which files are being opened, and which ones fail? |
biolatency-bpfcc |
What is the distribution of disk latency, not the average? |
biosnoop-bpfcc |
Which process is doing each disk operation? |
tcpconnect-bpfcc |
Who is opening outbound connections, and to where? |
tcpaccept-bpfcc |
Who is connecting to my services? |
tcpretrans-bpfcc |
Are there TCP retransmissions? (a real network problem) |
tcplife-bpfcc |
How long do connections last and how many bytes do they move? |
runqlat-bpfcc |
How long do processes wait in the queue for the CPU? |
cachestat-bpfcc |
What is the page cache's hit rate? |
ext4slower-bpfcc |
Which filesystem operations take longer than N ms? |
profile-bpfcc |
Sampling-based profiling, an alternative to perf record |
offcputime-bpfcc |
Where is it blocked waiting? |
funclatency-bpfcc |
How long does a specific kernel function take? |
Two of them deserve a comment for what they add over the classic counters:
# biolatency: the DISTRIBUTION, not the average. An average of 5 ms can hide
# the fact that 1% of the operations take 500 ms, and that 1% is what shows.
$ sudo biolatency-bpfcc -m 30 1
msecs : count distribution
0 -> 1 : 8412 |****************************************|
2 -> 3 : 1204 |***** |
4 -> 7 : 412 |* |
8 -> 15 : 88 | |
16 -> 31 : 12 | |
256 -> 511 : 3 | |Those three events at 256-511 ms appear in no average at all. If they coincide with the slow requests, they are the cause.
# runqlat: how long processes wait to get onto the CPU.
# It is the answer to "the CPU is not saturated but everything is slow".
$ sudo runqlat-bpfcc 10 1
usecs : count distribution
0 -> 1 : 12841 |****************************************|
2 -> 3 : 2104 |****** |
4 -> 7 : 412 |* |A decision table: which tool for which question
The table that summarises the lesson, and the one to come back to when you have a problem in front of you:
| Question | Tool | Cost |
|---|---|---|
| Is any resource saturated? | vmstat, iostat, free, sar (M5) |
None |
| Why did the service fail? | journalctl -u <unit> (M5) |
None |
| Which file is it looking for and not finding? | strace -e trace=%file | grep ENOENT |
High, brief |
| Where does a process's time go? | strace -f -c (first), then perf |
High / low |
| How long does each call take? | strace -T or perf trace |
High / low |
| Which function consumes CPU? | perf top, perf record -g + a flame graph |
Low |
| Is the code efficient (IPC, cache)? | perf stat |
None |
| Where is it blocked waiting? | offcputime-bpfcc + a flame graph |
Low |
| What is the distribution of disk latency? | biolatency-bpfcc |
Very low |
| Who opens connections, and to where? | tcpconnect-bpfcc, tcplife-bpfcc |
Very low |
| Are there short-lived processes I cannot see? | execsnoop-bpfcc |
Very low |
| Is it waiting to get onto the CPU? | runqlat-bpfcc |
Very low |
| Something very kernel-specific | A bespoke bpftrace |
Very low |
| Why did the process crash? | gdb on the dump, coredumpctl |
N/A |
And the rule of order: counters → perf stat → eBPF → strace. From lowest to highest cost, with strace last precisely because it is the most expensive. The opposite intuition — starting with strace because it is the best known — is what produces diagnoses that degrade the very service they are trying to fix.
A note on dumps: if the problem is a crash rather than slowness, the tool is a different one. Remember that in 06-06 you set * hard core 0 in limits.conf so that dumps would not expose secrets; to debug a crash you would have to reverse that temporarily, and systemd's coredumpctl is the modern route.
The Tramontana case: from 400 ms to 40 ms
health_check.sh starts returning 1. The 05-07 baseline says the application responds in 40 ms; now it takes 400.
Step 1: the symptom, measured
$ for i in {1..5}; do
curl -s -o /dev/null -w '%{time_total}\n' http://127.0.0.1:8080/houses
done
0.412844
0.398201
0.421077
0.404118
0.397882
$ grep -c 'ms=[0-9]\{3,\}' /var/log/tramontana/access.log
389Confirmed and reproducible: ~400 ms, not an isolated spike.
Step 2: ruling things out with counters (free)
$ vmstat 2 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 1284412 104882 1841204 0 0 0 12 412 882 3 2 95 0 0
0 0 0 1284188 104882 1841204 0 0 0 8 388 841 2 2 96 0 0
$ iostat -xz 2 3 | grep -A2 'Device'
Device r/s rkB/s w/s wkB/s r_await w_await aqu-sz %util
sda 0.50 8.00 2.00 16.00 0.42 0.88 0.01 0.40
$ free -h | head -2
total used free shared buff/cache available
Mem: 3.8Gi 1.2Gi 1.3Gi 12Mi 1.4Gi 2.4GiThe CPU 95% idle, the disk at 0.4% utilisation, 2.4 GB of memory available. No resource is saturated. This is exactly the scenario the close of 07-01 announced: the counters green and the service performing badly.
Step 3: perf stat confirms it is waiting, not working
$ sudo perf stat -p $(pgrep -u svc-tramontana -f tramontana) -- sleep 10 2>&1 | \
grep -E 'CPUs utilized|context-switches|insn per cycle'
1,284.17 msec task-clock # 0.128 CPUs utilized
3,412 context-switches # 2.657 K/sec
1,882,104,556 instructions # 0.61 insn per cycle12.8% of one core and 3,412 context switches. The process is blocking constantly. The hypothesis becomes: it is waiting for something external. The candidates are disk (ruled out by iostat) and network — that is, PostgreSQL.
Step 4: strace -c locates the time
With sudo, because of ptrace_scope, and with a time limit because of the cost:
$ sudo timeout 10 strace -f -c -p $(pgrep -u svc-tramontana -f tramontana)
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
89.14 2.417882 8062 300 connect
6.02 0.163291 272 600 sendto
3.11 0.084372 140 602 recvfrom
------ ----------- ----------- --------- --------- ----------------300 calls to connect in 10 seconds, at 8 ms each. With about 75 requests in that interval, that works out at roughly 4 new connections per request. An application with a connection pool should not be opening any.
Step 5: eBPF finds the real cause
This is where the discrepancy we left hanging gets resolved:
$ sudo timeout 20 tcplife-bpfcc
PID COMM LADDR LPORT RADDR RPORT TX_KB RX_KB MS
4318 tramontana 10.0.2.15 48812 10.0.2.15 5432 1 3 8.42
4318 tramontana 10.0.2.15 48814 10.0.2.15 5432 1 2 8.11
4318 tramontana 10.0.2.15 48816 10.0.2.15 5432 1 4 8.38
[... 297 more lines ...]Three hundred connections to PostgreSQL, each with 8 ms of life and a few KB. They open, run one query and close. That is a connection pool that is not working.
And the histogram explains the 8 ms that the connect call alone did not account for:
$ sudo timeout 30 bpftrace -e '
tracepoint:syscalls:sys_enter_connect /comm == "tramontana"/ { @i[tid] = nsecs; }
tracepoint:syscalls:sys_exit_connect /@i[tid]/ {
@us_syscall = hist((nsecs - @i[tid]) / 1000); delete(@i[tid]); }'
@us_syscall:
[4, 8) 142 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[8, 16) 128 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |The system call takes microseconds. The 8 milliseconds strace was seeing include everything around the connection: the TCP handshake, and above all PostgreSQL's authentication and session startup. perf report was already hinting at it with SSL_connect in the stack.
$ sudo timeout 20 offcputime-bpfcc -p 4318 -f | sort -k2 -rn | head -3
tramontana;db_connect;SSL_connect;read;schedule 18412042
tramontana;db_query;read;schedule 1204882
tramontana;epoll_wait;schedule 88210418.4 of 20 seconds blocked inside db_connect. Confirmed from a fourth independent tool.
Step 6: the root cause and the fix
The missing piece is a fact you already had. Remember the incident from 05-07: errors.log with active_connections=200 and db_timeout. The I/O saturation was resolved then, but the max_connections=200 stayed where it was:
$ grep -E 'max_connections|pool' /etc/tramontana/app.conf
max_connections=200
$ sudo -u postgres psql -tAc "SHOW max_connections;"
100There is the root cause, and it is configuration, not code: the application believes it can have 200 connections, PostgreSQL only accepts 100. When the pool tries to grow beyond 100, the new connections are refused, the application gives up on the pool and opens direct connections per request, and each one costs 8 ms of setting up and authenticating.
# Fix it: the pool below the real limit, with margin for everything else
$ sudo cp -p /etc/tramontana/app.conf /etc/tramontana/app.conf.bak-$(date +%F)
$ sudo chattr -i /etc/tramontana/app.conf
$ sudo sed -i.bak-$(date +%F) 's/^max_connections=200$/max_connections=80/' \
/etc/tramontana/app.conf
$ sudo chattr +i /etc/tramontana/app.conf
$ sudo diff -u /etc/tramontana/app.conf.bak-$(date +%F) /etc/tramontana/app.conf
@@ -5,7 +5,7 @@
-max_connections=200
+max_connections=80
$ sudo systemctl restart tramontana.serviceStep 7: measure afterwards
$ for i in {1..5}; do
curl -s -o /dev/null -w '%{time_total}\n' http://127.0.0.1:8080/houses
done
0.041882
0.038204
0.042118
0.039877
0.040412
$ sudo timeout 20 tcplife-bpfcc | wc -l
81
$ sudo timeout 10 strace -f -c -p $(pgrep -u svc-tramontana -f tramontana) 2>&1 | \
grep -E 'connect|total'
0.42 0.000841 10 80 connect
100.00 0.198442 1841 12 total
$ ~/scripts/health_check.sh; echo "status: $?"
status: 0From 400 ms to 40 ms. From 300 connections every 10 seconds to 80 in total, which is the size of the pool establishing itself once. And health_check.sh is back to 0.
What makes this diagnosis valid is not any tool in particular, but the convergence of five independent measurements — perf stat, strace -c, tcplife, bpftrace and offcputime — all pointing at the same place, plus a final hypothesis verified by a before-and-after comparison. And the note for the runbook: changing a limit on one side of a client-server relationship without checking the other side is the mistake that caused this, and it happened three modules ago.
Common Mistakes and Tips
- Starting with
strace. It is the best-known tool and the most expensive. The order is counters →perf stat→ eBPF →strace. stracewithout-e trace=or-cin production. A hundred thousand unreadable lines and a service a hundred times slower. If you have to trace in production,timeout 5 strace -f -c -p PID.- Forgetting
-f. Without following children and threads, you lose almost everything in a multithreaded application. - Confusing
strace -c's time with the process's total time.% timeis the proportion inside system calls. If the program burns CPU in its own code, it does not show up there: that is whatperfis for. - Killing
stracewithSIGKILL. It can leave the traced process stopped. Exit withCtrl+C. - Reading every
ENOENTas an error. A normal startup generates dozens of them looking for libraries and locales. Filter out the noise before drawing conclusions. - Lowering
ptrace_scopeand forgetting to restore it. It leaves memory readable across processes belonging to the same user, and with it the secrets. Usesudo, or a script with atrap. - Trusting the average latency.
iostatcan report a 1 ms average while 1% of the operations take 500 ms.biolatency-bpfccshows the distribution, and the tail is what the user notices. - Sampling at 100 Hz. It can synchronise with periodic system events and skew the sample. Use a nearby prime:
-F 99. - Looking for a latency problem in a normal flame graph. The CPU one shows where time is consumed; for a process that is waiting you need the off-CPU one (
offcputime-bpfcc). - Concluding from a single tool. This case was solved with five converging measurements. One alone would have given a plausible and probably incomplete answer.
- A tip on method. Store the diagnostic measurements alongside the 05-07 baseline: what you measured, with what command, and the normal value. Next time, step 2 is thirty seconds instead of twenty minutes.
Exercises
Exercise 1
backup_tramontana.sh used to take 40 minutes and now takes 3 hours, which means the 02:30 backup does not finish before the morning peak — the 05-07 incident all over again. iostat shows the disk at 45% utilisation, a long way from saturation. Design the diagnostic procedure, stating which tool you would use at each step and why, knowing that the backup uses restic over a LUKS-encrypted volume.
Exercise 2
Write a latency_diagnostics.sh script that, given the name of a systemd service, automatically runs steps 2, 3 and 4 of the methodology — counters, perf stat and strace -c — and produces a readable report. It must respect the course's conventions, handle ptrace_scope safely, and leave no residue behind.
Exercise 3
A colleague proposes setting kernel.yama.ptrace_scope = 0 permanently "so we can diagnose without sudo". Write the technical reply: what is gained, what is lost exactly, and what alternative you propose.
Solutions
Solution 1
The key to the question is "45% utilisation", which rules out saturation but does not rule out latency: they are two different things and confusing them is the usual mistake. A disk at 45% can have a queue of slow requests.
# Step 1. The symptom, measured and compared against the baseline
$ sudo journalctl -u tramontana-backup.service --since "7 days ago" \
| grep -E 'Started|Finished|Succeeded'
$ systemd-analyze --no-pager verify tramontana-backup.service
# And the direct source: how long each run takes
$ sudo systemctl show tramontana-backup.service -p ExecMainStartTimestamp \
-p ExecMainExitTimestamp# Step 2. Counters, free, while the backup runs.
# What to look for here: %util is NOT the indicator; w_await and aqu-sz are.
$ iostat -xz 5 6
Device r/s rkB/s w/s wkB/s r_await w_await aqu-sz %util
dm-1 2.00 32.0 84.0 1024.0 1.12 38.42 3.21 45.10
$ vmstat 5 6 # watch the 'wa' column and the 'b' one (blocked processes)
$ mpstat -P ALL 5 3 # look for a core at 100% in 'sy': a sign of encryptionWith a w_await of 38 ms and a %util of 45%, there is already an anomaly: there is latency without saturation, which points to individually slow operations or to a bottleneck somewhere in the stack.
# Step 3. Distribution, not average. It is THE tool for this symptom.
$ sudo biolatency-bpfcc -D 60 1
# -D separates by device: it lets you see whether the problem is in dm-1 (LUKS)
# or in sda (the physical disk underneath)And here is the reasoning specific to this exercise: /srv/tramontana/backups is a LUKS volume on LVM on sda. That is three layers, and the latency has to be attributed to one of them:
| Device | Layer | If the latency is here… |
|---|---|---|
sda |
The physical disk | The problem is the storage; it has nothing to do with the backup |
vg-data/lv-backups |
LVM | Unlikely; LVM adds very little |
dm-1 (backups-encrypted) |
LUKS | Encryption is the bottleneck |
# Step 4. Confirm whether encryption is the bottleneck: the operation burns
# kernel CPU in the kcryptd threads.
$ sudo timeout 30 profile-bpfcc -f 30 | grep -iE 'crypt|aes' | head -5
$ ps -eLo comm,pcpu | grep -E 'kcryptd|restic' | sort -k2 -rn | head -5
$ cryptsetup luksDump /dev/vg-data/lv-backups | grep -E 'Cipher|PBKDF'
# And check whether the hardware accelerates AES. If not, encryption runs in
# software and is between 5 and 10 times slower.
$ grep -o -m1 aes /proc/cpuinfo || echo "NO AES-NI acceleration"# Step 5. Rule out that it is restic rather than the encryption. A process can
# be slow because of I/O or because of CPU, and you need to know which.
$ sudo perf stat -p $(pgrep -f 'restic backup') -- sleep 20 2>&1 \
| grep -E 'CPUs utilized|context-switches'
$ sudo timeout 30 offcputime-bpfcc -p $(pgrep -f 'restic backup') -f \
| sort -k2 -rn | head -5The interpretation, which is what is being asked for:
CPUs utilizedclose to 1.00 andprofile-bpfccshowing AES functions: the bottleneck is software encryption. Possible causes: the VM does not expose AES-NI to the guest, or the CPU does not have it. You check with/proc/cpuinfoand resolve it by enablinghost-passthroughin the VM's configuration (you will see this in 07-04) or by changing the LUKS cipher.- Low
CPUs utilizedandoffcputimeshowing waits on I/O: the bottleneck is the disk. Thenbiolatency -Dsays whether the latency already comes fromsda, and the next step isext4slower-bpfccto see which specific operations are slow. - Very high context switches with both of the above low: contention between
resticand another process.runqlat-bpfccwould confirm it.
And two alternative hypotheses to rule out before touching anything, because they are more likely than a hardware problem:
# a) Has the volume of data grown? A bigger backup takes longer.
$ sudo restic -r /srv/tramontana/backups/restic stats latest
$ du -sh /opt/tramontana/releases/*/ /home/operator/data/
# b) Has deduplication degraded, or is a prune missing?
$ sudo restic -r /srv/tramontana/backups/restic snapshots | wc -l
$ sudo restic -r /srv/tramontana/backups/restic stats --mode raw-dataA restic repository with no forget --prune accumulates data and slows every operation down. If the GFS 14/8/12 retention stopped running, that is the simplest explanation — and checking it is free. Before you diagnose the hardware, rule out the boring stuff.
A final note on method: the ionice set in 05-07 still applies and it is correct that the process should yield I/O. But if the backup no longer fits in its window, ionice stops being enough and you have to attack the cause, not the symptom.
Solution 2
$ cat ~/scripts/latency_diagnostics.sh
#!/usr/bin/env bash
# latency_diagnostics.sh - Guided latency diagnosis of a service.
# Runs steps 2, 3 and 4 of the methodology: counters, perf stat and
# strace -c, and produces a readable report.
# Usage: latency_diagnostics.sh [-d seconds] [-o file] <unit.service>
# Exit: 0 report generated | 64 incorrect usage | 69 a tool is missing
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/common.sh
source "${SCRIPT_DIR}/lib/common.sh"
readonly LOG_TAG="latency-diagnostics"
readonly DEFAULT_DURATION=10
usage() {
sed -n '2,6s/^# \?//p' "$0"
}
section() {
printf '\n===== %s =====\n\n' "$1" >>"$REPORT"
}
main() {
local duration="${TRAMONTANA_DIAG_DURATION:-$DEFAULT_DURATION}"
local output=""
while getopts ":d:o:h" option; do
case "$option" in
d) duration="$OPTARG" ;;
o) output="$OPTARG" ;;
h) usage; return 0 ;;
*) usage >&2; die 64 "invalid option: -$OPTARG" ;;
esac
done
shift $((OPTIND - 1))
local unit="${1:-}"
[[ -n "$unit" ]] || { usage >&2; die 64 "the systemd unit is missing"; }
is_number "$duration" || die 64 "the duration must be a number"
require_command systemctl
require_command vmstat
require_command iostat
# A single temporary directory, cleaned up by trap: the 04-06 convention
local tmp
tmp="$(mktemp -d)"
REPORT="${output:-${tmp}/report.txt}"
readonly REPORT
trap 'rm -rf "$tmp"' EXIT
# The main PID is the source of truth; pgrep might pick up another process
local pid
pid="$(systemctl show "$unit" -p MainPID --value)"
[[ "$pid" =~ ^[0-9]+$ && "$pid" -gt 0 ]] \
|| die 69 "the unit $unit has no active main process"
log "diagnosing $unit (PID $pid) for ${duration}s"
{
printf 'LATENCY DIAGNOSTICS REPORT\n'
printf 'Unit: %s\n' "$unit"
printf 'PID: %s\n' "$pid"
printf 'Machine: %s\n' "$(hostname)"
printf 'Duration: %ss\n' "$duration"
} >"$REPORT"
# ---- Step 2: counters. Zero cost, always run. ----
section "STEP 2 - COUNTERS (the USE method)"
{
printf '# CPU and memory (vmstat)\n'
vmstat 2 3
printf '\n# Disk (iostat -xz)\n'
iostat -xz 2 2 | sed -n '/Device/,$p'
printf '\n# Memory (free -h)\n'
free -h
printf '\n# The process sockets (ss)\n'
ss -tanp 2>/dev/null | grep -F "pid=${pid}," || printf '(none)\n'
} >>"$REPORT" 2>&1
# ---- Step 3: perf stat. ~zero cost, requires root. ----
section "STEP 3 - PERF STAT (working or waiting?)"
if command -v perf >/dev/null 2>&1; then
sudo perf stat -p "$pid" -- sleep "$duration" >>"$REPORT" 2>&1 || \
printf '(perf stat failed: counters unavailable on this VM?)\n' >>"$REPORT"
else
printf '(perf not installed: apt install linux-tools-%s)\n' "$(uname -r)" >>"$REPORT"
fi
# ---- Step 4: strace -c. EXPENSIVE: always with a timeout, summary only. ----
section "STEP 4 - STRACE -c (where the syscall time goes)"
printf 'WARNING: strace slows the process down. Summary only, time-limited.\n\n' >>"$REPORT"
if command -v strace >/dev/null 2>&1; then
# With sudo: it ignores ptrace_scope without touching the sysctl (see 06-06).
# || true because timeout returns 124 when it cuts in, and that is expected.
sudo timeout "$duration" strace -f -c -p "$pid" >>"$REPORT" 2>&1 || true
else
printf '(strace not installed)\n' >>"$REPORT"
fi
# ---- Extra: eBPF if available. Very low cost. ----
if command -v tcplife-bpfcc >/dev/null 2>&1; then
section "EXTRA - CONNECTIONS (tcplife)"
sudo timeout "$duration" tcplife-bpfcc 2>/dev/null \
| awk -v p="$pid" 'NR==1 || $1==p' >>"$REPORT" || true
fi
section "SUGGESTED NEXT STEP"
{
printf 'If CPUs utilized is HIGH -> perf record -g + flame graph\n'
printf 'If CPUs utilized is LOW -> offcputime-bpfcc (it is blocked)\n'
printf 'If connect/sendto dominates -> tcplife-bpfcc, tcpconnect-bpfcc\n'
printf 'If read/write dominates -> biolatency-bpfcc, ext4slower-bpfcc\n'
} >>"$REPORT"
if [[ -n "$output" ]]; then
log "report written to $output"
else
cat "$REPORT"
fi
}
main "$@"$ chmod +x ~/scripts/latency_diagnostics.sh
$ shellcheck ~/scripts/latency_diagnostics.sh && echo "no warnings"
no warnings
$ ~/scripts/latency_diagnostics.sh -d 5 tramontana.service | head -20The design decisions that make this script usable in production rather than a hazard:
MainPIDinstead ofpgrep.systemctl show -p MainPIDgives the process systemd considers the main one.pgrep -f tramontanacould pick up the script itself, agrep, or a process from another release.- The order of the steps is one of increasing cost, and
stracegoes last. If the problem is visible in step 2, you have the answer before paying anything. sudoforstrace, never touchptrace_scope. It is way out A from the lesson, and it is the only acceptable one in a script anybody might run.- A compulsory
timeoutonstrace, with|| truebecausetimeout's exit code 124 is the expected result, not a failure. Without it,set -ewould abort the script just before the conclusions get written. - Graceful degradation. If
perfor eBPF are absent, the report says so and carries on. A diagnostic script that fails because an optional tool is missing is useless precisely when you need it most. - A
mktemp -dwith atrap, following 04-06: it leaves no residue even when interrupted. - The final section points to the next step. The script automates the mechanical steps; the interpretation remains human, and handing the operator the decision tree is more useful than trying to conclude automatically.
Solution 3
Technical reply: the proposal to set
kernel.yama.ptrace_scope = 0What is gained. Being able to run
strace,gdbandperfagainst processes belonging to the same user withoutsudo. In practice this saves typing four characters, because onsrv-tramontanathe application runs assvc-tramontanaand we run asoperator: withptrace_scope = 0we would still needsudo, since the value 0 only allows tracing processes belonging to your own user. The real benefit of the proposal, in our specific case, is zero.What is lost, exactly.
ptraceallows reading and writing all the memory of another process. The memory of the application's process contains, in the clear and necessarily:
- The database password, which in 06-05 we took out of
app.confand encrypted withsystemd-credsprecisely so that it would not be readable.- The TLS private key, if the process loads it.
- The personal data of guests in the requests being served.
With
ptrace_scope = 0, any compromised process running as the same user can extract all of that without touching a single file: with no writes for AIDE to detect, no accesses forauditdto record on the paths we watch, and no trace left in the logs. In other words, it would in practice undo the work of two entire lessons from the previous module.Being specific about our threat model from 06-06: the second most likely threat is a leaked credential, and the fourth is abuse of legitimate access. This proposal opens a direct route for both and closes neither.
Alternatives, in order of preference.
- Use
sudo. It is the right answer 95% of the time.CAP_SYS_PTRACEignores Yama's restriction, it is recorded inauth.log— which is an advantage, not a drawback — and it does not change the system's posture. Cost: five characters.- Use eBPF, which is the better tool.
perf trace,tcplife-bpfcc,biolatency-bpfcc,offcputime-bpfccandbpftracedo not useptraceat all, soptrace_scopedoes not affect them. And they are between ten and a hundred times cheaper, which makes them the only ones genuinely suitable for production. If the underlying motivation is "diagnosing comfortably", this is the technical answer, not lowering a protection.- If tracing without privileges really is needed, there is a middle route: grant
CAP_SYS_PTRACEto the diagnostic binary alone, rather than opening up the whole system:Picking up the capabilities from 05-02. It is still an increase in surface — anybody who can run$ sudo setcap cap_sys_ptrace+ep /usr/bin/stracestracewill be able to trace — but bounded to one binary instead of the entire system. Even so, I do not recommend it here: it does not solve our real case (different users) and it adds a privileged binary to audit.- Lower it temporarily, only during a diagnostic session, with restoration guaranteed by a
trap(~/scripts/trace_temporarily.sh). Acceptable in the lab; in production it is unnecessary given point 2.Recommendation. Keep
kernel.yama.ptrace_scope = 1. The proposal brings no benefit at all in our configuration — we would still needsudo— and it opens a route for extracting credentials and personal data that leaves no trace. If the underlying problem is friction when diagnosing, the solution is to install and learnbpfcc-toolsandbpftrace, which will also let us diagnose in production and under load, somethingstracenever allows.I am adding two notes for the runbook: the value
1is documented in/etc/sysctl.d/60-hardening.confwith a comment that already warned of this side effect; and it is worth recording here that the diagnosis of this week's latency incident was resolved entirely with eBPF andsudo strace, with no need to touch the parameter.
Conclusion
You now know how to look inside a running process. You have three families of tools and — most importantly — the criterion for choosing between them: the counters from Module 5 answer how much and are free, perf stat says whether a process is working or waiting at a negligible cost, eBPF instruments the kernel in production with histograms that reveal the tails the averages hide, and strace gives the exact answer at the price of slowing the process down by up to a hundred times. The order counters → perf stat → eBPF → strace is the practical lesson to take away, along with the one line that solves half of all configuration mysteries: strace -e trace=%file | grep ENOENT.
You have solved a complete case following the methodology: a measured, reproducible symptom, hypotheses ruled out with free counters, and five independent measurements converging on the same cause — a max_connections=200 against a max_connections=100, a mismatch that had been sitting there for three modules — with the fix verified from 400 ms to 40 ms. And you have run into the consequence of your own hardening: the ptrace_scope = 1 that prevents tracing without privileges, which turns out to be right, because a process's memory contains in the clear exactly the secrets you took such trouble to encrypt. That the correct solution to that friction is to use eBPF, and not to lower the protection, is the kind of decision that distinguishes an administrator from somebody following recipes.
Notice one detail of this diagnosis that points to what comes next: the cause was a configuration value, and you found it by comparing two sides of a single relationship. That is the territory of the next lesson, with one important difference: here the value was in the application, and now you are going to touch the kernel's. In lesson 07-03: Linux Kernel Tuning you will learn to adjust sysctl for performance — not for security, which you already did in 06-06: virtual memory with swappiness and the dirty page thresholds, the network with somaxconn and the BBR congestion control, the file and process limits, the I/O scheduler and why an NVMe wants none, and the transparent huge pages that every database asks you to turn off. You will also see kernel modules, dkms, and an honest answer to whether compiling the kernel makes sense on a production server. And you will do it with the rule that governs the whole business and that this lesson has already taught you to apply: you do not tune what you have not measured, one change at a time, and measure before and after. Remember that the default somaxconn is still sitting there, and that that errors.log with active_connections=200 had more than one cause.
Linux Course: From Beginner to System Administrator
Module 1: Introduction to Linux
- What Is Linux?
- History of Linux
- Linux Distributions
- Installing Linux
- First Contact with the System
- The Linux File System Structure
Module 2: Basic Linux Commands
- Introduction to the Command Line
- Getting Help and System Documentation
- Navigating the File System
- File and Directory Operations
- Viewing and Editing Files
- Hard and Symbolic Links
- File Permissions and Ownership
Module 3: Advanced Command-Line Skills
- The Shell Environment: Variables, Aliases and History
- Using Wildcards and Regular Expressions
- Searching Files and Content: find, locate and grep
- Pipes and Redirection
- Text Processing: cut, sort, uniq, sed and awk
- Process Management
- Scheduling Tasks with Cron
- Networking Commands
Module 4: Shell Scripting
- Introduction to Shell Scripting
- Variables and Data Types
- Script Input, Output and Arguments
- Control Structures
- Functions and Libraries
- Debugging and Error Handling
- Production Scripts: Best Practices
Module 5: System Administration
- User and Group Management
- sudo and Special Permissions
- Package Management
- Disk Management
- systemd and Service Management
- System Logs: journald and syslog
- System Monitoring and Performance Tuning
- Backup and Restore
Module 6: Networking and Security
- Network Configuration
- SSH and Remote Access
- Firewalls and Perimeter Security
- Intrusion Detection Systems
- Secrets Management and TLS Certificates
- Securing Linux Systems
Module 7: Advanced Topics
- The Boot Process and System Recovery
- Advanced Diagnostics: strace, perf and eBPF
- Linux Kernel Tuning
- Virtualization with Linux
- Linux Containers and Docker
- Automation with Ansible
- High Availability and Load Balancing
