Vous avez acheté un beau serveur double-socket parce que « deux fois plus de CPU » sonne comme « deux fois plus de débit ». Puis votre p95 de base de données s’est dégradé, votre pile de stockage a commencé à hoqueter, et l’équipe perf est arrivée avec des graphiques et une déception contenue.
Ceci est NUMA : Non-Uniform Memory Access. Ce n’est pas un bug. C’est la facture que vous payez pour avoir fait comme si un serveur était une grande piscine heureuse de CPU et de RAM.
Ce qu’est réellement NUMA (et ce que ce n’est pas)
NUMA signifie que la machine est physiquement composée de plusieurs « domaines de localité » (nœuds NUMA). Chaque nœud est un morceau de cœurs CPU et de mémoire qui sont proches les uns des autres. Accéder à la mémoire locale est relativement rapide. Accéder à la mémoire rattachée à l’autre socket est plus lent. Le « relativement » compte parce qu’il ne s’agit pas seulement de latence ; c’est aussi de la contention sur l’interconnexion inter-socket.
Sur un serveur x86 dual-socket moderne typique :
- Chaque socket a ses propres contrôleurs et canaux mémoire. Les barrettes DRAM sont câblées sur ce socket.
- Les sockets sont reliés par une interconnexion (Intel UPI, AMD Infinity Fabric, ou similaire).
- Les périphériques PCIe sont aussi physiquement attachés à un socket via des root complexes. Une carte réseau ou un NVMe est « plus proche » d’un socket que de l’autre.
Donc NUMA n’est pas « une option de tuning ». C’est une topologie. L’ignorer revient à laisser le noyau faire le gendarme du trafic alors que vous créez activement des embouteillages.
Aussi : NUMA n’est pas automatiquement une catastrophe. NUMA fonctionne bien quand la charge respecte la localité, que l’ordonnanceur a une chance, et que vous ne la sabotez pas avec un mauvais pinning ou un mauvais placement mémoire.
Une citation à garder : « L’espoir n’est pas une stratégie. » — Gen. Gordon R. Sullivan. En territoire NUMA, « espérer que le kernel s’en sorte » reste de l’espoir.
Pourquoi le double-socket n’est pas « deux fois plus rapide »
Parce que le goulot d’étranglement a changé. Ajouter un second socket augmente la capacité de calcul et la mémoire totale, mais augmente aussi les façons d’être lent. Vous ne doublez pas une seule ressource ; vous assemblez deux ordinateurs et dites à Linux de faire comme si c’était un seul.
1) La localité mémoire devient une dimension de performance
Sur une machine mono-socket, la mémoire « locale » d’un cœur est essentiellement la même. Dans une machine dual-socket, la mémoire a une adresse, et cette adresse a une « maison ». Quand un thread sur le socket 0 lit et écrit fréquemment des pages allouées depuis le nœud 1, chaque miss de cache devient un aller-retour inter-socket. L’accès inter-socket n’est pas gratuit ; c’est une route à péage aux heures de pointe.
2) Le trafic de cohérence de cache augmente
Le multi-socket nécessite la cohérence entre sockets. Si vous avez des structures partagées avec beaucoup d’écritures (verrous, files, compteurs « chauds », métadonnées d’allocateur), vous obtenez du ping-pong inter-socket. Parfois le CPU est « occupé » mais le travail utile par cycle chute.
3) La localité PCIe compte plus que vous ne le pensez
Votre NIC est connectée à un socket spécifique. Votre contrôleur NVMe est connecté à un socket spécifique. Si votre charge tourne principalement sur l’autre socket, vous avez inventé un saut interne supplémentaire pour chaque paquet ou complétion IO. Sur des systèmes à haut IOPS ou fort taux de paquets, ce saut devient une taxe.
4) Vous pouvez facilement empirer les choses avec des « optimisations »
Pinning des threads semble discipliné jusqu’au moment où vous épinglez les CPU sans épingler la mémoire, ou que vous épinglez les interruptions sur le mauvais socket, ou que vous forcez tout sur le nœud 0 parce que « ça marchait en staging ». La mauvaise configuration NUMA est un problème rare où faire quelque chose est souvent pire que de ne rien faire.
Blague #1 (courte, pertinente) : Les serveurs double-socket sont comme les bureaux ouverts : vous avez gagné de la « capacité », mais maintenant tout ce qui importe implique de traverser la pièce.
Faits et historique utiles au travail
Ce ne sont pas des faits pour une soirée quiz. Ce sont ceux que vous utilisez pour arrêter une mauvaise décision d’achat ou gagner un argument avec quelqu’un qui brandit un tableur.
- NUMA précède votre cloud. Les designs NUMA commerciaux existaient il y a des décennies ; l’idée est plus ancienne que la plupart des outils modernes de performance.
- Le SMP a cessé d’évoluer « gratuitement ». L’accès mémoire uniforme était plus simple, mais a heurté des limites physiques et électriques quand le nombre de cœurs a augmenté et que la bande passante mémoire n’a pas suivi linéairement.
- Les contrôleurs mémoire intégrés ont tout changé. Le fait de déplacer les contrôleurs mémoire sur le CPU a amélioré la latence mémoire, mais a aussi rendu la question « quel CPU possède la mémoire » inévitable.
- Les liaisons inter-socket ont évolué, mais restent plus lentes que la DRAM locale. UPI/Infinity Fabric sont rapides, mais ne remplacent pas les canaux locaux. Elles transportent aussi du trafic de cohérence.
- NUMA n’est pas que de la mémoire. Linux utilise la même topologie pour penser les périphériques PCIe, les interruptions et les domaines d’ordonnancement. La localité est une propriété de toute la machine.
- La virtualisation n’a pas supprimé NUMA ; elle l’a rendu plus facile à masquer. Les hyperviseurs peuvent exposer un NUMA virtuel, mais vous pouvez aussi accidentellement construire une VM qui traverse les sockets et ensuite vous demander pourquoi elle saccade.
- Transparent Huge Pages interagit avec NUMA. THP peut réduire la charge TLB, mais peut aussi rendre le placement et la migration de pages plus coûteux quand le système est sous pression.
- Les décisions de placement au démarrage comptent. Beaucoup d’allocateurs et de services allouent beaucoup de mémoire au démarrage ; « où ça tombe » peut déterminer les performances pendant des heures.
- Les piles de stockage sont sensibles à NUMA à haut débit. Les files NVMe, le traitement softirq et les boucles de polling en userspace adorent la localité ; traverser les sockets ajoute du jitter et réduit le plafond.
Comment ça échoue en production : modes de défaillance
Les problèmes NUMA n’ont généralement pas l’apparence de « problème NUMA ». Ils ressemblent à :
- p95 et p99 de latence qui montent pendant que le débit moyen semble « correct ».
- CPU élevé mais IPC bas et la machine donne l’impression de tourner dans la boue.
- Un socket est surchargé, l’autre est au repos parce que l’ordonnanceur a fait ce que vous avez demandé, pas ce que vous vouliez.
- Les accès mémoire distants augmentent et vous payez des allers-retours inter-socket pour chaque miss de cache.
- Déséquilibre IRQ qui fait s’accumuler le traitement réseau ou les complétions NVMe sur les mauvais cœurs.
- Performance instable sous charge parce que la migration de pages et le reclaim se déclenchent quand la mémoire est déséquilibrée entre nœuds.
Les problèmes de performance NUMA sont souvent multiplicatifs. L’accès mémoire distant ajoute de la latence ; cela augmente les temps de verrouillage ; cela augmente la contention ; cela augmente les changements de contexte ; cela augmente les misses de cache ; cela augmente les accès distants. Cette spirale explique pourquoi « hier c’était bien » est une ouverture courante.
Mode opératoire de diagnostic rapide (premier/deuxième/troisième)
Quand le système est lent et que vous soupçonnez NUMA, ne commencez pas par des benchmarks héroïques. Commencez par la topologie et le placement. Vous cherchez à répondre à une question : les CPU, la mémoire et les chemins IO sont-ils sur le même nœud pour le travail chaud ?
Premier : confirmer la topologie et si vous traversez des sockets
- Combien de nœuds NUMA existent ?
- Quels CPU appartiennent à chaque nœud ?
- La charge est-elle épinglée ou contrainte par cgroups/cpuset ?
Second : vérifier la localité mémoire et les accès distants
- La majeure partie de la mémoire est-elle allouée sur le nœud 0 alors que les threads tournent sur le nœud 1 (ou inversement) ?
- Les compteurs « numa miss » et « foreign » augmentent-ils ?
- Le noyau migre-t-il beaucoup de pages ?
Troisième : vérifier la localité PCIe et des interruptions
- Où est attachée la NIC / le NVMe (NUMA node) ?
- Ses interruptions arrivent-elles sur des CPU du même nœud ?
- Les files sont-elles réparties sensiblement sur des cœurs proches du périphérique ?
Si vous faites ces trois étapes, vous trouverez la cause d’une grande fraction des régressions mystérieuses sur dual-socket en moins de 20 minutes. Pas toutes. Assez pour sauver votre week-end.
Pratique : tâches NUMA avec commandes
Ce sont les tâches que j’exécute réellement quand quelqu’un dit « les nouveaux serveurs dual-socket sont plus lents ». Chaque tâche inclut la commande, un exemple de sortie, ce que cela signifie, et la décision que vous en tirez.
Task 1: See NUMA nodes, CPU mapping, and memory size
cr0x@server:~$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
node 0 size: 257728 MB
node 0 free: 18240 MB
node 1 cpus: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
node 1 size: 257728 MB
node 1 free: 214912 MB
node distances:
node 0 1
0: 10 21
1: 21 10
Ce que cela signifie : Deux nœuds. La matrice de distances montre que l’accès distant coûte plus cher que l’accès local. Notez le déséquilibre de mémoire libre : le nœud 0 est serré ; le nœud 1 est majoritairement libre.
Décision : Si votre charge tourne principalement sur les CPU 0–15 et que le nœud 0 est presque plein, vous êtes à haut risque d’allocations distantes ou de reclaim. Planifiez de rebalancer le placement mémoire ou le placement CPU.
Task 2: Check which node a PCIe device is local to
cr0x@server:~$ cat /sys/class/net/ens5f0/device/numa_node
1
Ce que cela signifie : La NIC est attachée au nœud NUMA 1.
Décision : Pour des charges à fort taux de paquets, préférez exécuter les threads réseau sur les CPU du nœud 1, et orientez les IRQs là-bas.
Task 3: Map NVMe devices to NUMA nodes
cr0x@server:~$ for d in /sys/class/nvme/nvme*; do echo -n "$(basename $d) "; cat $d/device/numa_node; done
nvme0 0
nvme1 0
Ce que cela signifie : Les deux contrôleurs NVMe sont locaux au nœud 0.
Décision : Gardez vos threads de soumission/complétion IO les plus chauds sur le nœud 0 si vous visez une faible latence, ou au moins alignez les cœurs de traitement IO avec le nœud 0.
Task 4: See per-node memory allocations and NUMA hit/miss counters
cr0x@server:~$ numastat
node0 node1
numa_hit 1245067890 703112340
numa_miss 85467123 92133002
numa_foreign 92133002 85467123
interleave_hit 10234 11022
local_node 1231123456 688000112
other_node 100000000 110000000
Ce que cela signifie : Des numa_miss et other_node non triviaux indiquent une utilisation mémoire distante. Un peu de distant est normal ; beaucoup indique un mauvais placement.
Décision : Si le trafic distant augmente pendant la fenêtre de ralentissement, concentrez-vous sur l’alignement CPU/mémoire ou la pression de reclaim/migration avant de toucher le code applicatif.
Task 5: Inspect a process’s NUMA memory map (per-node RSS)
cr0x@server:~$ pidof postgres
2481
cr0x@server:~$ numastat -p 2481
Per-node process memory usage (in MBs) for PID 2481 (postgres)
Node 0 Node 1 Total
--------------- --------------- ---------------
Private 62048.1 1892.0 63940.1
Heap 41000.0 256.0 41256.0
Stack 32.0 16.0 48.0
Huge 0.0 0.0 0.0
---------------- --------------- --------------- ---------------
Total 62080.1 1924.0 64004.1
Ce que cela signifie : La mémoire de ce processus est massivement sur le nœud 0.
Décision : Assurez-vous que les threads Postgres les plus sollicités s’exécutent principalement sur les CPU du nœud 0, ou liez explicitement l’allocation mémoire au nœud où vous voulez tourner.
Task 6: Check CPU affinity of a process (did someone pin it?)
cr0x@server:~$ taskset -cp 2481
pid 2481's current affinity list: 16-31
Ce que cela signifie : Le processus est épinglé sur les CPU du nœud 1, mais dans la Task 5 sa mémoire est sur le nœud 0. C’est la douleur classique d’accès distant.
Décision : Soit déplacez le processus vers les CPU 0–15, soit rétablissez la localité mémoire (redémarrage avec binding approprié, ou migration soigneuse si supportée).
Task 7: Launch a workload with explicit CPU + memory binding
cr0x@server:~$ numactl --cpunodebind=1 --membind=1 -- bash -c 'echo "bound"; sleep 1'
bound
Ce que cela signifie : Ce shell (et tout ce qu’il lance) s’exécutera sur le nœud 1 et allouera la mémoire depuis le nœud 1.
Décision : Utilisez ceci pour des tests ciblés. Si la performance s’améliore, vous avez confirmé la localité comme goulot et pouvez implémenter une stratégie de placement durable.
Task 8: Check automatic NUMA balancing status
cr0x@server:~$ sysctl kernel.numa_balancing
kernel.numa_balancing = 1
Ce que cela signifie : Le noyau peut migrer des pages entre nœuds pour améliorer la localité.
Décision : Pour certaines charges sensibles à la latence, l’équilibrage automatique peut ajouter du jitter. Si vous gérez déjà l’affinité explicitement, envisagez de le désactiver après tests.
Task 9: Observe NUMA balancing activity in vmstat
cr0x@server:~$ vmstat -w 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
2 0 0 18234000 10240 120000 0 0 12 45 610 1200 18 6 74 2 0
4 0 0 18190000 10240 120500 0 0 10 38 640 1600 22 7 68 3 0
6 0 0 18020000 10240 121100 0 0 11 40 780 2400 28 9 58 5 0
7 0 0 17800000 10240 121900 0 0 12 42 900 3000 31 10 52 7 0
8 0 0 17550000 10240 122800 0 0 15 50 1100 4200 35 11 46 8 0
Ce que cela signifie : L’augmentation des changements de contexte (cs) et la diminution de l’inactivité (id) peuvent accompagner la pression de migration/reclaim. C’est un signal grossier, pas une preuve NUMA.
Décision : Si cela se corrèle avec des statistiques mémoire distantes plus élevées, traitez-le comme « placement mémoire plus pression » et regardez la mémoire libre par nœud et le scanning de pages.
Task 10: Check per-node free memory directly
cr0x@server:~$ grep -E 'Node [01] (MemTotal|MemFree|FilePages|Active|Inactive)' /sys/devices/system/node/node*/meminfo
Node 0 MemTotal: 263913472 kB
Node 0 MemFree: 1928396 kB
Node 0 FilePages: 2210040 kB
Node 0 Active: 92233728 kB
Node 0 Inactive: 70542336 kB
Node 1 MemTotal: 263913472 kB
Node 1 MemFree: 198223224 kB
Node 1 FilePages: 5110040 kB
Node 1 Active: 12133728 kB
Node 1 Inactive: 8542336 kB
Ce que cela signifie : Le nœud 0 est presque à court de mémoire libre tandis que le nœud 1 en a beaucoup. Le noyau commencera à allouer à distance ou à réclamer agressivement sur le nœud 0.
Décision : Déplacez les threads de travail vers le nœud 1 et redémarrez pour que la mémoire s’alloue là-bas, ou répartissez intentionnellement les allocations entre nœuds (interleave) si la charge le tolère.
Task 11: Check IRQ distribution and whether interrupts align with device locality
cr0x@server:~$ grep -E 'ens5f0|nvme' /proc/interrupts | head
132: 1203345 0 0 0 IR-PCI-MSI 524288-edge ens5f0-TxRx-0
133: 0 1189922 0 0 IR-PCI-MSI 524289-edge ens5f0-TxRx-1
134: 0 0 1191120 0 IR-PCI-MSI 524290-edge ens5f0-TxRx-2
135: 0 0 0 1210034 IR-PCI-MSI 524291-edge ens5f0-TxRx-3
Ce que cela signifie : Les files sont réparties sur les CPU (colonnes). Bon signe. Mais vous devez toujours vérifier que ces CPU appartiennent au même nœud NUMA que la NIC.
Décision : Si la NIC est sur le nœud 1 mais que la majorité des interruptions arrivent sur des CPU du nœud 0, ajustez l’affinité IRQ (ou laissez irqbalance faire si il le fait correctement).
Task 12: Identify which CPUs are on which node (for IRQ affinity decisions)
cr0x@server:~$ lscpu | egrep 'NUMA node\(s\)|NUMA node0 CPU\(s\)|NUMA node1 CPU\(s\)'
NUMA node(s): 2
NUMA node0 CPU(s): 0-15
NUMA node1 CPU(s): 16-31
Ce que cela signifie : Cartographie claire des CPU vers les nœuds.
Décision : Lors du pinning d’app threads ou d’IRQs, gardez le chemin chaud sur un nœud à moins d’avoir une bonne raison de faire autrement.
Task 13: Check cgroup cpuset constraints (containers love this trap)
cr0x@server:~$ systemctl is-active kubelet
active
cr0x@server:~$ cat /sys/fs/cgroup/cpuset/kubepods.slice/cpuset.cpus
0-31
cr0x@server:~$ cat /sys/fs/cgroup/cpuset/kubepods.slice/cpuset.mems
0-1
Ce que cela signifie : Les pods peuvent tourner sur tous les CPU et allouer depuis les deux nœuds. C’est flexible, mais cela peut aussi devenir un « placement aléatoire ».
Décision : Pour des pods critiques en latence, utilisez une politique consciente de la topologie (Guaranteed QoS, CPU Manager static, et scheduling NUMA-aware) pour que CPU et mémoire restent alignés.
Task 14: Inspect a running process’s allowed NUMA nodes
cr0x@server:~$ cat /proc/2481/status | egrep 'Cpus_allowed_list|Mems_allowed_list'
Cpus_allowed_list: 16-31
Mems_allowed_list: 0-1
Ce que cela signifie : Le processus peut allouer de la mémoire sur les deux nœuds, mais ne peut s’exécuter que sur les CPU du nœud 1. Cette combinaison produit souvent des allocations distantes au démarrage et se « corrige » ensuite de manière imprévisible.
Décision : Pour un comportement prévisible, alignez Cpus_allowed_list et Mems_allowed_list sauf si vous avez mesuré un bénéfice de l’interleaving.
Task 15: Use performance counters for a sanity check (LLC misses and stalled cycles)
cr0x@server:~$ sudo perf stat -p 2481 -a -e cycles,instructions,cache-misses,stalled-cycles-frontend -I 1000 -- sleep 3
# time counts unit events
1.000993650 2,104,332,112 cycles
1.000993650 1,003,122,400 instructions
1.000993650 42,110,023 cache-misses
1.000993650 610,334,992 stalled-cycles-frontend
2.001976121 2,201,109,003 cycles
2.001976121 1,021,554,221 instructions
2.001976121 47,901,114 cache-misses
2.001976121 702,110,443 stalled-cycles-frontend
3.003112980 2,305,900,551 cycles
3.003112980 1,030,004,992 instructions
3.003112980 55,002,203 cache-misses
3.003112980 801,030,110 stalled-cycles-frontend
Ce que cela signifie : L’augmentation des cache-misses et des stalls frontend suggère des problèmes du sous-système mémoire. Cela n’indique pas « NUMA » à lui seul, mais soutient l’hypothèse de localité quand c’est corrélé avec numastat.
Décision : Si les cache-misses se corrèlent avec des statistiques mémoire distantes plus élevées, priorisez les corrections de placement et réduisez le bavardage inter-socket avant d’optimiser le code.
Task 16: Quick-and-dirty A/B test: single-node run vs spanning
cr0x@server:~$ numactl --cpunodebind=0 --membind=0 -- bash -c 'stress-ng --cpu 8 --vm 2 --vm-bytes 8G --timeout 20s --metrics-brief'
stress-ng: info: [3112] setting timeout to 20s
stress-ng: metrc: [3112] stressor bogo ops real time usr time sys time bogo ops/s
stress-ng: metrc: [3112] cpu 9821 20.00 18.90 1.02 491.0
stress-ng: metrc: [3112] vm 1220 20.00 8.12 2.10 61.0
Ce que cela signifie : Vous avez une base lorsque contraint à un nœud. Répétez sur le nœud 1 et comparez. Puis exécutez sans binding et comparez.
Décision : Si les résultats mono-nœud sont plus stables ou plus rapides que « tout libre », votre scheduling/placement par défaut ne préserve pas la localité. Corrigez la politique ; n’achetez pas simplement plus de CPU.
Blague #2 (courte, pertinente) : Le tuning NUMA, c’est l’art de rapprocher le travail de sa mémoire — parce que la téléportation n’est toujours pas dans la feuille de route du noyau.
Trois mini-récits d’entreprise (anonymisés, plausibles, techniquement exacts)
Mini-récit 1 : L’incident causé par une fausse hypothèse
Une équipe paiements a migré un service sensible à la latence depuis d’anciens serveurs mono-socket vers de nouveaux serveurs dual-socket. Les nouvelles machines avaient plus de cœurs, plus de RAM et un prix bien plus élevé, donc tout le monde s’attendait à un gain facile. Le test de charge semblait correct en débit moyen, alors le changement est passé en production.
En quelques heures, la latence p99 a commencé à dériver. Pas des pics — une dérive. L’astreinte a vu le CPU à 60–70 %, le réseau ok, les disques ok, et a supposé un problème de voisin bruyant dans la chaîne de dépendances en amont. Ils ont rollbacké. La latence est revenue à la normale. Le nouveau matériel a été étiqueté « instable ».
Des semaines plus tard, la même migration est revenue. Cette fois, quelqu’un a exécuté numastat -p et taskset. Le service était épinglé (par un script de déploiement bien intentionné) aux « 16 derniers CPU » parce que c’était ainsi que les anciennes machines séparaient les charges. Sur les nouvelles machines, les « 16 derniers CPU » appartenaient au nœud NUMA 1. La mémoire du service — allouée tôt au démarrage — était tombée majoritairement sur le nœud 0 à cause de l’endroit où le processus init tournait et de la structure du unit file.
Donc les threads les plus chauds tournaient sur le nœud 1, lisant et écrivant la mémoire sur le nœud 0, tout en gérant les interruptions réseau d’une NIC attachée au nœud 1. La charge faisait des lectures inter-socket pour l’état applicatif puis des écritures inter-socket pour les métadonnées de l’allocateur. C’était un cocktail de latence.
La correction fut ennuyeuse : aligner l’affinité CPU et la politique mémoire au démarrage du service, vérifier la localité des IRQ de la NIC, et arrêter d’utiliser des numéros CPU comme s’ils étaient des sémantiques stables. La vraie leçon du postmortem : dual-socket n’est pas « plus mono-socket ». C’est une topologie qu’il faut respecter.
Mini-récit 2 : L’optimisation qui a échoué
Une équipe stockage gérant un service NVMe intensif voulait réduire la latence de queue. Quelqu’un a proposé d’épingleur les threads de soumission IO à un petit ensemble de cœurs isolés. Ils l’ont fait. La latence s’est améliorée lors de tests en charge légère, alors ils ont déployé à grande échelle.
Sous charge réelle, le système a développé des stalls périodiques. Pas des pannes complètes — juste suffisamment pour provoquer des retries et des ralentissements visibles par les utilisateurs. Les graphiques CPU paraissaient « bons » : ces cœurs épinglés étaient occupés, d’autres étaient majoritairement inactifs. C’était le premier indice. Vous ne voulez pas la moitié d’un serveur au repos pendant que les clients attendent.
L’enquête a montré que les périphériques NVMe étaient attachés au nœud 0, mais que les threads IO épinglés étaient sur le nœud 1. Pire, les interruptions MSI-X étaient réparties entre les deux sockets. Chaque complétion impliquait des sauts inter-socket : interruption sur le nœud 0, réveil d’un thread sur le nœud 1, accès aux files allouées sur le nœud 0, toucher des compteurs partagés, répéter. Quand la charge a augmenté, le trafic de cohérence et les accès mémoire distants se sont amplifiés mutuellement. Le montage épinglé empêchait l’ordonnanceur de « corriger accidentellement » en déplaçant les threads près du périphérique.
Le rollback n’a pas été « arrêter d’épingler ». Ce fut « épinglez correctement ». Ils ont aligné les threads IO et l’affinité IRQ sur le nœud 0, puis réparti les files sur les cœurs de ce nœud. La latence de queue s’est stabilisée et le débit a augmenté parce que l’interconnexion inter-socket a cessé de faire du travail non payé.
Conclusion pratique : le pinning n’est pas une optimisation ; c’est un engagement. Si vous ne vous engagez pas sur la localité de bout en bout — CPU, mémoire et IO — le pinning n’est qu’une façon de rendre une mauvaise conception cohérente.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Un groupe plateforme de données exécutait des charges mixtes : une base de données, un service de type Kafka pour les logs, et des jobs batch. Ils ont standardisé sur des serveurs dual-socket pour la capacité, mais traitaient NUMA comme partie de la spécification de déploiement, pas comme une curiosité post-incident.
Chaque service avait un « contrat de placement » dans son runbook : quel nœud NUMA il devrait préférer, comment vérifier la localité des périphériques, et quoi faire si le nœud manque de mémoire libre. Ils utilisaient un petit ensemble de commandes — numactl --hardware, numastat, taskset, /proc/interrupts — et exigeaient des preuves dans les revues de changement pour toute modification de pinning CPU.
Un jour, après une mise à jour du noyau de routine, ils ont constaté une légère mais régulière régression du p95 sur le chemin d’ingestion des logs. Rien d’allumé. C’est là que la pratique ennuyeuse paie : quelqu’un a lancé les vérifications de placement. Une mise à jour du firmware NIC avait causé un changement de slot PCIe lors d’un cycle de maintenance, et la NIC s’est retrouvée attachée au nœud 1 alors que leurs threads d’ingestion étaient liés au nœud 0. Les IRQ ont suivi la NIC ; les threads non.
Ils ont ajusté l’affinité pour correspondre à la nouvelle topologie et ont récupéré les performances sans room de crise. Pas d’héroïsme. Pas de blâme. Juste des opérations conscientes de la topologie. Le ticket a été clôturé avec le type de commentaire que tout le monde ignore jusqu’à ce qu’il en ait besoin : « Les changements matériels sont des changements logiciels. »
Erreurs fréquentes : symptôme → cause → correction
Cette section est volontairement franche. Ce sont les motifs qui reviennent en production.
1) Symptom: one socket is pegged, the other is mostly idle
Cause racine : L’affinité CPU / cpuset confine la charge à un nœud, ou un goulot monothread force la sérialisation. Parfois c’est le traitement des IRQ concentré sur quelques cœurs.
Correction : Si la charge peut scaler, répartissez-la sur les cœurs d’un même nœud d’abord. N’étendez les sockets que si nécessaire. Si vous spannez, gérez aussi la politique mémoire et la localité IRQ. Validez avec taskset -cp, lscpu et /proc/interrupts.
2) Symptom: p99 latency worse on dual-socket than single-socket
Cause racine : Accès mémoire inter-socket et churn de cohérence (verrous, allocateurs partagés, compteurs chauds). Souvent déclenché par des threads sur le nœud A avec la mémoire sur le nœud B.
Correction : Alignez threads et mémoire via numactl --cpunodebind + --membind (ou via le gestionnaire de services / politique cgroup). Réduisez le partage inter-socket en shardant les files et en utilisant des compteurs par thread. Vérifiez avec numastat -p et les compteurs d’accès distant.
3) Symptom: throughput is okay, but jitter is awful
Cause racine : Équilibrage NUMA automatique et migration de pages sous charge, ou pression mémoire par nœud provoquant des pics de reclaim. THP peut amplifier le coût de la migration.
Correction : Assurez un mémoire libre adéquate sur le nœud où la charge s’exécute. Envisagez de désactiver l’équilibrage NUMA automatique pour les charges épinglées après test. Surveillez meminfo par nœud et numastat dans le temps.
4) Symptom: network performance caps early or drops under load
Cause racine : Les IRQs de la NIC et le traitement du réseau sur le mauvais socket ; les threads applicatifs sur l’autre socket ; le steering des paquets qui fight le pinning CPU.
Correction : Confirmez le NUMA node de la NIC via sysfs. Alignez l’affinité IRQ et les cœurs applicatifs. Utilisez le multi-queue sensément. Vérifiez la distribution dans /proc/interrupts et la cartographie CPU→nœud.
5) Symptom: NVMe latency spikes during heavy IO even with idle CPU elsewhere
Cause racine : Threads de soumission/complétion IO éloignés du contrôleur NVMe ; file mémoire sur nœud distant ; interruptions réparties entre sockets.
Correction : Gardez le chemin IO sur le nœud du périphérique. Validez le numa_node du contrôleur. Ajustez le placement des threads et l’affinité IRQ. N’« optimisez » pas en épinglant sans localité.
6) Symptom: adding more worker threads makes it slower
Cause racine : Vous avez franchi une frontière NUMA et transformé de la contention locale en contention distante ; les verrous et caches partagés sont devenus plus coûteux.
Correction : Scalez d’abord au sein d’un socket. Si vous avez besoin de plus, shardez la charge par nœud NUMA (deux pools indépendants) plutôt qu’un pool global. Traitez le partage inter-socket comme coûteux.
7) Symptom: containers behave differently across identical nodes
Cause racine : Topologie de slot PCIe différente, réglages BIOS ou politiques kubelet CPU/memory manager. « SKU identique » ne signifie pas « topologie identique ».
Correction : Standardisez les réglages BIOS, validez lscpu et les numéros NUMA des périphériques lors du provisioning, et utilisez le scheduling conscient de la topologie pour les pods critiques.
Checklists / step-by-step plan
Ce sont les étapes qui empêchent les machines dual-socket de vous voler silencieusement votre budget de latence.
Checklist A: Before you deploy a latency-sensitive workload on dual-socket
- Cartographiez la topologie : enregistrez la sortie de
numactl --hardwareetlscpu. Vous voulez cela dans le ticket, pas dans la tête de quelqu’un. - Cartographiez la localité des périphériques : pour les NIC et NVMe, capturez
/sys/class/net/*/device/numa_nodeet/sys/class/nvme/nvme*/device/numa_node. - Décidez d’un modèle de placement : mono-nœud (préféré), sharding par nœud, ou cross-node (seulement si nécessaire).
- Choisissez une stratégie d’affinité : soit « pas de pinning, laisser l’ordonnanceur agir », soit « pin + bind mémoire + aligner IRQs ». Ne jamais « pin seulement ».
- Vérifiez la capacité par nœud : assurez-vous que le nœud choisi dispose d’une marge mémoire suffisante pour page cache + heap + pics.
Checklist B: When you already have a slow box and you need a fix today
- Exécutez
numactl --hardware; cherchez un déséquilibre de mémoire libre par nœud. - Exécutez
numastat; cherchez desnuma_miss/other_nodeélevés et croissants. - Choisissez votre PID le plus chaud ; exécutez
numastat -pettaskset -cp. Vérifiez si le nœud CPU et le nœud mémoire correspondent. - Vérifiez le NUMA node du périphérique et
/proc/interrupts. Confirmez la localité des IRQ pour NIC/NVMe. - Si le mismatch est évident : corrigez le placement (redémarrage avec binding correct) plutôt que de chasser des micro-optimisations.
Checklist C: A sane long-term operating model
- Faites de la topologie une partie de l’inventaire : stockez la cartographie NUMA et l’attachement PCIe par hôte.
- Standardisez les réglages BIOS : évitez les modes surprises qui altèrent l’exposition NUMA (et documentez votre choix).
- Construez des « smoke tests NUMA » : exécutez de rapides tests A/B de localité lors du provisioning pour détecter les bizarreries tôt.
- Formez les équipes : « dual-socket ≠ deux fois plus rapide » doit être une connaissance courante, pas une légende.
- Passez les changements de pinning en revue comme du code : exigez preuve, plan de rollback et validation post-changement.
FAQ
Q1: Should I always avoid dual-socket servers?
Non. Le dual-socket est excellent pour la capacité mémoire, les lanes PCIe et le débit agrégé. Évitez-le lorsque votre charge est extrêmement sensible à la latence et fortement partagée, et que vous pouvez tenir dans un bon SKU mono-socket.
Q2: Is NUMA only a database problem?
Non. Les bases de données sont fortement impactées à cause de grandes empreintes mémoire et structures partagées, mais NUMA touche aussi le réseau, NVMe, les services JVM, l’analytics, et tout ce qui génère beaucoup de cache-misses.
Q3: If Linux has automatic NUMA balancing, why do I need to care?
Parce que l’équilibrage est réactif et n’est pas gratuit. Il peut améliorer le débit pour certaines charges générales, mais il peut ajouter du jitter pour des services sensibles à la latence — surtout si vous épinglez des CPU.
Q4: What’s better: interleaving memory across nodes or binding to one node?
Le binding est généralement meilleur pour la latence et la prévisibilité quand vous pouvez garder la charge dans un nœud. L’interleaving aide quand vous avez vraiment besoin de bande passante des deux contrôleurs mémoire et que la charge est bien parallélisée.
Q5: How do I know if my workload is crossing sockets?
Utilisez taskset -cp (où il s’exécute) et numastat -p (où vit sa mémoire). Si le nœud CPU et le nœud mémoire ne correspondent pas, vous traversez les sockets de la pire des manières.
Q6: Can I fix NUMA issues without restarting the service?
Parfois vous pouvez atténuer avec des changements d’affinité CPU et du steering IRQ, mais le placement mémoire est souvent fixé au moment de l’allocation. La correction propre est souvent un redémarrage avec le binding correct et une mémoire libre suffisante par nœud.
Q7: Does huge page usage help or hurt NUMA?
Ça peut aider en réduisant la pression TLB, ce qui diminue la surcharge CPU. Ça peut nuire si cela rend le placement mémoire et la migration plus difficiles sous pression. Mesurez ; n’assumez pas.
Q8: What about hyperthreading—does it change NUMA behavior?
L’hyperthreading ne change pas quelle mémoire est locale. Il change la contention par cœur. Pour certaines charges, utiliser moins de threads par cœur améliore la latence et réduit la contention inter-socket.
Q9: In Kubernetes, what’s the most common NUMA foot-gun?
Les pods Guaranteed avec pinning CPU qui atterrissent sur un nœud tandis que les allocations mémoire se répandent ou sont par défaut ailleurs, plus des IRQ NIC sur le nœud opposé. L’alignement compte.
Q10: If I need all cores across both sockets, what’s the least bad design?
Shardez par nœud NUMA. Exécutez deux pools de workers, deux allocateurs/arenas quand possible, files par nœud, et n’échangez des données entre sockets que lorsque nécessaire. Traitez l’interconnect comme une ressource rare.
Prochaines étapes à faire réellement
Les serveurs dual-socket ne sont pas maudits. Ils sont simplement honnêtes. Ils vous disent, très directement, si votre conception respecte la localité.
- Choisissez un hôte de production et enregistrez la topologie + la localité des périphériques :
numactl --hardware,lscpu, et les fichiersnuma_nodepour NIC/NVMe. - Choisissez vos 3 services les plus sensibles à la latence et capturez :
numastat -p,taskset -cp, et/proc/<pid>/statusallowances mem/CPU pendant le pic. - Décidez d’une politique par service : binding mono-nœud, sharding par nœud, ou interleaving mesuré. Écrivez-la. Mettez-la dans le runbook.
- Arrêtez les changements « pin-only » sauf si le changement spécifie aussi la politique mémoire et la localité des interruptions. Si vous ne pouvez pas expliquer le chemin de données complet, vous n’avez pas fini.
- Validez avec un test A/B (lié mono-nœud vs défaut) avant et après le prochain rafraîchissement matériel. Faites de NUMA une partie de l’acceptation, pas du postmortem.