Són les 03:12. El telèfon vibra: «L'API de Meteora respon lenta. Clients queixant-se des de fa uns minuts.» No hi ha més informació, ningú no sap què ha canviat i la persona que va escriure l'agregador és de vacances. Tens accés per SSH a meteo-01 i la resta depèn de tu.
Aquesta lliçó és diferent de totes les anteriors. No introdueix conceptes nous: fa servir tots els que has après. Recorreràs un incident complet, amb les sortides reals de cada ordre, les hipòtesis que es descarten, les dues que resulten certes i una tercera que apareix sense buscar-la i que canvia del tot la naturalesa del problema. Veuràs com la teoria del mòdul 2 sobre patrons d'accés al disc es converteix en una línia d'iostat, com la jerarquia de bloqueigs del mòdul 3 es converteix en un fil penjat a /proc/<pid>/stack, i com el mòdul 5 obliga a aturar la investigació de rendiment en sec.
Al final tancarem el curs: un mapa del que hem recorregut, els camins que s'obren i com continuar practicant.
Contingut
- El guió d'actuació
- Primera fase: els 60 segons inicials
- Segona troballa: l'
agregadori el patró d'accés - Mitigació davant de solució de fons
- Tercera troballa: fils en estat
Dque no avancen - L'interbloqueig, la seva correcció i la prova
- Quarta troballa, inesperada: l'incident canvia de naturalesa
- Post mortem sense culpables
- Exercicis finals
- Tancament del curs
El guió d'actuació
Abans d'escriure una sola ordre, tres decisions. Costen trenta segons i canvien el resultat de l'incident.
Primera: acotar el símptoma amb dades, no amb impressions. «Va lenta» no permet comprovar si alguna cosa millora. Necessites un número i un instant.
awk '$4 ~ /03:0[0-9]|03:1[0-9]/ {print $NF}' /var/log/meteora/meteo-api.log \
| sort -n | awk '{a[NR]=$1} END {printf "n=%d p50=%.3f p95=%.3f p99=%.3f\n", NR, a[int(NR*.5)], a[int(NR*.95)], a[int(NR*.99)]}'
# n=41208 p50=0.089 p95=2.914 p99=6.102Compara-ho amb la línia base d'un dimarts normal (p50=0.031 p95=0.104 p99=0.198) i ja tens el símptoma quantificat: el p99 s'ha multiplicat per 30. Repetint la consulta minut a minut, la degradació comença entre les 03:04 i les 03:06. Aquest instant és la teva àncora: tot el que investiguis es correlaciona amb ell.
Segona: decidir si mitigues o investigues primer. Són objectius en tensió. Mitigar restaura el servei però destrueix evidències: si reinicies meteo-api, l'estat que explica l'errada desapareix. Investigar preserva les proves però allarga la caiguda. El criteri és l'impacte:
| Situació | Què fer primer |
|---|---|
| Servei caigut del tot, amb dany creixent | Mitigar, capturant abans el que és més volàtil |
| Degradat però funcionant, com aquí | Investigar 10-15 minuts, després mitigar |
| Sospita de compromís de seguretat | Preservar, contenir i escalar |
A les 03:12 l'API respon, amb una latència dolenta però sense errors massius. Decideixes investigar, amb un límit de temps explícit: quinze minuts.
Tercera: anotar-ho tot amb marca de temps. Obre un quadern de l'incident i desa-hi cada sortida:
mkdir -p /var/tmp/incident-2026-08-31
export REG=/var/tmp/incident-2026-08-31/bitacola.log
anota() { printf '\n===== %s : %s =====\n' "$(date -Is)" "$*" >> "$REG"; }
anota "Inici. Símptoma: p99 6,1 s (base 0,2 s) des de ~03:05"Per què això importa tant. D'aquí a tres hores no recordaràs si l'aqu-sz era 18 o 27, i el post mortem depèn d'aquests números. A més, si l'incident acaba sent de seguretat —com passarà—, la bitàcola es converteix en part de la cadena de custòdia de 05-04.
Primera fase: els 60 segons inicials
Executes la llista de 07-03 sencera, per ordre, sense saltar-te'n res.
Què descarta. Res encara, però orienta: 28 de càrrega en una màquina de 8 nuclis, amb la mitjana d'1 minut molt per damunt de la de 15. El problema és recent i està creixent. No hi va haver reinici (41 dies d'uptime), així que descartem una arrencada fallida.
dmesg -T | tail -20 | tee -a "$REG"
# [Mon Aug 31 02:41:03 2026] md0: recovery done.
# [Mon Aug 31 03:05:11 2026] meteo-api[1834]: segfault at 0 ip ... (no apareix)Què descarta. No hi ha morts per OOM, no hi ha errors de dispositiu, no hi ha RAID degradat, no hi ha segfault. Això elimina de cop les tres causes catastròfiques més freqüents. La línia de recovery done a les 02:41 és d'una resincronització del RAID ja acabada: hauria pogut ser la causa, però va acabar 24 minuts abans del símptoma. Anotada com a no descartada del tot, perquè tota coincidència temporal mereix revisió.
vmstat 1 5 | tee -a "$REG"
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 1 31 131072 402112 9104 5218836 0 0 16384 1024 5210 9821 4 3 4 89 0
# 2 29 131072 399820 9104 5219004 0 0 15872 896 5104 9740 3 2 5 90 0Aquí hi ha la primera troballa gran. r=1 (un sol procés llest per executar) davant de b=31 (trenta-un processos bloquejats en E/S ininterrompible). si/so a zero: no és memòria. us+sy al voltant del 6 % amb wa al 89 %: no és CPU. La càrrega de 28 s'explica íntegrament pels processos en estat D de 02-01, que a Linux —i només a Linux— compten a la càrrega mitjana.
mpstat -P ALL 1 3 | tail -10 | tee -a "$REG"
# CPU %usr %nice %sys %iowait %irq %soft %steal %idle
# all 3,6 1,1 2,4 89,4 0,0 0,4 0,0 3,1
# 0 3,9 0,8 2,2 90,1 0,0 0,6 0,0 2,4
# 3 3,1 1,4 2,7 88,9 0,0 0,3 0,0 3,6Què descarta. El %iowait està repartit per igual entre els vuit nuclis, així que no és un nucli saturat ni una interrupció mal distribuïda (02-07). I %steal a zero descarta l'hipervisor (06-01): ningú no ens està robant CPU.
free -m | tee -a "$REG"
# total usada lliure compartida mem/cau disponible
# Mem: 16037 9422 402 912 5218 6104
ss -s | head -3 | tee -a "$REG"
# TCP: 1421 (estab 1188, closed 190, orphaned 0, timewait 189)
cat /proc/pressure/{cpu,io,memory} | tee -a "$REG"
# cpu some avg10=6.11 avg60=4.02 avg300=2.10
# io some avg10=94.28 avg60=78.55 avg300=41.09
# io full avg10=71.62 avg60=58.31 avg300=28.44
# memory some avg10=0.00 avg60=0.00 avg300=0.00Què descarten. Un disponible de 6,1 GB i una pressió de memòria zero: la memòria queda definitivament fora. Les connexions establertes són al rang normal. I PSI és concloent: un io full avg10 del 71,6 % significa que en els últims deu segons la màquina n'ha passat set de cada deu sense poder progressar per esperar el disc, mentre que la pressió de CPU és residual.
Conclusió de la primera fase, a les 03:17: el coll d'ampolla és l'E/S de disc. N'hi ha hagut prou amb tres minuts i s'han descartat amb dades la CPU, la memòria, l'intercanvi, la xarxa, l'hipervisor i una errada de maquinari. Anota-ho i continua.
Segona troballa: l'agregador i el patró d'accés
iostat -xz 1 3 | tail -12 | tee -a "$REG"
# Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s r_await w_await aqu-sz rareq-sz %util
# md0 4102,0 38,0 16408,0 912,0 0,0 2,0 52,10 3,04 27,40 4,00 99,90
# sda 2054,0 19,0 8216,0 456,0 0,0 1,0 51,88 3,01 13,72 4,00 99,80
# sdb 2048,0 19,0 8192,0 456,0 0,0 1,0 52,33 3,07 13,68 4,00 99,90Lectura completa d'aquesta sortida, que és el cor del diagnòstic. L'amplada de banda és ridícula: 16 MB/s, una cosa que qualsevol disc de fa vint anys donaria sense despentinar-se. Però són 4.102 operacions per segon, i rareq-sz està clavat a 4,00 KB: peticions de la mida mínima. rrqm/s és zero, és a dir, el planificador d'E/S (02-05) no pot fusionar res, cosa que només passa quan els blocs demanats no són contigus. És la signatura exacta de la lectura aleatòria.
Un RAID 1 de dos discos mecànics dona de l'ordre de 150-200 IOPS aleatòries per disc, unes 300-400 en lectura sumant-los tots dos perquè el mirall reparteix les lectures. N'estem demanant més de deu vegades aquesta capacitat. D'aquí l'aqu-sz=27,4 —vint-i-set peticions esperant de mitjana, la saturació d'USE— i el r_await=52 ms. El %util del 99,9 % és cert però poc informatiu: el que fa mal és la cua.
Qui ho provoca?
pidstat -d 1 3 | tee -a "$REG"
# UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
# 990 1834 412,00 88,00 0,00 41 meteo-api
# 990 9127 15984,00 804,00 0,00 2914 agregadorQuè demostra. L'agregador està llegint gairebé 16 MB/s —el total del dispositiu— i acumula un iodelay enorme: és ell qui satura el disc. meteo-api tot just llegeix, però el seu iodelay de 41 indica que també està esperant, i aquesta espera és la latència que pateix l'usuari. Tenim culpable i víctima.
systemctl status agregador.service | head -8 | tee -a "$REG"
# Active: active (running) since Mon 2026-08-31 03:00:14 CEST; 17min ago
# CGroup: /system.slice/agregador.service └─9127 /usr/local/bin/agregador --hora-anteriorVa arrencar a les 03:00:14 pel seu temporitzador i porta 17 minuts corrent, quan normalment en triga dos. Encaixa perfectament amb l'inici del símptoma a les 03:05.
cat /proc/9127/io | tee -a "$REG"
# rchar: 18402144256
# read_bytes: 17962827776
filefrag -v /var/lib/meteora/lectures/2026-08-30.dat | tail -3 | tee -a "$REG"
# ...
# /var/lib/meteora/lectures/2026-08-30.dat: 5314 extents foundLa troballa definitiva. El fitxer d'un dia ocupa 17.280.000 bytes, uns 16,5 MiB, i hauria de cabre en uns pocs extents contigus. En té 5.314. I read_bytes diu que l'agregador ha llegit 17,9 GB del disc per processar un fitxer de 16,5 MB: està rellegint el mateix fitxer més de mil vegades, o accedint-hi de manera completament desordenada.
Per què el patró d'accés importa tant, que és la teoria de 02-05 i 04-05 feta carn. En un disc mecànic, una lectura seqüencial de 16,5 MB és un sol posicionament del capçal seguit d'una transferència contínua: uns 0,15 segons. Els mateixos 16,5 MB llegits com a 4.200 peticions aleatòries de 4 KB són 4.200 posicionaments d'uns 10 ms cadascun: 42 segons, gairebé tres-centes vegades més, amb exactament els mateixos bytes llegits. El disc no és lent; el patró és dolent. I la fragmentació en 5.314 extents converteix fins i tot una lectura «seqüencial» del fitxer en una cosa semblant a l'accés aleatori, perquè els blocs lògicament contigus estan físicament dispersos: és la conseqüència que l'ingestor hagi anat afegint al fitxer durant 24 hores mentre el sistema de fitxers assignava espai on podia.
Mitigació davant de solució de fons
Són les 03:24 i cal restaurar el servei. Distingeix sempre les dues coses.
Mitigació immediata (minuts, reversible, no arregla la causa):
ionice -c 3 -p 9127 # classe idle: només llegeix si ningú més no vol el disc
anota "Aplicat ionice -c3 al PID 9127 de l'agregador"Què fa i per què funciona. ionice -c 3 mou el procés a la classe idle del planificador d'E/S: les seves peticions només s'atenen quan no n'hi ha cap altra de pendent. meteo-api deixa de competir en igualtat de condicions i la seva latència ha de caure immediatament.
Verifica, sempre, que la mitigació funciona:
sleep 60; iostat -xz 1 3 | grep md0
# md0 3980,0 36,0 15920,0 864,0 0,0 2,0 11,40 2,90 6,10 4,00 99,70
awk '$4 ~ /03:2[5-9]/ {print $NF}' /var/log/meteora/meteo-api.log | sort -n \
| awk '{a[NR]=$1} END {printf "p99=%.3f\n", a[int(NR*.99)]}'
# p99=0.940Resultat parcial. r_await baixa de 52 a 11 ms, aqu-sz de 27 a 6, i el p99 de l'API passa de 6,1 s a 0,94 s. Millora enorme… però la línia base era 0,198 s. Continua havent-hi alguna cosa malament, i aquest residu és el que portarà a la tercera troballa. Anotar-ho és fonamental: el parany clàssic de l'incident és donar per tancat el cas tan bon punt millora prou.
Mitigació duradora, aplicant els cgroups de 06-02 des de la unitat, perquè no depengui que algú executi ionice a mà:
[Service]
IOSchedulingClass=idle
IOWeight=10
IOReadBandwidthMax=/dev/md0 20M
MemoryMax=1G
CPUWeight=20systemctl daemon-reload
systemctl show agregador.service -p IOWeight -p IOSchedulingClass
cat /sys/fs/cgroup/system.slice/agregador.service/io.max # el límit, tal com el veu el nucliQuè aconsegueix. IOSchedulingClass=idle fa permanent el que feia l'ionice; IOWeight=10 davant de l'IOWeight=200 de meteo-api reparteix vint a un quan tots dos competeixen; i IOReadBandwidthMax posa un sostre dur traduït a io.max al cgroup del servei. És el mateix mecanisme del nucli que limita un contenidor, aplicat a un servei natiu. Nota important: MemoryMax=1G acota a més el dany d'una eventual fuita, matant només l'agregador en lloc de deixar que l'OOM killer triï víctima (02-04).
Solució de fons, que no es fa a les tres de la matinada però s'anota com a acció del post mortem:
| Problema real | Solució de fons | Per què |
|---|---|---|
L'agregador rellegeix el fitxer mil vegades |
Un sol recorregut seqüencial acumulant a memòria | Converteix 42 s de posicionaments en 0,15 s de transferència |
| Fitxers en 5.314 extents | Preassignar amb fallocate en crear el fitxer del dia |
El sistema de fitxers reserva espai contigu d'una vegada |
| Lectures aleatòries sobre dades històriques | Índex per hora, o format columnar | Llegir només el que cal |
| Competeix amb el servei en hores de trànsit | Executar-lo sobre una rèplica, o a una hora vall | Elimina la competència en origen |
| Ningú no se'n va assabentar fins que es van queixar clients | Alerta sobre io full avg60 > 20 % |
Detecció abans que l'usuari |
Tercera troballa: fils en estat D que no avancen
Són les 03:31. El disc ja no està saturat, però el p99 continua a 0,94 s, gairebé cinc vegades la línia base. Tornes a mirar meteo-api.
ps -eLo pid,tid,stat,wchan:24,comm | awk '$1==1834' | tee -a "$REG"
# 1834 1834 Sl ep_poll meteo-api
# 1834 1841 Sl futex_wait_queue meteo-api
# 1834 1842 D flock_lock_inode_wait meteo-api
# 1834 1843 D flock_lock_inode_wait meteo-api
# 1834 1844 D flock_lock_inode_wait meteo-api
# 1834 1845 Sl futex_wait_queue meteo-apiQuè revela. ps -eLo llista fils (-L), no processos: sense aquesta opció no veuries res de tot això (03-02). Tres fils són en estat D i el camp wchan —la funció del nucli on dormen— diu exactament què esperen: flock_lock_inode_wait, és a dir, un bloqueig de fitxer de 04-04. I dos més són a futex_wait_queue, esperant un mutex d'espai d'usuari (03-04).
for t in 1842 1843 1844; do echo "--- TID $t"; cat /proc/1834/task/$t/stack; done | tee -a "$REG"
# --- TID 1842
# [<0>] flock_lock_inode_wait+0x11e/0x150
# [<0>] sys_flock+0x14a/0x1a0
# [<0>] do_syscall_64+0x5c/0xc0Confirmació des del nucli. La pila del fil dins del nucli confirma que és dins de la crida flock() i no en un altre lloc. És la mateixa tècnica que vas aprendre a 03-06 per diagnosticar un procés encallat en D.
lsof -p 1834 | grep -E 'meteora|lock' | tee -a "$REG"
# meteo-api 1834 meteora 7u REG 9,0 17280000 2621441 /var/lib/meteora/lectures/2026-08-31.dat
# meteo-api 1834 meteora 9u REG 9,0 0 2621509 /run/meteora/cache.lock
# meteo-api 1834 meteora 11u REG 9,0 4194304 2621602 /var/log/meteora/meteo-api.log
cat /proc/locks | grep -E '2621441|2621509' | tee -a "$REG"
# 12: FLOCK ADVISORY WRITE 1834 09:00:2621509 0 EOF
# 13: FLOCK ADVISORY WRITE 9127 09:00:2621441 0 EOFEl quadre complet. /proc/locks és la taula de bloqueigs del nucli, i aquí hi ha dos protagonistes: el PID 1834 (meteo-api) té pres el bloqueig de la memòria cau (cache.lock), i el PID 9127 (agregador) té pres el del fitxer de dades. Cadascun espera el que té l'altre.
sudo gdb -p 1834 -batch -ex 'thread apply all bt' 2>/dev/null | grep -A4 'Thread 4' | tee -a "$REG"
# Thread 4 (Thread 0x7f2a... (LWP 1842)):
# #0 0x00007f2a... in flock () from /lib/x86_64-linux-gnu/libc.so.6
# #1 0x000055c1... in bloquejar_fitxer_lectures () at magatzem.c:214
# #2 0x000055c1... in refrescar_cache_des_de_disc () at cache.c:96
# #3 0x000055c1... in atendre_consulta () at api.c:341I aquí hi ha la causa arrel. La pila d'usuari delata l'ordre real d'adquisició: refrescar_cache_des_de_disc() pren primer el bloqueig de la memòria cau i després demana el del fitxer. Però la jerarquia acordada a 03-06 per a tot el sistema és:
configuració → memòria cau → fitxer → registre
Un moment: aquest codi respecta la jerarquia (memòria cau abans que fitxer). Qui la viola és l'altre extrem. Mirant l'agregador:
sudo gdb -p 9127 -batch -ex bt 2>/dev/null | head -5 | tee -a "$REG"
# #0 0x00007f4b... in flock () from /lib/x86_64-linux-gnu/libc.so.6
# #1 0x000055aa... in bloquejar_cache () at cache.c:58
# #2 0x000055aa... in bolcar_mitjanes () at agregador.c:187Confirmat: interbloqueig per violació de la jerarquia. L'agregador va prendre primer el bloqueig del fitxer i ara demana el de la memòria cau; meteo-api va prendre el de la memòria cau i demana el del fitxer. És el cicle d'espera circular, la quarta condició de Coffman, en la seva forma més pura i amb només dos participants. gdb -batch amb -ex 'thread apply all bt' no és destructiu si es fa servir amb cura, però atura el procés mentre s'executa: fes-lo servir breument i sabent que hi afegeixes latència.
Nota important sobre per què no s'havia vist abans: amb l'agregador acabant en dos minuts, la finestra d'encavalcament era mínima. En allargar-se a 17 minuts per la saturació de disc, la probabilitat de coincidència es va disparar. El primer problema en va destapar un segon, que portava mesos latent. És un patró habitual: els interbloqueigs rars es tornen freqüents quan alguna cosa alenteix el sistema.
L'interbloqueig, la seva correcció i la prova
Mitigació immediata, perquè el cicle no es trenca sol:
anota "Interbloqueig confirmat 1834<->9127. S'acaba l'agregador per trencar el cicle."
kill -TERM 9127
sleep 5; ps -p 9127 || echo "agregador acabat"
cat /proc/locks | grep -E '2621441|2621509'
# 12: FLOCK ADVISORY WRITE 1834 09:00:2621509 0 EOF (i l'allibera de seguida)Per què matar l'agregador i no meteo-api. És la recuperació per terminació de 03-06, triant la víctima amb dos criteris: l'agregador és reexecutable —el seu temporitzador el tornarà a llançar, i amb Persistent=true no es perd l'execució— mentre que reiniciar meteo-api tallaria 1.188 connexions establertes. A més, SIGTERM en lloc de SIGKILL li dona l'oportunitat de tancar netament, evitant exactament els fitxers truncats que detectaria la comprovació mida % 24 != 0 de 07-01.
awk '$4 ~ /03:4[2-9]/ {print $NF}' /var/log/meteora/meteo-api.log | sort -n \
| awk '{a[NR]=$1} END {printf "n=%d p50=%.3f p99=%.3f\n", NR, a[int(NR*.5)], a[int(NR*.99)]}'
# n=39004 p50=0.033 p99=0.204Servei restaurat a les 03:44: p99 de 0,204 s, indistingible de la línia base de 0,198 s. I ara, la correcció de fons, que és de codi:
/* MALAMENT — agregador.c:187, viola la jerarquia: fitxer → memòria cau */
flock(fd_fitxer, LOCK_EX);
flock(fd_cache, LOCK_EX); /* <-- ordre invertit */
/* BÉ — jerarquia global: configuració → memòria cau → fitxer → registre */
flock(fd_cache, LOCK_EX);
flock(fd_fitxer, LOCK_EX);I la prova que ja no passa, que és la part que gairebé tothom es salta:
# 1. Prova d'estrès dirigida en preproducció: forçar l'encavalcament 500 vegades
for i in $(seq 1 500); do
systemctl start agregador.service &
curl -s -o /dev/null "https://meteo-pre/v1/lectures?estacio=EST-0142&refresh=1" &
wait
done
# 2. Verificació automàtica: cap fil en D esperant flock
watch -n5 'ps -eLo stat,wchan:24,comm | grep -c "^D.*flock"'
# 3. Comprovació estàtica permanent: un sol punt d'adquisició
grep -rn 'flock(' src/ | grep -v 'bloquejos.c' # ha d'estar buitPer què així. La primera prova reprodueix l'escenari que en producció era rar; si l'interbloqueig persistís, apareixeria en uns pocs intents. La segona és una comprovació contínua barata que es pot convertir en mètrica. La tercera és la més valuosa a llarg termini: centralitzar tota adquisició de bloqueigs en un únic mòdul que els prengui sempre en l'ordre de la jerarquia converteix la disciplina en una cosa que el compilador i una regla de revisió poden vigilar, en lloc de dependre que cada programador recordi l'acord. És la prevenció de 03-06 portada a la pràctica.
Quarta troballa, inesperada: l'incident canvia de naturalesa
Són les 03:52. El servei està bé i estàs reunint material per al post mortem. Revises si l'agregador va deixar restes:
T'atures. Aquest fitxer té el bit setuid (rws), pertany a root, té nom ocult i data de les 02:51. Res del sistema de Meteora no crea això. I llavors comproves l'altra cosa que t'havia estranyat:
awk '$4 ~ /02:[0-9][0-9]/ {split($4, t, ":"); print t[2]":"t[3]}' \
/var/log/meteora/meteo-api.log | uniq -c | awk '$1 < 50'
# 0 02:47
# 0 02:58
grep -c . /var/log/meteora/meteo-api.log
journalctl --since '02:40' --until '03:00' | tail -20Hi ha un buit d'onze minuts (02:47-02:58) en un registre que escriu milers de línies per minut. Un servei que estigués bloquejat deixaria menys línies, no cap. Un buit exacte i net en un fitxer de registre és, fins que es demostri el contrari, manipulació.
Aquí l'incident deixa de ser de rendiment. Dos indicadors independents —un binari setuid de root aparegut a les 02:51 a /tmp i un buit als registres que l'envolta— apunten a un possible compromís. A partir d'aquest punt, tot el que has après al mòdul 7 se subordina al que has après al mòdul 5.
El primer és el que NO es fa:
- No executis el binari, ni tan sols amb
--helpo en una màquina «de proves». És setuid de root. - No l'esborris. És la prova principal.
- No reiniciïs la màquina. Perdries tota la memòria, les connexions i els processos, que és justament el més valuós.
- No continuïs «investigant el rendiment». Cada ordre que executes modifica temps d'accés, entrades del diari i historial de shell, i contamina l'escena.
- No avisis pel canal que podria estar compromès. Si l'atacant hi té accés, llegirà els teus missatges.
Preservar per ordre de volatilitat (05-04), del més efímer al més durador:
| Ordre | Què | Com |
|---|---|---|
| 1 | Memòria RAM | Bolcat amb LiME o avml a un destí extern |
| 2 | Estat de processos i xarxa | ps -eLf, ss -tanp, lsof -n, /proc/<pid>/maps |
| 3 | Connexions i taula ARP | ss -tunap, ip neigh |
| 4 | Discos | Imatge bit a bit amb dd, i hash abans i després |
| 5 | Registres remots i còpies | Els que ja són fora de la màquina |
# Metadades del fitxer SENSE executar-lo ni alterar-lo
stat /tmp/.sysupd | tee -a "$REG"
sha256sum /tmp/.sysupd | tee -a "$REG"
# Còpia forense dels registres i de l'evidència, amb integritat verificable
tar -czf - /var/log/meteora /var/log/auth.log /tmp/.sysupd \
| tee /mnt/evidencies/meteo-01-$(date +%s).tgz | sha256sum | tee -a "$REG"
# Context del sistema
last -F | head -20 | tee -a "$REG"
ausearch -ts 02:40 -te 03:00 -m EXECVE 2>/dev/null | tee -a "$REG"
find / -xdev -perm -4000 -newermt '2026-08-30' -ls 2>/dev/null | tee -a "$REG"Què aporta cadascun. stat dona els tres temps de l'inode sense obrir el fitxer. El hash SHA-256 permet demostrar després que l'evidència no es va alterar: és el fonament tècnic de la cadena de custòdia. last -F mostra els inicis de sessió amb data completa. ausearch consulta auditd —si estava actiu, i per això s'instal·la abans de necessitar-lo— per les execucions d'aquella finestra. I el find busca altres fitxers setuid recents a tot el sistema, perquè un atacant rarament en deixa només un.
Contenir sense destruir, en aquest ordre:
- Aïllar la xarxa mantenint la màquina engegada: regles nftables que només permetin el teu accés d'administració. Apagar-la destrueix la memòria; desconnectar-la del tot també pot alertar l'atacant.
- No canviar credencials encara si això avisés l'intrús, llevat d'indicació de qui coordini la resposta.
- Escalar immediatament: responsable de seguretat, propietari del servei, direcció, i assessoria jurídica i de compliment. Si hi va haver accés a dades personals, a la UE el RGPD marca terminis de notificació de 72 hores que comencen a córrer des del coneixement del fet, i aquesta decisió no la pren qui és a la consola a les quatre del matí.
- Canviar a un canal de comunicació fora de banda i documentar qui sap què i des de quan.
Advertència expressa. Aquest relat és material didàctic. Una resposta a incidents real ha de seguir el procediment formal de la teva organització i comptar amb assessorament legal. Les decisions sobre preservació de proves, notificació a autoritats i clients, comunicació pública i eventual denúncia tenen conseqüències jurídiques i contractuals que excedeixen el que és tècnic. Si la teva organització no té aquest procediment escrit, redactar-lo abans del pròxim incident és més valuós que qualsevol eina.
I per què s'atura aquí la investigació de rendiment. Per tres raons sòlides. Primera, contaminació d'evidències: cada ordre altera l'escena i pot inutilitzar l'anàlisi forense. Segona, canvi de prioritat: una latència alta costa diners, un compromís pot costar les dades de milers de persones. I tercera, canvi d'hipòtesi: si hi ha un intrús, tot el que has diagnosticat es torna sospitós —l'agregador es va alentir sol, o algú el va forçar?, el buit del registre tapa el moment en què es va col·locar el binari?—. No saps si el rendiment va ser la causa, la conseqüència o la cortina de fum. Investigar rendiment sobre un sistema possiblement compromès és construir sobre sorra.
Post mortem sense culpables
Un post mortem sense culpables (blameless) parteix d'una premissa demostrada: les persones actuen raonablement amb la informació que tenen en aquell moment. Si l'objectiu és assenyalar algú, la gent amaga informació i la mateixa errada torna a passar. Si l'objectiu és entendre el sistema, s'aprèn.
Cronologia (totes les hores en CEST del 31/08/2026):
| Hora | Fet |
|---|---|
| 02:41 | Acaba una resincronització del RAID (descartada com a causa) |
| 02:47-02:58 | Buit inexplicat a meteo-api.log |
| 02:51 | Apareix /tmp/.sysupd, setuid de root |
| 03:00:14 | El temporitzador llança l'agregador |
| ~03:05 | El p99 de l'API comença a degradar-se |
| 03:12 | Avís de clients |
| 03:17 | Acotat a E/S: io full 71 %, aqu-sz 27, rareq-sz 4 KB |
| 03:22 | Identificat l'agregador (pidstat -d, filefrag: 5.314 extents) |
| 03:24 | Mitigació amb ionice; el p99 baixa de 6,1 s a 0,94 s |
| 03:31 | Fils de meteo-api en D esperant flock |
| 03:40 | Confirmat interbloqueig per violació de la jerarquia |
| 03:42 | SIGTERM a l'agregador; cicle trencat |
| 03:44 | Servei restaurat: p99 0,204 s |
| 03:52 | Trobat el binari setuid i el buit del registre |
| 03:55 | L'incident es reclassifica com a seguretat; escalat |
Causa arrel davant de causes contribuents. La distinció no és acadèmica: determina en què s'inverteix l'esforç.
- Causa arrel de la degradació: l'
agregadorllegeix les dades amb un patró aleatori de 4 KB —rellegint 17,9 GB per processar 16,5 MB— sobre fitxers fragmentats en més de 5.000 extents, cosa que excedeix en un ordre de magnitud la capacitat d'IOPS del RAID 1. - Contribuent 1: no hi havia límits d'E/S a la unitat de l'
agregador, així que podia consumir el 100 % del disc competint d'igual a igual amb el servei de cara a l'usuari. - Contribuent 2: l'
agregadorviola la jerarquia de bloqueigs acordada. Latent durant mesos, es va manifestar en allargar-se la seva execució. No en va ser la causa, però sense ell el servei hauria tornat a la normalitat al minut 12 en lloc del 32. - Contribuent 3: no hi havia alerta sobre pressió d'E/S. L'avís va arribar dels clients, no del sistema, amb set minuts de retard.
- Contribuent 4:
/tmpestava muntat sensenoexecninosuid, cosa que va permetre que un binari setuid fos executable allà. - Contribuent 5: els registres només eren a la màquina, per la qual cosa el buit no es pot contrastar amb cap còpia externa.
Accions preventives concretes, cadascuna amb responsable i termini:
| # | Acció | Mòdul | Termini |
|---|---|---|---|
| 1 | Límits IOWeight, IOReadBandwidthMax i MemoryMax a agregador.service |
06-02, 07-02 | Fet |
| 2 | Alerta sobre io full avg60 > 20 % durant 5 min |
07-03 | 3 dies |
| 3 | Reescriure la lectura de l'agregador en un sol recorregut seqüencial |
02-05 | 2 setmanes |
| 4 | Preassignar els fitxers del dia amb fallocate |
04-05 | 2 setmanes |
| 5 | Centralitzar flock en un mòdul únic que imposi la jerarquia |
03-06 | 3 setmanes |
| 6 | Prova d'estrès d'encavalcament en integració contínua | — | 3 setmanes |
| 7 | Tornar a muntar /tmp amb noexec,nosuid,nodev |
04-03 | 1 dia |
| 8 | Auditoria periòdica de fitxers setuid i AIDE al dia | 05-03 | 1 setmana |
| 9 | Enviament de registres a un col·lector extern en mode només-afegir | 05-04 | 1 setmana |
| 10 | Procediment escrit de resposta a incidents, amb contactes legals | 05-04 | 1 mes |
Fixa't en el patró: cap acció no és «anar amb més compte». Totes són canvis en el sistema —límits, alertes, muntatges, estructura de codi— que fan que l'errada sigui impossible o que es detecti sola. Aquest és el criteri per saber si un post mortem ha servit d'alguna cosa.
Errors Habituals i Consells
| Error | Conseqüència | Què cal fer |
|---|---|---|
| Reiniciar el servei «a veure si s'arregla» | Destrueix l'evidència i el problema torna | Captura l'estat abans de mitigar |
| Aturar-se a la primera troballa | Aquí hauries deixat el p99 a 0,94 s | Compara sempre amb la línia base |
| Aplicar diverses mitigacions alhora | No saps quina ha funcionat | Una cada vegada, mesurant |
| Confondre mitigació amb solució | L'incident es repeteix el mes següent | Registra-les totes dues per separat |
| No anotar amb marca de temps | El post mortem es basa en records | Bitàcola amb tee des del minut u |
strace sobre el servei en producció |
Alentiment de 10× a 100× | perf, eBPF, o piles de /proc |
| Executar un binari sospitós «per veure què fa» | Pot ser el pas final de l'atac | No tocar-lo; preservar i escalar |
| Continuar amb el rendiment després d'indicis d'intrusió | Contamines proves i prioritzes malament | Aturar, contenir, escalar |
| Post mortem amb noms propis | La gent amaga informació | Sense culpables, sobre el sistema |
| Accions preventives vagues | No canvien res | Canvis concrets, amb responsable i termini |
Consells finals per al torn de guàrdia: comença sempre per dmesg; quantifica abans de tocar i torna a mesurar després; posa un límit de temps a la fase d'investigació i respecta'l; avisa aviat encara que no tinguis la resposta, perquè la gent tolera molt millor la incertesa comunicada que el silenci; i desconfia de la primera explicació que encaixa, perquè els incidents reals, com aquest, solen tenir més d'una causa.
Exercicis
Exercici 1: el servei que mor cada matinada
Cada nit entre les 02:00 i les 04:00, meteo-api deixa de respondre uns segons. systemctl status diu active (running) quan el mires al matí, però Main PID ha canviat. Dades recollides:
dmesg -T | grep -i oom [Mon Aug 31 03:22:41 2026] agregador invoked oom-killer: gfp_mask=0x140cca, order=0, oom_score_adj=0 [Mon Aug 31 03:22:41 2026] Out of memory: Killed process 1834 (meteo-api) total-vm:4210408kB, anon-rss:2914208kB, file-rss:0kB, shmem-rss:8192kB, UID:990 free -m (03:20): total 16037 usada 15102 lliure 198 mem/cau 737 disponible 402 vmstat (03:20): r=3 b=2 si=1204 so=1890 cs=14022 /proc/pressure/memory: some avg60=58.11 full avg60=31.04
Diagnostica amb el mètode de 07-03: qui va causar el problema, per què va morir qui va morir, i quina mitigació i quina solució de fons aplicaries.
Exercici 2: el servei que no arrenca després d'un canvi
Després d'editar /etc/meteora/meteora.conf per afegir-hi un camí de memòria cau nou, meteo-api no arrenca:
Active: failed (Result: exit-code) since Mon 2026-08-31 09:14:02 CEST; 30s ago Process: 20114 ExecStart=/usr/local/bin/meteo-api --config /etc/meteora/meteora.conf (code=exited, status=1/FAILURE) journalctl -u meteo-api -n 3: meteo-api[20114]: fatal: no es pot crear /var/cache/meteora/idx: Read-only file system
Executat a mà com a meteora, el binari arrenca sense problemes. Explica la contradicció i dona la solució correcta, a més de dues d'incorrectes que cal evitar i per què.
Solucions
Solució 1
Diagnòstic. Els indicadors són inequívocs i cap no és d'E/S: un disponible de només 402 MB, si=1.204 i so=1.890 pàgines per segon simultàniament —el sistema fica i treu pàgines alhora, que és la definició de thrashing (02-04)— i una pressió de memòria del 58 % (some) amb un 31 % de full: gairebé un terç del temps la màquina sencera no progressa.
Qui va causar el problema i qui va morir no són el mateix. La primera línia de dmesg ho diu literalment: agregador invoked oom-killer, és a dir, va ser la petició de memòria de l'agregador la que va esgotar el sistema i va disparar el mecanisme. Però el triat va ser meteo-api, amb 2,9 GB d'anon-rss. La raó és a l'oom_score, que és essencialment proporcional a la memòria resident: l'OOM killer mata el procés més gran, no el culpable. I com que meteo-api corre sota systemd amb Restart=on-failure, es reinicia sol, cosa que explica l'active (running) amb un Main PID diferent al matí i el fet que ningú no se n'assabentés.
La finestra 02:00-04:00 coincideix amb les execucions nocturnes de l'agregador; el mecanisme és una acumulació de memòria al procés d'agregació —probablement carrega a la RAM tot l'històric en lloc de processar-lo per blocs.
Mitigació (aquesta nit, sense tocar codi):
Amb això, quan l'agregador superi 1 GB, l'OOM killer del cgroup el matarà a ell i només a ell, sense tocar la resta del sistema; MemoryHigh hi afegeix un esglaó previ en què el nucli el frena i reclama pàgines agressivament abans d'arribar al límit dur. És exactament el mecanisme de 06-02. Convé a més protegir la víctima amb MemoryMin=512M a meteo-api.service, que reserva memòria que el nucli no li reclamarà.
Solució de fons: processar l'històric per blocs amb un consum acotat i constant, en lloc de carregar-lo sencer. I verificació: seguir el RSS de l'agregador durant una execució (while true; do awk '/VmRSS/{print $2}' /proc/$(pgrep -x agregador)/status; sleep 10; done) per comprovar que s'estabilitza en lloc de créixer linealment. Alerta preventiva: memory some avg60 > 20 % durant 5 minuts, que hauria avisat setmanes abans que el primer client.
El que no cal fer: abaixar vm.swappiness (no falta menys swap, falta memòria), afegir RAM sense entendre el creixement (només endarrereix el problema), ni ajustar l'oom_score_adj de meteo-api perquè no el triïn (mouries la mort a un altre procés innocent).
Solució 2
La contradicció és aparent i la seva explicació és l'enfortiment de la unitat. A mà, el binari corre amb el sistema de fitxers normal i meteora pot escriure allà on els seus permisos li deixin. Sota systemd, la unitat té ProtectSystem=strict, que munta tot l'arbre del sistema en només lectura dins de l'espai de noms de muntatge del servei, amb les úniques excepcions declarades a ReadWritePaths=: /var/lib/meteora, /var/log/meteora i /run/meteora. El camí nou, /var/cache/meteora, no és en aquesta llista, així que el procés veu un sistema de fitxers de només lectura i falla amb EROFS. És el mecanisme d'aïllament de 05-03 funcionant exactament com cal.
Solució correcta, que a més delega en systemd la creació del directori amb el propietari i el mode adequats:
systemctl daemon-reload && systemctl restart meteo-api.service
systemctl show meteo-api.service -p ReadWritePaths
journalctl -u meteo-api.service -n 20 --no-pagerCacheDirectory=meteora crea /var/cache/meteora amb propietari meteora:meteora, l'afegeix automàticament als camins escrivibles i el gestiona dins del cicle de vida del servei. L'alternativa acceptable, si el directori ja existeix i el gestiona un altre procés, és afegir ReadWritePaths=/var/cache/meteora.
Dues solucions incorrectes i per què:
- Treure
ProtectSystem=strict. Resol el símptoma desarmant la protecció: el servei passa a poder escriure a tot el sistema, inclosos/etci/usr. Canvies un problema de configuració de cinc línies per un augment permanent de la superfície d'atac, justament al servei exposat a Internet. La puntuació desystemd-analyze securityho reflectiria immediatament. - Executar el servei com a root (o donar
chmod 777al directori). Llença per la borda tota la cadena de decisions del curs: el compte sense shell, l'UID 990,CAP_NET_BIND_SERVICEen lloc de root,NoNewPrivileges. I777permetria a qualsevol usuari del sistema —inclòs un procés compromès— manipular la memòria cau del servei.
Lliçó general: quan un servei funciona a mà i falla sota systemd, la diferència és gairebé sempre a l'entorn (PATH, directori de treball, variables) o a l'enfortiment (ProtectSystem, ReadWritePaths, SystemCallFilter, capabilities). I la resposta correcta és gairebé sempre declarar l'excepció concreta, mai desactivar la protecció.
Conclusió
Has resolt un incident complet, i en fer-ho has fet servir el curs sencer. Val la pena veure'n el mapa, perquè cada peça teòrica ha acabat convertida en una eina de diagnòstic.
Del mòdul 1 venia la idea que el sistema operatiu és una màquina estesa i un gestor de recursos, i que tot passa per crides al sistema; sense això, strace, wchan i /proc/<pid>/stack serien màgia. Del mòdul 2 van sortir els estats de procés —D i el seu pes a la càrrega mitjana—, els patrons d'accés al disc que expliquen per què 16,5 MB poden trigar 42 segons, l'OOM killer triant per mida i no per culpa, i RSS davant de VSZ per detectar una fuita. Del mòdul 3 van venir els fils, flock, el futex i, sobretot, la jerarquia de bloqueigs la violació de la qual va produir l'interbloqueig i la centralització de la qual el preveu. Del mòdul 4, els inodes i /proc/locks, la fragmentació en extents que filefrag va revelar, i l'escriptura atòmica que evita fitxers truncats. Del mòdul 5, tot el que va passar a partir de les 03:52: el binari setuid, el buit al registre, l'ordre de volatilitat, la cadena de custòdia, noexec a /tmp i l'obligació d'escalar. Del mòdul 6, els cgroups que van limitar l'agregador amb io.max i MemoryMax. I del mòdul 7, la shell que ho va executar tot, systemd que ho governa i el mètode que va ordenar la investigació.
Aquest és el missatge central del curs: la teoria no és un peatge previ a la pràctica, és el que permet interpretar el que veus. Dues persones executen iostat -x i veuen els mateixos números; només una sap que un rareq-sz de 4 KB amb rrqm/s a zero significa accés aleatori, que la cua importa més que la utilització i que un RAID 1 duplica lectures però no escriptures. La diferència no és a l'ordre, sinó als mòduls 2 i 4.
Els camins que s'obren des d'aquí:
| Camí | Què aprofundir | Pas següent natural |
|---|---|---|
| Administració de sistemes | Xarxes, emmagatzematge, alta disponibilitat, còpies | Certificacions LFCS/RHCSA; muntar el teu propi laboratori |
| DevOps i núvol | Infraestructura com a codi, CI/CD, Kubernetes, observabilitat | Terraform, Ansible, un clúster de pràctiques |
| Seguretat | Anàlisi forense, resposta a incidents, hardening, criptografia | Enginyeria inversa, CTF, auditoria de sistemes reals |
| Sistemes encastats i temps real | Yocto, FreeRTOS, Zephyr, controladors | Una placa barata i un projecte amb terminis reals |
| Desenvolupament de nucli | C, estructures de dades del nucli, subsistemes | Linux Kernel Development; compilar i pedaçar un nucli |
Tots comparteixen la mateixa base: la que acabes d'acabar.
Com continuar practicant, que és l'única cosa que consolida tot això:
- Munta una màquina virtual de laboratori i trenca-la expressament. Provoca un OOM, satura el disc amb
fio, crea un interbloqueig amb dos guions iflock, esborra un fitxer d'unitat i arregla'l. Res no ensenya tant com reparar una cosa que has trencat tu. - Llegeix
/procamb curiositat. Cada fitxer de/proc/<pid>/és una finestra a una estructura del nucli. Dedica una tarda a recórrerstatus,maps,io,limits,stack,fd/ienvirond'un procés real: n'entendràs més que amb molts capítols. - Reprodueix els exercicis del curs a la teva pròpia màquina, canviant-ne els números. Els del mòdul 3 i els laboratoris de 07-03 són els que més rendeixen.
- Llegeix registres encara que no passi res. Familiaritzar-te amb l'aspecte d'un
journalctlnormal és el que et permetrà detectar l'anormal en tres segons. - Escriu els teus propis post mortem, encara que l'incident sigui domèstic i tu l'únic lector. Posar per escrit la cronologia i la causa arrel és el que converteix una experiència en coneixement.
Vam començar definint el sistema operatiu com una màquina estesa que amaga la complexitat del maquinari. Acabem a les quatre del matí en un servidor de debò, llegint aquesta complexitat a través de les finestres que el sistema mateix ens ofereix: /proc, el diari, els comptadors del nucli. Entre un punt i l'altre hi ha set mòduls, però en realitat hi ha una sola idea repetida: el sistema operatiu no és una caixa negra. És un programa, escrit per persones, que pren decisions comprensibles sobre recursos limitats, i que deixa rastre de totes elles. Aprendre a llegir aquest rastre és el que separa qui fa servir un ordinador de qui l'entén.
Ja saps fer-ho. La pròxima vegada que el telèfon soni a les tres de la matinada, no sabràs la resposta —ningú no la sap—, però sabràs com trobar-la: quantificar el símptoma, descartar amb dades, buscar la saturació i no la utilització, desconfiar de la primera explicació que encaixi i anotar-ho tot perquè a la persona següent li sigui més fàcil.
Gràcies per haver arribat fins aquí. Ara apaga això, obre un terminal i trenca alguna cosa.
Fonaments de Sistemes Operatius
Mòdul 1: Introducció als Sistemes Operatius
- Conceptes Bàsics de Sistemes Operatius
- Història i Evolució dels Sistemes Operatius
- Tipus de Sistemes Operatius
- Funcions Principals d'un Sistema Operatiu
- Arquitectura del Nucli: Monolític, Microkernel i Híbrid
- Mode Usuari, Mode Nucli i Crides al Sistema
Mòdul 2: Gestió de Recursos
- Gestió de Processos
- Planificació de la CPU
- Gestió de Memòria
- Memòria Virtual i Paginació
- Gestió d'Emmagatzematge
- Gestió de Dispositius
- Controladors, Interrupcions i Operacions d'E/S
Mòdul 3: Concurrència
- Conceptes de Concurrència
- Fils i Processos
- Comunicació entre Processos (IPC)
- Sincronització i Exclusió Mútua
- Problemes Clàssics de Concurrència
- Interbloquejos: Prevenció, Detecció i Recuperació
Mòdul 4: Estructures de Fitxers
- Sistemes de Fitxers
- Estructures de Directoris
- Particions, Muntatge i Sistema de Fitxers Virtual
- Gestió de Fitxers
- Assignació d'Espai, Journaling i Integritat
- Seguretat i Permisos de Fitxers
Mòdul 5: Protecció i Seguretat del Sistema
- Principis de Protecció i Control d'Accés
- Usuaris, Autenticació i Escalada de Privilegis
- Amenaces Habituals i Enfortiment del Sistema
- Auditoria, Registres i Resposta a Incidents
Mòdul 6: Virtualització i Contenidors
- Virtualització: Hipervisors i Màquines Virtuals
- Contenidors: Namespaces i cgroups
- El Sistema Operatiu al Núvol
- Sistemes Operatius Mòbils i de Temps Real
