Vous l’avez vu : le service a reçu « plus de CPU » et est devenu plus lent. Ou le fournisseur a promis « deux fois plus de cœurs »,
et votre latence p99 a haussé les épaules comme si elle était payée au fixe. Pendant ce temps, vos tableaux de bord affichent beaucoup de marge—
sauf que les utilisateurs rafraîchissent la page, le canal d’incident est animé, et la personne en astreinte a ce regard particulier de mille yards.
Le tournant n’a pas été le jour où les CPU ont gagné des cœurs supplémentaires. Ce fut le jour où nous avons collectivement appris—souvent à la dure—que
les fréquences ne nous sauvaient plus. À partir de là, la performance est devenue une discipline d’ingénierie plutôt qu’une virée shopping.
Le tournant : des fréquences aux cœurs
Pendant longtemps, le travail sur la performance était essentiellement un flux d’achats. Vous achetiez la génération CPU suivante, elle tournait à
une fréquence plus élevée, et votre application s’améliorait comme par magie—même si votre code relevait du musée. Cette ère a pris fin lorsque la chaleur et
la consommation électrique sont devenues le facteur limitant, pas le nombre de transistors.
Une fois la croissance des fréquences ralentie, « plus rapide » s’est transformé en « plus parallèle ». Mais le parallélisme n’est pas un déjeuner gratuit ; c’est une facture qui
arrive tous les mois, détaillée en contentions de verrous, défauts de cache, bande passante mémoire, overhead du planificateur, latence résiduelle, et
« pourquoi ce thread est à 100% ? »
Voici la vérité inconfortable : les cœurs ne battent pas automatiquement les fréquences. Ils les battent seulement lorsque votre logiciel et votre
modèle opérationnel savent exploiter la concurrence sans se noyer dans les coûts de coordination.
Blague sèche #1 : Ajouter des cœurs à une appli lourde en verrous, c’est comme ajouter plus de caisses enregistreuses tout en gardant un seul caissier qui insiste pour valider chaque coupon lui-même.
Le « vrai tournant » est quand vous cessez de traiter le CPU comme un scalaire et commencez à considérer la machine comme un système :
CPU et mémoire et stockage et réseau et ordonnancement du noyau. En production, ces sous-systèmes ne se relaient pas poliment.
Faits et contexte à retenir
Ce sont des points courts et concrets qui vous évitent de répéter l’histoire. Pas des anecdotes. Des ancres.
-
L’échelle des fréquences a atteint un plafond au milieu des années 2000 car la densité de puissance et la dissipation thermique ont fait de « simplement augmenter l’horloge »
un problème de fiabilité et d’emballage, pas seulement un défi d’ingénierie. -
Le « Dennard scaling » n’a plus été votre ami : à mesure que les transistors rétrécissaient, la tension ne baissait plus au même rythme,
donc la puissance par surface augmentait et a imposé des choix de fréquence conservateurs. -
Le multicœur n’était pas un luxe, mais un contournement. Si vous ne pouvez pas augmenter la fréquence en toute sécurité, vous ajoutez des unités d’exécution parallèles.
Ensuite, vous transférez la complexité vers le logiciel. -
La loi d’Amdahl est devenue une réalité opérationnelle : la fraction sérielle de votre charge détermine votre plafond d’échelle,
et le trafic en production adore trouver le chemin sériel. -
La hiérarchie des caches est devenue un facteur de premier ordre. Le comportement L1/L2/L3, le trafic de cohérence de cache et les effets NUMA
dominent régulièrement les graphiques « utilisation CPU ». -
L’exécution spéculative et l’out-of-order ont acheté de la performance sans fréquences plus élevées, mais ont aussi augmenté la complexité et
la surface de vulnérabilité ; les mitigations ultérieures ont changé les profils de performance de manière mesurable. -
La virtualisation et les conteneurs ont changé la signification d’« un cœur ». L’ordonnancement vCPU, le steal time, la limitation CPU et les voisins bruyants
peuvent rendre un nœud 32 cœurs aussi fatigué qu’un portable. -
Le stockage est devenu plus rapide, mais la latence est restée têtue. NVMe a beaucoup amélioré les choses, pourtant la différence entre « rapide » et « lent » tient souvent à l’encombrement,
au comportement du système de fichiers et aux sémantiques de sync—pas aux spécifications brutes du dispositif.
Ce qui a réellement changé en production
1) « CPU » a cessé d’être le goulot et est devenu le messager
À l’époque de la montée des fréquences, l’utilisation CPU était un proxy raisonnable pour « nous sommes occupés ». À l’ère des cœurs, le CPU est souvent juste l’endroit où vous
remarquez le symptôme : un thread en boucle sur un verrou, une pause GC, un chemin noyau faisant trop de travail par paquet, ou une tempête d’appels système
causée par de petites E/S.
Si vous traitez un CPU élevé comme le problème, vous réglerez la mauvaise chose. Si vous traitez le CPU comme le messager, vous demanderez :
quel travail est accompli, pour qui, et pourquoi maintenant ?
2) La latence de queue est devenue la métrique qui compte
Les systèmes parallèles excellent à produire des moyennes qui ont l’air correctes pendant que les utilisateurs souffrent. Quand vous exécutez de nombreuses requêtes simultanément,
les plus lentes—détenteurs de verrous, traînards, caches froids, misses NUMA, pics de file disque—définissent l’expérience utilisateur et les timeouts.
Les cœurs amplifient la concurrence ; la concurrence amplifie l’encombrement ; l’encombrement amplifie le p99.
Vous pouvez livrer un système « plus rapide » qui est en réalité pire, si vos optimisations augmentent la variance. En d’autres termes : le débit gagne les démonstrations ; la stabilité gagne les pages d’astreinte.
3) Le planificateur du noyau est devenu partie de votre architecture applicative
Avec plus de cœurs, le scheduler a plus de choix—et plus d’occasions de vous nuire. La migration de threads peut détruire les caches.
Un mauvais placement des IRQ peut voler des cycles à vos threads chauds. Les cgroups et quotas CPU peuvent introduire une limitation
qui ressemble à une « latence mystérieuse ».
4) La mémoire et le NUMA ont cessé d’être des « sujets avancés »
Dès que vous avez plusieurs sockets, la mémoire n’est pas juste de la RAM ; c’est de la RAM locale versus de la RAM distante.
Un thread sur le socket 0 lisant de la mémoire allouée sur le socket 1 peut subir un impact mesurable, et cet impact se cumule quand vous
saturez la bande passante mémoire. Votre code peut être « lié au CPU » jusqu’à devenir « lié à la mémoire », et vous ne le remarquerez pas
en regardant seulement l’utilisation CPU.
5) La performance du stockage est devenue plus une question de coordination que de dispositifs
Les systèmes de stockage sont aussi parallèles : files d’attente, merges, readahead, writeback, journaling, copy-on-write, checksums.
Le dispositif peut être rapide tandis que le système est lent parce que vous avez créé la tempête parfaite de petites écritures synchrones,
de contention métadonnées, ou d’amplification d’écriture.
En tant qu’ingénieur stockage, je le dis sans détour : pour de nombreuses charges, le système de fichiers est la première dépendance de performance de votre base de données.
Traitez-le avec le même respect que votre planificateur de requêtes.
Une citation qui tient en opérations :
« L’espoir n’est pas une stratégie. »
— Général Gordon R. Sullivan
Goulots : où « plus de cœurs » disparaît
Sériel : Amdahl encaisse son loyer
Chaque système a une fraction sérielle : verrous globaux, leader unique, un seul thread de compaction, un seul écrivain WAL, un coordinateur de shard,
un mutex noyau dans un chemin chaud. Votre nombre de cœurs brillant augmente surtout le nombre de threads qui attendent leur tour.
Règle de décision : si ajouter de la concurrence améliore le débit mais aggrave la latence, vous avez probablement frappé un point de congestion sériel plus de l’encombrement.
Vous n’avez pas besoin de plus de cœurs. Vous devez réduire la contention ou sharder la ressource sérielle.
Contention de verrous et état partagé
L’état partagé est la taxe classique de la concurrence : mutex, rwlocks, atomics, allocateurs globaux, comptage de références, pools de connexions,
et « juste un petit verrou pour les métriques ». Parfois le verrou n’est pas dans votre code ; il est dans le runtime, l’allocateur libc, le noyau,
ou le système de fichiers.
Bande passante mémoire et cohérence de cache
Les CPU modernes sont rapides en calcul. Ils sont plus lents à attendre la mémoire. Ajoutez des cœurs et vous augmentez le nombre de bouches affamées
rivalisant pour la bande passante mémoire. Puis le trafic de cohérence de cache apparaît : les cœurs passent du temps à se mettre d’accord sur le sens d’une ligne de cache
au lieu de faire du travail utile.
NUMA : quand la « RAM » a une géographie
Les problèmes NUMA ressemblent souvent à de l’aléatoire : même requête, latence différente, selon quel cœur l’a exécutée et où vit sa mémoire.
Si vous ne fixez pas, n’allouez pas localement, ou ne choisissez pas une config consciente de la topologie, vous obtenez une « dérive de performance » qui va et vient.
Temps noyau : syscalls, changements de contexte et interruptions
Des taux élevés de context switch peuvent effacer les gains du parallélisme. Beaucoup de syscalls issus de petites E/S ou d’un logging bavard peuvent rendre un service
CPU-dépendant sans faire de travail métier. Des interruptions mal placées peuvent épingler la charge d’IRQ d’une file NIC entière sur les mêmes cœurs qui exécutent vos
threads sensibles à la latence.
E/S stockage : file d’attente et amplification d’écriture
Les cœurs ne servent à rien si vous êtes bloqué sur des patterns fsync synchrones, de petites écritures aléatoires, ou une pile de stockage qui amplifie les écritures
via copy-on-write et mises à jour de métadonnées. Pire : plus de cœurs peuvent émettre plus d’E/S concurrentes, augmentant la profondeur des files et la latence.
Réseau : traitement des paquets et temps softirq
Si vous faites du PPS élevé, le goulot peut être le traitement softirq, conntrack, règles iptables, ou TLS. Le CPU n’est pas « occupé »
par votre code ; il est occupé à aider la carte réseau.
Blague sèche #2 : La seule chose qui évolue linéairement dans ma carrière, c’est le nombre de tableaux de bord qui prétendent que tout va bien.
Mode d’intervention rapide
C’est l’ordre qui vous amène rapidement au goulot sans une semaine de danse interprétative dans Grafana. Le but n’est pas d’être
malin ; c’est d’être rapide et correct.
Première étape : établir ce qui sature (CPU vs mémoire vs I/O vs réseau)
- Vérifier la moyenne de charge versus les threads exécutables et l’attente I/O.
- Vérifier la répartition CPU (user/system/iowait/steal) et la limitation.
- Vérifier la latence disque et la profondeur des files ; confirmer si les attentes corrèlent avec le p99.
- Vérifier les drops/retransmits NIC et le temps CPU softirq si le service est orienté réseau.
Deuxième étape : déterminer si la limite est sérielle, partagée ou externe
- Un thread à 100% pendant que les autres sont inactifs : travail sériel ou verrou chaud.
- Tous les cœurs modérément occupés mais p99 mauvais : encombrement, contention ou stalls mémoire.
- CPU bas mais latence élevée : I/O ou dépendance externe.
- CPU élevé en noyau : réseau, syscalls, système de fichiers, ou interruptions.
Troisième étape : valider avec du profiling ciblé (pas des impressions)
- Utiliser
perf top/perf recordpour les chemins chauds CPU. - Utiliser les flame graphs si possible, mais même des traces de pile au bon moment aident.
- Utiliser
pidstatpour le CPU par thread et les changements de contexte. - Utiliser
iostatet les stats du système de fichiers pour la distribution de latence I/O et la saturation.
Quatrième étape : changer une variable, mesurer, revenir en arrière rapidement
- Réduire la concurrence et voir si la queue améliore (diagnostic d’encombrement).
- Fixer les threads / ajuster l’affinité IRQ si les effets de cache et de scheduler dominent.
- Changer la stratégie sync avec prudence (batch fsync, group commit) si c’est sûr.
- Scaler horizontalement quand vous avez prouvé que ce n’est pas une limite de coordination mono-nœud.
Tâches pratiques avec commandes : mesurer, interpréter, décider
Celles-ci sont délibérément « exécutables à 03:00 ». Chacune inclut : commande, sortie d’exemple, ce que cela signifie, et la décision à prendre.
Utilisez-les dans l’ordre quand vous êtes perdu.
Task 1: Check CPU saturation and iowait quickly
cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.5.0 (db01) 01/09/2026 _x86_64_ (32 CPU)
01:12:01 PM CPU %usr %nice %sys %iowait %irq %soft %steal %idle
01:12:02 PM all 42.10 0.00 12.40 8.60 0.00 2.10 0.20 34.60
01:12:02 PM 7 98.00 0.00 1.00 0.00 0.00 0.00 0.00 1.00
01:12:02 PM 12 10.00 0.00 40.00 30.00 0.00 5.00 0.00 15.00
Sens : Le CPU 7 est essentiellement saturé en espace user (probablement un thread chaud). Le CPU 12 passe beaucoup de temps en sys + iowait (noyau + attentes stockage).
Décision : Si un CPU est saturé, cherchez un goulot mono-thread ou un verrou. Si l’iowait est élevé, passez aux vérifications de latence disque avant d’ajuster le CPU.
Task 2: Separate runnable load from I/O load
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
3 1 0 421312 91240 812340 0 0 1024 2048 5400 9800 41 12 35 9 3
9 0 0 418900 91300 811900 0 0 120 250 6200 21000 55 15 25 2 3
Sens : r montre les threads exécutables ; b montre les bloqués (souvent I/O). La première ligne indique un certain blocage, la seconde indique une pression CPU (r=9).
Un cs élevé (context switches) suggère de la contention ou une concurrence trop bavarde.
Décision : Si r est constamment > nombre de CPU, vous êtes CPU-bound ou en thrashing. Si b augmente, priorisez l’investigation I/O.
Task 3: Spot CPU throttling in containers (cgroups)
cr0x@server:~$ cat /sys/fs/cgroup/cpu.stat
usage_usec 9034211123
user_usec 7011200456
system_usec 2023010667
nr_periods 120934
nr_throttled 23910
throttled_usec 882134223
Sens : La charge a été limitée pendant 23 910 périodes ; presque 882 secondes de temps throttled accumulé.
Cela peut ressembler à de la « latence aléatoire » et à du « CPU idle » simultanément.
Décision : Si la limitation est non triviale pendant les incidents, augmentez les limites/requests CPU, corrigez les voisins bruyants, ou cessez de supposer que « idle = disponible ».
Task 4: Check steal time on virtual machines
cr0x@server:~$ sar -u 1 3
Linux 6.5.0 (app03) 01/09/2026 _x86_64_ (8 CPU)
01:18:01 PM CPU %user %system %iowait %steal %idle
01:18:02 PM all 22.10 7.30 1.20 18.40 51.00
01:18:03 PM all 24.00 8.10 1.10 19.20 47.60
Sens : ~19% de steal signifie que l’hyperviseur prend du temps ; vous ne pouvez pas régler cela depuis l’invité.
Décision : Si le steal est élevé, migrez d’hôte, changez le type d’instance, ou réduisez la contention au niveau virtualisation.
Task 5: Identify per-thread CPU hogs and context switch storms
cr0x@server:~$ pidstat -t -p 2147 1 3
Linux 6.5.0 (api01) 01/09/2026 _x86_64_ (32 CPU)
01:21:10 PM UID TGID TID %usr %system %CPU cswch/s nvcswch/s Command
01:21:11 PM 1001 2147 2159 98.00 1.00 99.00 0.00 12.00 java
01:21:11 PM 1001 2147 2164 5.00 18.00 23.00 12000.00 8000.00 java
Sens : Un thread est lié au CPU (probablement une boucle chaude ou un goulot sériel). Un autre est lourd en temps noyau et en switching—souvent contention de verrous, syscalls, ou churn du scheduler.
Décision : Thread lié au CPU : profiler avec perf. Switching élevé : inspecter verrous, comportement de l’allocateur, logging, et hotspots noyau.
Task 6: Find CPU hotspots with perf (fast triage)
cr0x@server:~$ sudo perf top -p 2147
Samples: 2K of event 'cycles', Event count (approx.): 2289012345
38.12% libpthread-2.35.so [.] pthread_mutex_lock
14.55% libc-2.35.so [.] __memmove_avx_unaligned_erms
10.09% [kernel] [k] tcp_recvmsg
7.44% [kernel] [k] ext4_da_write_end
Sens : Une grosse part dans pthread_mutex_lock est une signature de contention. Les hotspots noyau suggèrent que la réception réseau et les chemins d’écriture du système de fichiers sont aussi pertinents.
Décision : Si le lock mutex domine, réduisez l’état partagé, augmentez le sharding, ou changez le modèle de concurrence. Si le noyau réseau domine, vérifiez softirq/IRQ et les taux de paquets.
Task 7: Confirm disk latency and queue depth
cr0x@server:~$ iostat -x 1 3
Linux 6.5.0 (db01) 01/09/2026 _x86_64_ (32 CPU)
Device r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
nvme0n1 120.0 950.0 4800 81200 154.0 9.80 11.2 3.1 12.3 0.7 82.0
Sens : Un avgqu-sz élevé et un await élevé indiquent de l’encombrement. %util à 82% suggère que le dispositif est occupé ; la latence augmentera sous rafales.
Décision : Si await augmente avec la charge, réduisez la pression d’écritures sync, regroupez les écritures, ajustez le système de fichiers/ZFS, ou déplacez les données/logs chauds vers des dispositifs plus rapides ou dédiés.
Task 8: See which processes are generating I/O
cr0x@server:~$ sudo iotop -o -b -n 3
Total DISK READ: 0.00 B/s | Total DISK WRITE: 58.23 M/s
PID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
2147 be/4 app 0.00 B/s 22.10 M/s 0.00 % 12.50 % java -jar api.jar
1762 be/4 postgres 0.00 B/s 31.90 M/s 0.00 % 18.00 % postgres: wal writer
Sens : Le WAL writer pousse des écritures importantes ; votre application écrit aussi beaucoup. Si les problèmes de latence corrèlent, vous pouvez être lié aux fsync ou au journaling.
Décision : Validez les patterns fsync et la configuration du stockage. Envisagez de déplacer WAL/logs sur un périphérique séparé, ou d’ajuster les paramètres de commit en gardant à l’esprit les exigences de durabilité.
Task 9: Check filesystem and mount options (the boring truth)
cr0x@server:~$ findmnt -no SOURCE,TARGET,FSTYPE,OPTIONS /var/lib/postgresql
/dev/nvme0n1p2 /var/lib/postgresql ext4 rw,noatime,data=ordered
Sens : noatime évite des écritures métadonnées supplémentaires. Ext4 en mode ordered est généralement sensé pour des bases sur Linux.
Décision : Si vous voyez des options surprenantes (comme sync ou des barrières étranges désactivées sans raison), corrigez-les. Ne cargo-cultivez pas des flags de performance.
Task 10: Check ZFS pool health and latency pressure (if you run it)
cr0x@server:~$ sudo zpool iostat -v tank 1 3
capacity operations bandwidth
pool alloc free read write read write
---------- ----- ----- ----- ----- ----- -----
tank 1.20T 2.30T 210 1800 8.20M 95.1M
mirror 1.20T 2.30T 210 1800 8.20M 95.1M
nvme1n1 - - 110 920 4.10M 47.6M
nvme2n1 - - 100 880 4.10M 47.5M
Sens : Un nombre élevé d’opérations d’écriture par rapport à la bande passante implique des petites écritures. Les mirrors peuvent le gérer, mais la latence dépend du comportement sync et de la présence d’un SLOG.
Décision : Si les petites écritures sync dominent, évaluez un SLOG séparé, le recordsize, et le batching des fsync côté application—sans compromettre la durabilité.
Task 11: Detect memory pressure and reclaim thrash
cr0x@server:~$ sar -B 1 3
Linux 6.5.0 (cache01) 01/09/2026 _x86_64_ (16 CPU)
01:33:20 PM pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
01:33:21 PM 0.0 81234.0 120000.0 12.0 90000.0 0.0 54000.0 39000.0 72.2
Sens : Un scan massif de pages et un pgpgout élevé suggèrent une pression de reclaim ; des fautes majeures indiquent du swapping réel sur disque.
Décision : Si le reclaim est actif pendant les pics de latence, réduisez l’empreinte mémoire, corrigez la taille des caches, ou migrez vers des nœuds avec plus de RAM. Plus de cœurs n’aideront pas.
Task 12: Check NUMA locality problems
cr0x@server:~$ numastat -p 2147
Per-node process memory usage (in MBs) for PID 2147 (java)
Node 0 18240.3
Node 1 2240.8
Total 20481.1
Sens : La mémoire est fortement concentrée sur le Node 0 ; si les threads tournent sur les deux sockets, les threads du Node 1 accéderont souvent à de la mémoire distante.
Décision : Si le déséquilibre NUMA corrèle avec la latence, envisagez d’affecter le processus à un socket, d’activer l’allocation NUMA-aware, ou d’ajuster le placement des threads.
Task 13: Check interrupt distribution and softirq load
cr0x@server:~$ cat /proc/interrupts | head -n 8
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
24: 9123401 102332 99321 88210 90111 93321 92011 88712 PCI-MSI 524288-edge eth0-TxRx-0
25: 10231 8231201 99221 88120 90211 93411 92101 88602 PCI-MSI 524289-edge eth0-TxRx-1
NMI: 2012 1998 2001 2003 1999 2002 1997 2004 Non-maskable interrupts
Sens : Les IRQs sont concentrées sur CPU0 et CPU1 pour des files séparées. Ce n’est pas toujours mauvais, mais si vos threads chauds partagent ces CPU, vous aurez de la gigue.
Décision : Si les threads sensibles à la latence et les CPU chargés en IRQ se chevauchent, fixez l’affinité IRQ pour les isoler, ou déplacez les threads applicatifs loin des CPU IRQ.
Task 14: Confirm TCP retransmits and drops (network-induced latency)
cr0x@server:~$ netstat -s | egrep -i 'retrans|segments retransmited|listen drops|RTO' | head
124567 segments retransmited
98 timeouts after RTO
1428 SYNs to LISTEN sockets dropped
Sens : Les retransmissions et RTO créent de la latence queue qui ressemble à « l’appli est devenue lente ». Les SYN drops peuvent ressembler à des échecs de connexion aléatoires sous charge.
Décision : Si les retransmissions grimpent pendant les incidents, vérifiez la saturation NIC, les réglages de file, la santé du load balancer, conntrack, et la perte de paquets en amont.
Task 15: Measure file descriptor pressure (hidden serialization)
cr0x@server:~$ cat /proc/sys/fs/file-nr
24576 0 9223372036854775807
Sens : Le premier nombre est le nombre de handles de fichiers alloués ; près des limites vous verrez des échecs et des retries qui créent des patterns de contention étranges.
Décision : Si vous approchez des limites, augmentez-les et corrigez les fuites. Ne laissez pas une pénurie de descripteurs masquer un problème de scalabilité CPU.
Task 16: Spot one-core bottlenecks in application metrics using top
cr0x@server:~$ top -H -p 2147 -b -n 1 | head -n 12
top - 13:41:01 up 12 days, 3:22, 1 user, load average: 6.20, 5.90, 5.10
Threads: 98 total, 2 running, 96 sleeping, 0 stopped, 0 zombie
%Cpu(s): 45.0 us, 12.0 sy, 0.0 ni, 35.0 id, 8.0 wa, 0.0 hi, 0.0 si, 0.0 st
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2159 app 20 0 9856.2m 3.1g 41224 R 100.0 9.8 32:11.20 java
2164 app 20 0 9856.2m 3.1g 41224 S 23.0 9.8 10:05.88 java
Sens : Un thread qui saturе un cœur est l’exemple type de « les cœurs n’aideront pas ». Ce thread est votre plafond de débit et votre falaise de latence.
Décision : Profilez-le, puis redessinez : scindez le travail, réduisez la portée des verrous, shardez l’état, ou déplacez le travail hors du chemin de la requête.
Trois mini-récits d’entreprise (anonymisés, plausibles et techniquement exacts)
Mini-récit n°1 : Un incident causé par une mauvaise hypothèse
Une entreprise SaaS de taille moyenne a déplacé sa couche API d’anciennes instances 8 cœurs vers de nouvelles instances 32 cœurs. Même mémoire. Même classe de stockage.
Le plan de migration était simple : remplacer les nœuds, garder les mêmes seuils d’auto-scaling, profiter du coût par cœur inférieur.
Le premier jour ouvrable après la bascule, les taux d’erreur ont lentement augmenté. Pas catastrophiquement—juste assez pour déclencher des retries clients.
La latence a monté, puis plafonné, puis monté de nouveau. Les tableaux de bord disaient que le CPU était « correct » : 40–55% à travers la flotte.
Le commandant d’incident a posé la question classique : « Pourquoi sommes-nous lents si le CPU n’est qu’à moitié utilisé ? »
La mauvaise hypothèse était que le pourcentage CPU se mappe linéairement à la capacité. Ce qui s’est passé était plus subtil : l’application avait un verrou global
protégeant une structure de cache utilisée à chaque requête. Sur des machines 8 cœurs le verrou était ennuyeux. Sur des machines 32 cœurs il est devenu
un festival de contention. Plus de cœurs signifiaient plus de prétendants simultanés, des taux de context switch plus élevés, et des temps d’attente plus longs par requête.
Le débit n’a pas augmenté ; la latence tail a.
La correction n’a pas été « ajouter du CPU ». La correction a été de sharder le cache par keyspace et d’utiliser du lock striping. Ils ont aussi baissé temporairement la limite de concurrence en ingress—moins de débit sur le papier, meilleur p99 en réalité—jusqu’à ce que le changement de code soit déployé.
La leçon retenue : les cœurs amplifient à la fois votre parallélisme et vos frais de coordination. Si vous ne mesurez pas la contention, vous devinez.
Et deviner, c’est la manière dont on reçoit des pages.
Mini-récit n°2 : Une optimisation qui s’est retournée contre eux
Une équipe plateforme de données voulait réduire la latence d’écriture pour un service d’ingestion. Ils ont constaté que leur base dépensait du temps à flush et sync.
Quelqu’un a proposé un « gain rapide » : déplacer les logs et le WAL sur le même volume NVMe rapide utilisé pour les données, et augmenter les threads workers pour « utiliser tous les cœurs ».
Le changement a été déployé progressivement, avec un petit test qui semblait positif : débit plus élevé, latence moyenne plus basse.
Deux semaines plus tard, lors d’un pic de trafic prévisible, le système a commencé à timeout. Pas uniformément—juste assez pour faire mal. Les graphes de nœud montraient
le %util disque grimper. iostat montrait la profondeur de file augmenter. Le CPU restait disponible. Les ingénieurs ont monté les pools de threads pour « forcer le passage ».
Ça a empiré.
Le retour de flamme était un effondrement par encombrement : plus de threads ont généré plus de petites écritures sync, ce qui a augmenté la profondeur des files du périphérique, ce qui a augmenté
la latence par opération, ce qui a augmenté le nombre d’opérations en vol, ce qui a augmenté encore la profondeur de file. Une boucle de rétroaction.
Le débit moyen avait l’air acceptable, mais le p99 a explosé parce que certaines requêtes se retrouvaient derrière de longues files I/O.
La correction a été volontairement ennuyeuse : limiter la concurrence, séparer les dispositifs WAL/logs, et implémenter du batching pour grouper les appels fsync.
Ils ont aussi ajouté des alertes sur await et la profondeur de file, pas seulement sur le débit. Après cela, le système a géré les pics sans drame.
La leçon : « utiliser tous les cœurs » n’est pas un objectif. C’est un risque. La concurrence est un réglage, et la bonne valeur dépend de la ressource partagée la plus lente.
Mini-récit n°3 : Une pratique ennuyeuse mais correcte qui a sauvé la mise
Une fintech exploitait un service de règlement qui faisait beaucoup de lectures, des écritures modérées, et nécessitait une durabilité stricte pour un sous-ensemble d’opérations.
Ils avaient une habitude qui ne gagnerait pas de prix au hackathon : chaque trimestre ils faisaient une répétition de capacité et de modes de défaillance sous charge proche de la production,
avec runbooks stricts et règle « pas d’héroïsme ».
Pendant une répétition, ils ont remarqué quelque chose de pas sexy : la latence p99 augmentait à l’approche du débit maximal, même si le CPU semblait correct.
Ils ont collecté pidstat, iostat et des profils perf et ont trouvé une légère contention de verrous plus une montée de profondeur de file stockage
lors de rafales périodiques ressemblant à des checkpoints. Rien n’était « cassé », juste proche du bord.
Ils ont fait deux changements : (1) épingler des pools de workers spécifiques sur des CPU éloignés des cœurs IRQ NIC, et (2) ajuster le layout de stockage pour que le journal durable
vive sur un périphérique séparé avec une latence prévisible. Ils ont aussi défini des SLO explicites sur le p99 et ajouté des alertes sur la limitation et le steal time.
Des mois plus tard, un vrai pic de trafic a frappé pendant un événement voisin bruyant au niveau virtualisation. Leurs systèmes se sont dégradés,
mais sont restés dans les SLO assez longtemps pour réduire la charge gracieusement. D’autres équipes ont eu des incidents ; la leur a eu un thread Slack
et un postmortem sans adrénaline.
La leçon : les pratiques ennuyeuses sont ce qui rend les cœurs utilisables. Répétez, mesurez les bonnes choses, et vous verrez la falaise avant d’y tomber.
Erreurs courantes : symptôme → cause racine → correctif
1) Symptom: CPU is “only 50%” but latency is awful
Cause racine : goulot mono-thread, contention de verrous, ou limitation cgroup masquant une saturation réelle.
Correctif : Utiliser top -H/pidstat -t pour trouver le thread chaud ; utiliser perf top pour repérer un lock ou une boucle chaude.
Vérifier /sys/fs/cgroup/cpu.stat pour la limitation. Redessiner le chemin sériel ; n’ajoutez pas simplement des instances.
2) Symptom: Throughput increases with more threads, then suddenly collapses
Cause racine : effondrement par file d’attente sur l’I/O ou une dépendance en aval ; la concurrence dépasse la zone stable d’exploitation du service.
Correctif : Limiter la concurrence, ajouter du backpressure, et mesurer la profondeur de file/await. Ajuster les pools de threads à la baisse jusqu’à stabiliser le p99.
3) Symptom: Random p99 spikes after moving to larger multi-socket machines
Cause racine : effets NUMA et problèmes de localité de cache ; les threads migrent et accèdent à de la mémoire distante.
Correctif : Vérifier numastat. Épingler les processus ou utiliser des allocateurs NUMA-aware. Garder les services sensibles à la latence dans un socket si possible.
4) Symptom: CPU system time climbs with traffic, but app code didn’t change
Cause racine : syscalls et overhead noyau provenant du réseau, de petites E/S, du logging, ou de la rotation des métadonnées du système de fichiers.
Correctif : Utiliser perf top pour voir les symboles noyau, vérifier la distribution des interruptions, réduire le taux de syscalls (batch, buffer, async),
et réévaluer le volume de logging et les politiques de flush.
5) Symptom: High iowait and “fast disks”
Cause racine : file d’attente du périphérique, patterns d’écriture sync, amplification d’écriture (copy-on-write, petits blocs), ou partage d’un périphérique avec une autre charge.
Correctif : Confirmer avec iostat -x et iotop. Séparer WAL/logs, batcher les fsync, régler le record size du système de fichiers,
et s’assurer que le dispositif sous-jacent n’est pas sur-utilisé.
6) Symptom: Scaling out adds nodes but not capacity
Cause racine : dépendance centralisée (écrivain DB unique, hotspot d’élection de leader, cache partagé, module en aval limité).
Correctif : Identifier la dépendance partagée, la sharder ou la répliquer correctement, et s’assurer que les clients distribuent la charge équitablement.
« Plus de pods sans état » ne résoudra pas un point d’étranglement stateful.
7) Symptom: Performance is worse after “making it more concurrent”
Cause racine : augmentation de la contention et du trafic de cohérence de cache ; plus de threads causent plus d’écritures partagées et de false sharing.
Correctif : Réduire l’état partagé, éviter les compteurs atomiques chauds sur le chemin de la requête, utiliser du batching par cœur/par thread, et profiler la contention.
Listes de contrôle / plan étape par étape
Étape par étape : prouver si les cœurs aideront
-
Mesurez la latence tail sous charge (p95/p99) et corrélez avec la répartition CPU (usr/sys/iowait/steal).
Si le p99 se dégrade alors que le CPU est « disponible », suspectez la contention ou des attentes externes. -
Trouvez le thread limitant ou le verrou :
lanceztop -Hetpidstat -tpour localiser les threads chauds et les tempêtes de switching. -
Profilez avant de tuner :
utilisezperf toppour identifier les fonctions principales (verrous, memcpy, syscalls, chemins noyau). -
Vérifiez la latence I/O et la profondeur des files :
iostat -xetiotoppour confirmer si le stockage est l’élément limitant. -
Vérifiez les limites CPU artificielles :
la limitation cgroup, le steal time, et les contraintes du scheduler peuvent imiter du « mauvais code ». -
Validez la NUMA et le placement IRQ :
confirmer la localité avecnumastat, confirmer la distribution des interruptions avec/proc/interrupts. -
Décidez ensuite seulement :
- Si le CPU est vraiment saturé en user time sur les cœurs : plus de cœurs (ou des cœurs plus rapides) peuvent aider.
- Si la contention domine : redessinez la concurrence ; des cœurs supplémentaires peuvent empirer la situation.
- Si l’I/O domine : corrigez le chemin stockage ; des cœurs supplémentaires ne feront que générer plus d’attente.
- Si le réseau/noyau domine : ajustez IRQ, offloads et chemin paquets ; envisagez des cœurs plus rapides.
Checklist opérationnelle : rendre le comportement multicœur prévisible
- Définir des limites de concurrence explicites (par instance) et les traiter comme des contrôles de capacité, pas comme des « rustines temporaires ».
- Alertes sur la limitation CPU et le steal time ; ce sont des tueurs de capacité silencieux.
- Suivre
awaitdisque et la profondeur de file ; le débit seul est un menteur. - Mesurer les changements de contexte et la longueur de la run-queue ; des valeurs élevées précèdent souvent la douleur p99.
- Garder l’état chaud sharded ; ne centralisez pas compteurs et maps sur le chemin de la requête.
- Séparer les logs sensibles à la durabilité des données volumineuses quand c’est possible.
- Valider le placement NUMA sur les machines multi-socket ; épingler si vous avez besoin de déterminisme.
- Répéter des charges de pointe avec des données proches de la production ; le chemin sériel se révélera.
FAQ
1) Should I prefer higher clock speed or more cores for latency-sensitive services?
Préférez une performance par cœur plus élevée quand vous avez un composant sériel connu, un traitement noyau/réseau lourd, ou du code sensible aux verrous.
Plus de cœurs aident lorsque la charge est embarrassingly parallel et que l’état partagé est minimal.
2) Why does CPU utilization look low when the service is timing out?
Parce que le service peut attendre : des verrous, de l’I/O, des appels en aval, ou être limité par des cgroups. Aussi, « faible moyenne CPU »
peut cacher un cœur saturé. Regardez toujours les vues par-cœur et par-thread.
3) What’s the quickest way to detect lock contention?
Sur Linux, perf top montrant pthread_mutex_lock (ou des chemins futex dans le noyau) est un signal fort.
Associez-le à pidstat -t pour les changements de contexte et au CPU par-thread pour trouver le coupable.
4) How do I know if I’m I/O-bound versus CPU-bound?
Si iostat -x montre une montée de await et de la profondeur de file lors des pics de latence, vous êtes probablement I/O-bound.
Si vmstat montre des threads exécutables élevés et un iowait bas, vous êtes probablement CPU-bound ou contention-bound.
5) Can adding more threads reduce latency?
Parfois, pour des charges I/O-heavy où la concurrence masque le temps d’attente. Mais une fois que vous touchez un goulot partagé, plus de threads augmentent l’encombrement
et la variance. Le bon geste est souvent une concurrence limitée avec backpressure.
6) What’s the most common “cores vs clocks” trap in Kubernetes?
Les limites CPU causant de la limitation (throttling). Le pod peut afficher « utilisation CPU sous la limite », mais être quand même throttlé par rafales, créant des pics de latence.
Vérifiez /sys/fs/cgroup/cpu.stat dans le conteneur et corrélez avec la latence des requêtes.
7) Why do bigger machines sometimes perform worse than smaller ones?
Effets NUMA, migration du scheduler, et localité de cache. De plus, les machines plus grandes attirent souvent plus de workloads co-localisés, augmentant la contention
sur des ressources partagées comme la bande passante mémoire et l’I/O.
8) Is storage still relevant if I’m on NVMe?
Tout à fait. NVMe améliore la latence et le débit de base, mais l’encombrement existe toujours, les sémantiques sync existent toujours, et les systèmes de fichiers font toujours du travail.
Si vous générez beaucoup de petites écritures sync, NVMe vous permettra simplement d’atteindre le mur de file plus rapidement.
9) What metrics should I put on a dashboard to reflect the “core era” reality?
CPU par cœur, steal CPU, limitation CPU, changements de contexte, longueur de run queue, attente disque et profondeur de file, retransmissions réseau, et latence p95/p99.
Les moyennes sont acceptables, mais seulement en tant qu’acteurs secondaires.
Prochaines étapes réalisables cette semaine
Si vous voulez des systèmes qui tirent profit de plus de cœurs au lieu d’en être ridiculisés, faites ce qui suit—pratiquement, pas en mode aspirationnel.
-
Ajoutez un panneau de dashboard montrant le CPU par cœur et les threads principaux (ou exportez le CPU par thread pour le processus principal).
Attrapez le plafond mono-cœur tôt. - Alarmez sur la limitation CPU et le steal time. Si vous êtes en conteneurs ou VM et que vous n’alertez pas sur ces métriques, vous choisissez la surprise.
-
Suivez le disque
awaitet la profondeur de file aux côtés de la latence p99. Si vous ne suivez que le débit, vous optimisez le mauvais type de succès. -
Faites un « drill goulot » d’une heure : sous une charge contrôlée, capturez
mpstat,vmstat,iostat,pidstat, et un court échantillonperf.
Notez les trois facteurs limitants principaux. Répétez trimestriellement. - Définissez des limites de concurrence explicites pour vos services les plus chargés. Traitez la limite comme un contrôle de stabilité ; ajustez-la comme un disjoncteur.
Le vrai tournant n’était pas les CPU multicœurs. Ce fut le moment où nous avons dû arrêter de faire confiance aux fréquences pour couvrir nos péchés.
Si vous mesurez la contention, l’encombrement et la localité—et si vous acceptez de baisser la concurrence quand cela aide—les cœurs battront les fréquences.
Sinon, ce seront eux qui vous battront.