Tout est lent, les graphiques sont rouges et quelqu’un a déjà proposé la solution habituelle : « Ajoutez simplement du CPU. » Une autre personne répond « C’est le stockage. » Une troisième insiste : « C’est le réseau. » C’est ainsi que les équipes gaspillent des jours, de l’argent et de la crédibilité : en résolvant la théorie la plus bruyante au lieu de la limite réelle.
Le remède est ennuyeux et fiable : mesurez la saturation, la latence, le débit et les files—en même temps—sur le chemin critique. Puis prenez une décision conforme à la physique. Pas aux sensations.
Ce qu’est un goulot d’étranglement (et ce que ce n’est pas)
Un goulot d’étranglement est la ressource qui limite le travail utile de votre système pour une charge donnée. Ce n’est pas « la chose avec le plus grand graphique ». Ce n’est pas « le composant que vous avez mis à niveau le trimestre dernier ». Ce n’est pas « ce qui est à 90 % ».
Les goulots d’étranglement dépendent de la charge. « Le stockage est le goulot d’étranglement » n’est pas un diagnostic. C’est une catégorie. Le diagnostic est : « Les lectures aléatoires 4k à QD=1 sont limitées par la latence de lecture NVMe ; nous atteignons p99 à 3.2ms et il y a mise en file dans le thread pool de l’app. » C’est actionnable. C’est un couteau, pas un cor-fusée.
Trois définitions à graver dans votre runbook
- Capacité : combien de travail une ressource peut effectuer par unité de temps (par ex., requêtes/sec, MB/sec, IOPS).
- Saturation : à quel point vous êtes proche de cette capacité sous une charge (et si des files se forment).
- Latence en queue : ce que voient vos utilisateurs les plus affectés. P95/P99, c’est là que vit votre pager.
Attention : les goulots d’étranglement bougent. Corrigez le CPU et vous exposerez la contention sur les locks. Corrigez les locks et vous exposerez le stockage. Corrigez le stockage et vous exposerez le réseau. Félicitations : votre système est désormais assez rapide pour trouver une nouvelle façon d’être lent.
Une citation pour rester honnête. L’idée paraphrasée de Gene Kim : Améliorer le flux signifie trouver les contraintes et les élever ; optimiser des non-contraintes crée juste une vitesse locale et une douleur globale.
— Gene Kim (idée paraphrasée)
Blague n°1 (courte, pertinente) : Un « correctif de performance rapide » est comme une « règle de pare-feu temporaire ». Il sera là à votre fête de départ à la retraite.
Les quatre signaux qui comptent : saturation, latence, débit, erreurs
Si vous voulez moins de disputes et des appels d’incident plus rapides, standardisez un vocabulaire de mesure. J’utilise quatre signaux parce qu’ils imposent la clarté :
Saturation : « Sommes‑nous à la limite ? »
La saturation concerne les files et la contention. Files d’exécution CPU. Files de requêtes disque. Files de transmission NIC. Pools de connexions de base de données. Ordonnancement des pods Kubernetes. Si les files augmentent, vous êtes saturé ou mal configuré.
Latence : « Combien de temps prend une unité de travail ? »
Mesurez la latence de bout en bout et la latence par composant. La latence moyenne est une fiction polie ; utilisez des percentiles. La latence en queue est l’endroit où les goulots apparaissent en premier parce que les files amplifient la variance.
Débit : « Combien de travail utile achevons-nous ? »
Le débit n’est pas « octets transférés » si l’entreprise se soucie des « commandes passées ». Idéalement, mesurez les deux : débit au niveau utilisateur et débit système. Un système peut déplacer beaucoup d’octets tout en achevant moins de transactions. C’est ainsi que les retries et les thundering herds financent le nouveau yacht de votre fournisseur cloud.
Erreurs : « Échouons‑nous rapidement ou lentement ? »
Quand la saturation arrive, les systèmes échouent souvent lentement avant d’échouer bruyamment. Timeouts, retries, erreurs de checksum, retransmissions TCP, erreurs I/O, context deadline exceeded — ce sont de la fumée indiquant un goulot. Traitez les erreurs comme des signaux de performance de première classe.
Ce que signifie « la limite réelle »
La limite réelle est le point où l’augmentation de la charge offerte n’accroît plus le travail accompli, alors que la latence et la mise en file explosent. C’est un « genou » mesurable. Votre objectif est d’identifier ce genou pour la charge qui compte, pas pour un benchmark synthétique qui flatte votre bon de commande.
Files : où la performance meurt en silence
La plupart des goulots en production sont des problèmes de mise en file déguisés en « lenteur ». La machine n’est pas nécessairement lente ; elle attend son tour derrière d’autres travaux. C’est pourquoi « l’utilisation de la ressource » peut sembler correcte alors que le système est misérable.
Little’s Law, mais en mode opérationnel
Little’s Law : L = λW (nombre moyen dans le système = taux d’arrivée × temps dans le système). Vous n’avez pas besoin d’un diplôme en maths pour l’utiliser. Si la latence (W) augmente et que le taux d’arrivée (λ) reste similaire, le nombre de choses en attente (L) doit augmenter. C’est de la mise en file. Trouvez la file.
Types de files que vous croiserez à 2h du matin
- File d’exécution CPU : threads prêts à s’exécuter mais pas planifiés (surveillez load et longueur de run queue).
- File d’E/S bloquante : requêtes en attente pour les disques/NVMe (surveillez avgqu-sz, await, utilisation du device).
- Files de mutex/locks : threads en attente d’un lock (perf, eBPF, ou métriques applicatives).
- Pools de connexions : pools DB/HTTP qui plafonnent la concurrence (surveillez le temps d’attente du pool).
- Files réseau du kernel : pertes dans qdisc, dépassements de ring buffer (surveillez drops, retransmits).
- Stalls GC et allocateur : mise en file à l’intérieur des runtimes (JVM, Go, Python).
Les files ne sont pas toujours mauvaises. Elles deviennent mauvaises lorsqu’elles sont incontrôlées, cachées ou couplées aux retries. Les retries multiplient les files. Une petite hausse de latence peut devenir une DDoS auto‑infligée.
Mode d’emploi pour un diagnostic rapide (premier/deuxième/tiers)
Voici la séquence « entrer dans une pièce en feu sans devenir le feu ». Elle est conçue pour la réalité de l’on-call : vous avez une observabilité partielle, des parties prenantes en colère et une chance unique de ne pas empirer la situation.
Premier : confirmer le symptôme en termes utilisateur
- Qu’est‑ce qui est lent ? Un chargement de page ? Un endpoint API ? Un job batch ? Une requête DB ?
- S’agit‑il de latence, de débit, ou des deux ?
- Est‑ce global ou isolé (une AZ, un pool de nœuds, un tenant) ?
- Qu’est‑ce qui a changé récemment (deploys, config, forme du trafic, croissance des données) ?
Deuxième : trouvez la file la plus proche de la douleur
Commencez par le service face à l’utilisateur et descendez la pile :
- Métriques app : percentiles de latence des requêtes, concurrence, timeouts, taux de retry.
- Pools de threads/workers : longueur de file, saturation, travaux rejetés.
- DB : attente de pool de connexions, requêtes lentes, waits de locks.
- OS : run queue CPU, iowait, pression mémoire, context switches.
- Stockage : latence/device queue depth, stalls système de fichiers, ZFS txg sync, writeback des pages sales.
- Réseau : retransmits, drops, saturation d’interface, latence DNS.
Troisième : mesurez la saturation et la marge, puis arrêtez de deviner
Choisissez 2–3 contraintes candidates et rassemblez des preuves solides en 10 minutes :
- CPU : run queue, steal time, throttling, hotspots par cœur.
- Mémoire : fautes majeures, swap, reclaim, risque OOM.
- Disque/NVMe : await, svctm (précaution), avgqu-sz, util, stalls discard/writeback.
- Réseau : retransmits, drops, qdisc, erreurs NIC, RTT.
Si vous ne pouvez pas expliquer la lenteur avec une de ces catégories, cherchez des goulots de coordination : locks, élections de leader, services centralisés, quotas, limites d’API, ou une seule shard chaude.
Tâches pratiques : commandes, sorties, décisions (12+)
Ces tâches sont destinées à être exécutées sur un hôte Linux dans le chemin chaud. Chacune inclut : une commande, ce que signifie la sortie, et la décision à prendre. Exécutez‑les pendant un incident, mais aussi au calme pour savoir à quoi ressemble le « normal ».
Task 1: Identify whether CPU is actually the limit (run queue + iowait)
cr0x@server:~$ uptime
14:02:11 up 37 days, 3:19, 2 users, load average: 18.24, 17.90, 16.88
Meaning: Load average counts runnable tasks and uninterruptible sleep (often I/O wait). “18” on a 32-core box might be fine, or terrible, depending on what those tasks are doing.
Decision: Don’t declare CPU bottleneck from load alone. Follow with vmstat and per-core view.
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
12 7 0 82124 51200 842120 0 0 120 9820 820 2400 22 8 41 29 0
15 8 0 81560 51200 841980 0 0 140 10120 900 2600 21 9 39 31 0
Meaning: r is runnable tasks; b is blocked (often on I/O). High wa with high b points to I/O stalls, not CPU shortage.
Decision: If b and wa are high, shift attention to storage/network I/O before scaling CPU.
Task 2: Check per-core saturation and steal/throttling (virtualization pain)
cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.1.0 (server) 01/10/2026 _x86_64_ (32 CPU)
13:58:01 CPU %usr %sys %iowait %irq %soft %steal %idle
13:58:02 all 24.10 10.20 18.50 0.00 0.90 0.00 46.30
13:58:02 7 89.00 9.00 0.00 0.00 0.00 0.00 2.00
13:58:02 12 10.00 5.00 70.00 0.00 1.00 0.00 14.00
Meaning: One CPU pegged can indicate single-threaded bottleneck or IRQ imbalance; high %steal indicates hypervisor contention; high %iowait on specific CPUs can correlate with IRQ affinity and device queues.
Decision: If a few cores are pinned, investigate single-threaded hotspots, interrupt distribution, or affinity settings before adding more cores.
Task 3: Identify top waiters: CPU vs I/O vs locks (high-level triage)
cr0x@server:~$ top -b -n 1 | head -n 20
top - 13:59:41 up 37 days, 3:17, 2 users, load average: 18.24, 17.90, 16.88
Tasks: 412 total, 11 running, 401 sleeping, 0 stopped, 0 zombie
%Cpu(s): 22.4 us, 8.9 sy, 0.0 ni, 46.1 id, 22.1 wa, 0.0 hi, 0.5 si, 0.0 st
MiB Mem : 64000.0 total, 1100.2 free, 4200.1 used, 58700.0 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 59600.0 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
23120 app 20 0 4120.5m 520.2m 152.0m D 35.0 0.8 12:20.11 java
Meaning: Process in state D is uninterruptible sleep, commonly waiting on I/O. High system-wide wa reinforces I/O as a candidate bottleneck.
Decision: Follow the “D” processes to see what they’re waiting on (block devices, NFS, etc.).
Task 4: Map a stuck process to its open files and the backing filesystem
cr0x@server:~$ lsof -p 23120 | head
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 23120 app cwd DIR 253,0 4096 2 /srv/app
java 23120 app 12u REG 253,0 1048576 98342 /srv/app/data/index.bin
Meaning: Shows which paths the process is actively using. You’re trying to connect “app is slow” to “this filesystem/device is slow.”
Decision: Identify the mount and device that backs the hot path; focus I/O tooling there.
cr0x@server:~$ findmnt -T /srv/app/data/index.bin
TARGET SOURCE FSTYPE OPTIONS
/srv /dev/nvme0n1p2 ext4 rw,relatime
Decision: Now you know which device to inspect (nvme0n1 here).
Task 5: Device-level latency and queueing (the truth serum)
cr0x@server:~$ iostat -x 1 3
Linux 6.1.0 (server) 01/10/2026 _x86_64_ (32 CPU)
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz %util
nvme0n1 820.0 2100.0 3280.0 26800.0 0.0 0.0 0.00 0.00 1.2 18.9 42.10 98.00
Meaning: High %util with high aqu-sz means the device is saturated and requests are queueing. Write await is high, so writes are waiting a long time.
Decision: Stop pretending CPU will fix it. Either reduce write pressure, change I/O pattern, add devices, or move workload. Also check if the latency is coming from the device or from filesystem flush behavior.
Task 6: NVMe-specific health and error counters
cr0x@server:~$ sudo nvme smart-log /dev/nvme0
Smart Log for NVME device:nvme0 namespace-id:ffffffff
critical_warning : 0
temperature : 41 C
available_spare : 100%
percentage_used : 7%
media_errors : 0
num_err_log_entries : 0
Meaning: If you see media errors or a high percentage used, performance can degrade, and your “bottleneck” becomes “failing drive.”
Decision: If errors show up, switch from performance mode to reliability mode: plan replacement and reduce load.
Task 7: Filesystem and kernel writeback pressure (dirty pages can be your queue)
cr0x@server:~$ cat /proc/meminfo | egrep 'Dirty|Writeback|MemFree|MemAvailable'
MemFree: 80124 kB
MemAvailable: 61045232 kB
Dirty: 1248204 kB
Writeback: 92160 kB
WritebackTmp: 0 kB
Meaning: Large and persistent Dirty can mean the system is buffering writes and later flushing them in painful bursts, creating latency spikes.
Decision: If dirty data grows without draining, investigate writeback throttling, filesystem journaling, and application flush patterns.
Task 8: Detect memory pressure (the sneaky bottleneck that impersonates CPU)
cr0x@server:~$ vmstat 1 5 | tail -n +3
6 0 0 10240 45000 1200000 0 0 0 2000 1100 3800 30 12 48 10 0
5 0 0 8120 45000 1180000 0 0 0 2200 1200 4200 29 11 44 16 0
Meaning: Watch for si/so (swap in/out) and consistently low free memory with high cs (context switches). Even without swap, reclaim storms can spike latency.
Decision: If reclaim is heavy (check sar -B or PSI), reduce memory footprint or increase RAM; don’t chase phantom CPU issues.
Task 9: Pressure Stall Information (PSI): prove time lost to contention
cr0x@server:~$ cat /proc/pressure/io
some avg10=12.34 avg60=10.21 avg300=8.90 total=98324212
full avg10=4.12 avg60=3.20 avg300=2.75 total=31244211
Meaning: PSI quantifies how much time tasks are stalled due to I/O pressure. full means no task could make progress due to I/O at times—this is real pain, not theoretical.
Decision: If PSI is high, prioritize reducing I/O contention (batching, caching, queue limits) over micro-optimizing CPU.
Task 10: Network: find retransmits and drops (latency that looks like “app slowness”)
cr0x@server:~$ ss -s
Total: 1252 (kernel 0)
TCP: 1023 (estab 812, closed 141, orphaned 0, timewait 141)
Transport Total IP IPv6
RAW 0 0 0
UDP 12 9 3
TCP 882 740 142
INET 894 749 145
FRAG 0 0 0
Meaning: High connection churn can indicate retries/timeouts. This output is just the appetizer.
cr0x@server:~$ netstat -s | egrep 'retransmit|segments retransmited|packet receive errors|dropped'
18432 segments retransmited
92 packet receive errors
1184 packets received, dropped
Meaning: Retransmits and drops are throughput killers and tail-latency factories.
Decision: If retransmits rise during slowness, treat the network path as a candidate bottleneck: check NIC stats, queues, and upstream devices.
Task 11: NIC-level counters (prove it’s not “the app”)
cr0x@server:~$ ip -s link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 0a:1b:2c:3d:4e:5f brd ff:ff:ff:ff:ff:ff
RX: bytes packets errors dropped missed mcast
9823412231 9123421 0 812 0 0
TX: bytes packets errors dropped carrier collsns
11233412211 10233421 0 221 0 0
Meaning: Drops at the host are unambiguous. They may be due to ring buffer overruns, qdisc issues, or just too much traffic.
Decision: If drops climb, reduce burstiness (pacing), increase buffers carefully, or fix upstream congestion; don’t just “add threads.”
Task 12: Check filesystem latency spikes from sync-heavy workloads (ext4 example)
cr0x@server:~$ sudo dmesg -T | egrep -i 'blocked for more than|I/O error|nvme|EXT4-fs' | tail -n 10
[Fri Jan 10 13:55:12 2026] INFO: task java:23120 blocked for more than 120 seconds.
[Fri Jan 10 13:55:12 2026] EXT4-fs (nvme0n1p2): Delayed block allocation failed for inode 98342 at logical offset 152343 with max blocks 4
Meaning: Kernel tells you processes are blocked and sometimes why. This can implicate allocation pressure, writeback, or device stalls.
Decision: Treat “blocked for more than” as a serious I/O stall. Stop rolling restarts; find the I/O cause.
Task 13: ZFS: spot transaction group sync pressure (common “why are writes spiky?” culprit)
cr0x@server:~$ sudo zpool iostat -v 1 3
capacity operations bandwidth
pool alloc free read write read write
tank 1.20T 2.40T 820 2400 3.2M 26.8M
mirror 1.20T 2.40T 820 2400 3.2M 26.8M
nvme0n1 - - 410 1200 1.6M 13.4M
nvme1n1 - - 410 1200 1.6M 13.4M
Meaning: Good for bandwidth and ops, but not latency by itself. If writes look fine but app latency is awful, you may be stuck on sync writes, slog contention, or txg sync bursts.
Decision: If you suspect sync behavior, check the workload and dataset properties next.
cr0x@server:~$ sudo zfs get sync,logbias,compression,recordsize tank/app
NAME PROPERTY VALUE SOURCE
tank/app sync standard local
tank/app logbias latency local
tank/app compression lz4 local
tank/app recordsize 128K local
Decision: If the app does many small sync writes (databases, journaling), validate SLOG design and whether sync semantics are required. Don’t flip sync=disabled in production unless you enjoy explaining data loss to legal.
Task 14: Benchmark safely with fio (don’t lie to yourself)
cr0x@server:~$ sudo fio --name=randread --filename=/srv/app/.fiotest --size=2G --rw=randread --bs=4k --iodepth=1 --numjobs=1 --direct=1 --time_based --runtime=30 --group_reporting
randread: (groupid=0, jobs=1): err= 0: pid=31022: Fri Jan 10 14:01:02 2026
read: IOPS=18.2k, BW=71.0MiB/s (74.4MB/s)(2131MiB/30001msec)
lat (usec): min=55, max=3280, avg=68.11, stdev=22.50
clat percentiles (usec):
| 1.00th=[ 58], 50.00th=[ 66], 95.00th=[ 82], 99.00th=[ 120]
Meaning: This measures 4k random read latency at QD=1, which often matches real application patterns better than “big sequential read.” Percentiles show tail behavior.
Decision: If your app is latency-sensitive and fio shows p99 climbing under load, you’ve identified a storage latency ceiling. Plan mitigation (cache, sharding, faster media, better batching) instead of tuning unrelated layers.
Task 15: Identify block layer queue depth and scheduler
cr0x@server:~$ cat /sys/block/nvme0n1/queue/nr_requests
1024
cr0x@server:~$ cat /sys/block/nvme0n1/queue/scheduler
[none] mq-deadline kyber bfq
Meaning: nr_requests influences how deep queues can get. Scheduler choice can affect tail latency, fairness, and throughput.
Decision: If tail latency is the pain, consider a scheduler that behaves better under contention and reduce queueing where appropriate. Don’t cargo-cult; test with your workload.
Task 16: Spot throttling (containers and cgroups: the invisible bottleneck)
cr0x@server:~$ cat /sys/fs/cgroup/cpu.stat
usage_usec 92833423211
user_usec 81223311200
system_usec 11610112011
nr_periods 124112
nr_throttled 32110
throttled_usec 8823122210
Meaning: If nr_throttled and throttled_usec climb, your workload is being CPU-throttled by cgroups. It looks like “we need more CPU” but it’s actually “we set a limit.”
Decision: Adjust CPU limits/requests, reduce burstiness, or move the workload. Don’t upgrade hardware to compensate for your own quota.
Blague n°2 (courte, pertinente) : Si votre goulot est « la base de données », félicitations — vous avez découvert la gravité.
Faits intéressants et un peu d’histoire (pour arrêter de répéter de vieilles erreurs)
- La loi d’Amdahl (1967) a formalisé pourquoi accélérer une partie d’un système donne des gains décroissants si le reste reste sériel. C’est le math derrière « nous avons doublé le CPU et gagné 12 % ».
- La loi de Little (publiée en 1961) est devenue un des outils les plus pratiques pour raisonner sur la latence et la concurrence bien avant l’observabilité moderne.
- La courbe « utilisation vs latence » était enseignée depuis des décennies en théorie des files ; les incidents modernes « p99 est devenu vertical » sont cette courbe, pas un mystère.
- La confusion autour d’IOwait est ancienne : Linux compte les tâches en sommeil ininterruptible dans le load average. Les gens s’envoient toujours des pages pour un « load élevé » qui est en réalité de l’attente disque.
- L’Ethernet est plus rapide que beaucoup de piles de stockage depuis des années ; en pratique, les coûts logiciels (chemins kernel, TLS, sérialisation) bottleneckent souvent avant le débit ligne.
- NVMe a amélioré grandement le parallélisme en offrant plusieurs files et une faible surcharge par rapport à SATA/AHCI, mais il a aussi facilité la dissimulation de la latence derrière un profond enchaînement de files — jusqu’à ce que la latence tail vous morde.
- « The Free Lunch Is Over » (mi‑2000s) ne parlait pas seulement des CPU ; il a forcé les logiciels à affronter les goulots parallèles : locks, contention cache et coûts de coordination.
- Les caches d’écriture et les barrières ont une longue histoire d’échanges entre durabilité et vitesse. Beaucoup de « victoires de performance » catastrophiques étaient en fait des réglages de perte de données non détectés.
- La latence tail est devenue grand public dans les services web à grande échelle parce que les moyennes n’expliquaient pas la douleur des utilisateurs. Le passage de l’industrie au p95/p99 est l’une des rares améliorations culturelles mesurables.
Trois mini-récits d’entreprise du terrain
Mini-récit 1 : L’incident causé par une mauvaise hypothèse
Le symptôme était classique : la latence API a doublé, puis triplé, puis le budget d’erreurs a commencé à hurler. L’équipe on-call a vu des load averages élevés sur les nœuds app et a déclaré « CPU is pegged ». Ils ont augmenté le nombre de pods et n’ont vu… aucune amélioration. Plus de pods, même douleur.
La mauvaise hypothèse était subtile et courante : ils traitaient le load average comme « utilisation CPU ». En réalité, les nœuds avaient beaucoup de CPU idle. Le load était gonflé par des threads bloqués en sommeil ininterruptible. Ils attendaient le disque.
La cause racine vivait dans une autre couche : un changement dans la stratégie de logging de l’application. Une mise à jour « sans danger » est passée du log bufferisé à des écritures synchrones pour « l’exactitude de l’audit ». Les systèmes de fichiers étaient sur des volumes réseau avec un débit correct mais une latence de sync médiocre. Sous pic de trafic, chaque requête faisait au moins un fsync. La latence est devenue verticale parce que le goulot n’était pas le débit ; c’était la latence fsync et la mise en file.
La correction n’était pas « plus de CPU ». Ils ont déplacé les logs d’audit vers un pipeline asynchrone avec buffering durable, et isolé l’I/O des logs des threads de requêtes. Ils ont aussi ajouté un budget strict sur les opérations sync par chemin de requête. Le postmortem a inclus une nouvelle règle : si vous touchez fsync, vous devez joindre des mesures p99 de la latence sur un stockage semblable à la production.
Mini-récit 2 : L’optimisation qui a mal tourné
Une équipe plateforme voulait accélérer du batch. Ils ont remarqué que leur job d’ingestion sur stockage objet « n’utilisait pas pleinement le réseau » et ont décidé d’augmenter la concurrence : plus de workers, tampons plus grands, files plus profondes. Le débit a augmenté en staging. On s’est tapé dans la main. La modification est sortie le vendredi après‑midi, parce que bien sûr.
En production, le job a atteint un débit plus élevé—brièvement. Puis le reste de la flotte a commencé à souffrir. La latence tail API a grimpé sur des services non liés. Les connexions DB se sont accumulées. Les retries ont éclaté. Le canal d’incident est devenu un festival de captures d’écran.
Ce qui s’est passé est un parfait exemple d’optimiser une non-contrainte jusqu’à ce qu’elle devienne une contrainte pour tout le monde. Le job batch n’utilisait pas seulement plus de réseau ; il consommait des ressources partagées : buffers top-of-rack, queues NIC hôtes, et surtout le budget de requêtes du backend de stockage. Les services sensibles à la latence se sont retrouvés en compétition avec un job d’arrière-plan sans backpressure et sans contrôles d’équité.
La correction finale n’était pas « limiter le job à un thread ». C’était implémenter ordonnancement et isolation explicites : shaping réseau cgroup, pools de nœuds séparés, et limitation du débit vers le backend. Ils ont aussi introduit une concurrence SLO-aware : le job recule quand le p99 du trafic client se dégrade. Ce n’est pas sexy. C’est adulte.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une entreprise exécutant des charges mixtes—bases de données, queues et un cluster de recherche—avait une habitude peu flatteuse en revue d’architecture : chaque trimestre, ils faisaient des tests de charge contrôlés en production hors‑pic et enregistraient des « points de genou » de référence pour les services clés. Mêmes scripts. Mêmes fenêtres de mesure. Mêmes dashboards. Pas d’héroïsme.
Un mardi, la latence client a monté. Pas une panne totale, juste une lente dégradation : le p99 est passé de « correct » à « les gens se plaignent ». Le premier intervenant a ouvert le rapport de base de deux semaines plus tôt et a remarqué quelque chose : le système atteignait le genou de débit à 20 % de trafic en moins qu’avant. Cela signifiait que la capacité avait effectivement rétréci.
Parce qu’ils disposaient de baselines, ils n’ont pas perdu de temps en débats. Ils ont comparé iostat et PSI au baseline et ont vu un nouveau motif : une latence d’écriture plus élevée et des files plus profondes sur un sous-ensemble spécifique de nœuds. Ces nœuds partageaient la même version de firmware sur leurs disques NVMe—récemment mise à jour dans le cadre d’une « maintenance de routine ». La mise à jour avait changé le comportement de gestion d’énergie et introduit des pics de latence sous écritures soutenues.
Ils ont rollbacké le firmware sur la flotte affectée et le point de genou est revenu. Pas de nouvelle architecture. Pas d’Olympiade du blâme. Juste un rollback calme appuyé par des preuves. L’action du postmortem était délicieusement terne : verrouiller les versions de firmware et ajouter un test de régression de latence aux procédures de maintenance.
Erreurs fréquentes : symptôme → cause racine → correction
1) Load average élevé → « On a besoin de plus de CPU » → vous scalez et rien ne change
Symptôme : Load average élevé, utilisateurs se plaignent, CPU a l’air « occupé » à première vue.
Cause racine : Les tâches sont bloquées en i/o wait (sommeil ininterruptible). Le load les inclut.
Correction : Confirmez avec vmstat (b, wa), iostat -x, PSI I/O. Résolvez la latence stockage ou réduisez les I/O sync dans le chemin de requête.
2) Disk %util à 100% → « Le disque est saturé » → vous achetez des disques plus rapides et voyez toujours des pics de latence
Symptôme : %util bloqué, await élevé, profondeur de file grande.
Cause racine : Le disque peut être correct ; le pattern de la charge est pathologique (petites écritures sync, fsync storms, contention du journal), ou la file est trop profonde et amplifie la latence tail.
Correction : Caractérisez la taille des I/O, la fréquence des syncs et la concurrence. Introduisez du batching, déplacez le fsync hors du chemin critique, ajustez scheduler/queue depth, et validez avec fio en reproduisant le pattern applicatif.
3) « Le réseau va bien, on a de la bande passante » → la latence tail est toujours catastrophique
Symptôme : Mbps de l’interface sous la ligne, pourtant timeouts et p99 longs.
Cause racine : Drops/retransmits, bufferbloat ou microbursts. Les graphiques de débit mentent quand des paquets sont renvoyés.
Correction : Vérifiez netstat -s, ip -s link, stats qdisc. Appliquez pacing, fair queueing, ou réduisez la concurrence en rafale.
4) « On a optimisé le hit rate du cache » → la pression mémoire tue silencieusement la performance
Symptôme : Utilisation de cache plus élevée, mais pics de latence et augmentation du temps système CPU.
Cause racine : La croissance du cache déclenche reclaim, fautes de page ou stalls de compaction ; le système passe du temps à gérer la mémoire plutôt qu’à servir les requêtes.
Correction : Mesurez PSI memory, major faults et reclaim. Limitez les caches, dimensionnez-les volontairement et gardez une marge pour le kernel et le working set.
5) « Plus de threads = plus de débit » → le débit stagne tandis que la latence explose
Symptôme : Concurrence augmentée, p99 se détériore, CPU pas pleinement utilisé.
Cause racine : Vous avez saturé un downstream partagé (DB, disque, lock ou pool). Plus de concurrence ne fait que creuser les files.
Correction : Trouvez la ressource limitante, ajoutez du backpressure et réduisez la concurrence jusqu’au point de genou. Privilégiez le shedding de charge plutôt que l’attente infinie.
6) « On a activé la compression » → le CPU va bien mais le débit chute
Symptôme : Moins d’écritures disque, mais débit de requêtes baisse et latence tail augmente.
Cause racine : La compression déplace le goulot vers le CPU, la bande passante mémoire ou des étapes de compression mono‑thread ; elle change aussi le pattern d’I/O.
Correction : Benchmarquez avec des données représentatives. Surveillez les hotspots par cœur. Utilisez des algos plus rapides, ajustez tailles d’enregistrement/bloc, ou compressez seulement les chemins froids.
7) « On a désactivé fsync pour la vitesse » → tout est rapide jusqu’à ce que ce ne soit plus le cas
Symptôme : Immense « amélioration » après un changement de config ; plus tard, corruption ou transactions perdues après un crash.
Cause racine : Les sémantiques de durabilité ont été sacrifiées pour la latence sans décision métier.
Correction : Restaurez la durabilité correcte, concevez un buffering/logging approprié et documentez les exigences de durabilité. Si vous devez relâcher la durabilité, faites‑le explicitement et de façon visible.
Checklists / plan étape par étape
Étape par étape : établir la vraie limite (période calme)
- Choisissez une charge qui compte (endpoint, job, requête) et définissez le succès (p95/p99, débit, taux d’erreur).
- Tracez le chemin critique : client → service → downstreams (DB, cache, stockage, réseau).
- Instrumentez les files : profondeur des worker pools, attente pool DB, PSI kernel, profondeur de queue disque.
- Lancez une montée de charge contrôlée : augmentez la charge offerte progressivement ; maintenez chaque palier.
- Trouvez le genou : là où le débit cesse de monter et la latence tail s’accélère.
- Enregistrez des baselines : point de genou, p99, métriques de saturation et contributeurs majeurs.
- Définissez des garde‑fous : limites de concurrence, timeouts, budgets de retry et backpressure.
- Retestez après modifications : déploiements, mises à jour kernel/firmware, changements de type d’instance, migration de stockage.
Checklist d’incident : évitez de thrasher et de conjecturer
- Arrêtez l’hémorragie : si la latence explose, limitez la concurrence ou activez le shedding. Protégez le système d’abord.
- Confirmez la portée : quels services, quels nœuds, quels tenants, quelles régions.
- Vérifiez les signaux d’erreur : timeouts, retries, retransmits TCP, erreurs I/O.
- Vérifiez les files : longueur file app, attente pool DB, run queue, profondeur de queue disque, PSI.
- Choisissez une hypothèse de goulot et collectez 2–3 mesures qui peuvent la falsifier.
- Changez une chose à la fois sauf en cas de confinement d’urgence.
- Consignez la timeline pendant que ça se passe. Le futur‑vous est un témoin peu fiable.
Règles de décision (opinionnées, parce que vous êtes occupé)
- Si p99 augmente alors que le débit reste plat, vous avez de la mise en file. Trouvez la file, n’ajoutez pas de travail.
- Si les erreurs augmentent (timeouts/retries), traitez la performance comme de la fiabilité. Réduisez la charge ; corrigez la contrainte ensuite.
- Si la latence du device monte alors que la bande passante est modérée, suspectez des écritures sync, writeback ou firmware/gestion d’énergie.
- Si le CPU est bas mais la latence élevée, suspectez de l’attente : I/O, locks, pools, retransmits réseau, GC.
- Si un cœur est saturé, arrêtez d’augmenter l’horizontal et trouvez la section sérielle ou le point de contention.
FAQ
1) Comment savoir rapidement si je suis limité par le CPU ou par l’I/O ?
Utilisez vmstat 1 et iostat -x 1. Un wa élevé et des tâches bloquées (b) indiquent des attentes I/O. Un r élevé avec un wa faible suggère une saturation CPU ou une contention de runnable.
2) Un %util disque élevé est‑il toujours mauvais ?
Non. Cela peut signifier que le device est occupé (correct) ou saturé avec des files profondes (mauvais). Regardez aqu-sz et await. Un disque occupé avec un await faible est sain ; un disque occupé avec un await élevé est un goulot.
3) Pourquoi ajouter des threads empire la latence alors que le CPU est idle ?
Parce que vous satuez probablement une ressource downstream partagée (connexions DB, queue disque, lock ou pool) et créez des files plus longues. Un CPU idle ne signifie pas de marge ; cela peut signifier que vous êtes bloqué ailleurs.
4) Quelle est la différence entre goulots de débit et goulots de latence tail ?
Les goulots de débit plafonnent le travail accompli ; les goulots de latence tail gâchent d’abord l’expérience utilisateur et peuvent survenir bien avant le débit maximal. Les problèmes tail sont souvent causés par la mise en file, le garbage collection, la gigue ou des pics de contention.
5) Le caching peut‑il « réparer » un goulot durablement ?
Le caching peut déplacer le goulot en réduisant le travail, mais il crée de nouveaux modes de défaillance : stampedes de cache, pression mémoire et complexité de cohérence. Traitez le cache comme un système d’ingénierie avec des limites et du backpressure, pas comme une couverture magique.
6) Comment mesurer le « point de genou » en sécurité en production ?
Monte la charge progressivement pendant une fenêtre à faible risque, isolez le trafic de test si possible et limitez la concurrence. Surveillez p95/p99, erreurs et métriques de saturation. Arrêtez quand les erreurs ou la latence tail s’accélèrent.
7) Pourquoi mes benchmarks sont bons mais la production est lente ?
Votre benchmark mesure probablement la mauvaise chose : I/O séquentielle au lieu d’aléatoire, QD=32 au lieu de QD=1, cache chaud vs cache froid, ou il ignore les sémantiques sync. De plus, la production a de la contention et des voisins bruyants. Benchmarquez la charge que vous exécutez réellement.
8) Quand monter en puissance vs optimiser ?
Montez en puissance quand vous avez une saturation claire d’une ressource qui scale linéairement avec la capacité (CPU pour le calcul parallèle, bande passante pour le streaming). Optimisez quand le goulot est de la coordination, de la latence tail ou de la contention ; le hardware corrige rarement ces cas.
9) Quelle est la façon la plus rapide d’éviter l’hystérie sur les goulots dans une équipe ?
Mettez-vous d’accord sur un petit set de mesures standard et un ordre de triage. Exigez des preuves : « iostat montre 20ms write await et queue de 40 » bat « on dirait que c’est le disque ». Intégrez‑le dans le template d’incident.
Étapes suivantes que vous pouvez réellement faire
Si vous voulez moins d’alarmes de performance, cessez de traiter les goulots comme du folklore. Traitez‑les comme des contraintes mesurables.
- Choisissez un parcours utilisateur critique et instrumentez les percentiles de latence, les erreurs et la concurrence.
- Ajoutez des métriques de files (files app, pools, PSI, queues device). Les goulots se cachent dans l’attente, pas dans l’exécution.
- Capturez un point de genou baseline sous charge contrôlée. Gardez‑le dans le runbook. Mettez‑le à jour après des changements significatifs.
- Pendant les incidents, suivez le playbook de diagnostic rapide et collectez des preuves falsifiables avant de changer quoi que ce soit.
- Implémentez du backpressure (caps de concurrence, budgets de retry, timeouts). Un système avec backpressure échoue de manière prévisible plutôt que théâtrale.
La limite réelle n’est jamais un mystère. Elle se cache juste derrière vos hypothèses, attendant que vous la mesuriez.