Server NUMA Locality et CPU Memory Affinity pour une performance d'hébergement maximale

Serveur NUMA La localisation et l'affinité de la mémoire CPU déterminent à quel point les threads travaillent près de leur RAM et à quel point les latences restent constantes dans les piles d'hébergement. Je montre de manière pratique comment tu peux obtenir un meilleur débit mesurable avec la reconnaissance de la topologie, les stratégies d'affinité et les chemins d'E/S proches des nœuds et Latence de manière significative.

Points centraux

Pour une orientation rapide, je résume les messages clés avant d'expliquer les étapes en détail et de les étayer par des exemples ; tu peux ainsi voir directement par où tu dois commencer pour Localité et d'utiliser Affinity de manière profitable. J'insiste sur les liens clairs entre les threads, la mémoire et les entrées/sorties, afin que tu puisses déterminer les priorités de manière propre et Décisions de la même manière. Je citerai également des scénarios dans lesquels l'entrelacement a du sens sans diluer tes chemins critiques, et je montrerai comment tu peux démontrer de réels progrès par le biais du monitoring, et Erreur éviter les problèmes. Pour les environnements virtualisés, je donne des conseils sur le placement des vCPUs et de la vRAM, afin que les systèmes invités ne glissent pas à travers plusieurs nœuds et que les systèmes de stockage ne soient pas endommagés. À distance-Le nombre de visites explose. Pour finir, je transpose les connaissances acquises dans une courte feuille de route, afin que tu puisses procéder de manière structurée et que chaque étape soit bien comprise. mesurable de la sécurité.

  • Localité tout d'abord : garder les threads près de sa propre RAM, éviter les threads distants.
  • Affinity fixer les données : Lier les noyaux et la mémoire par une politique.
  • Topologie lire les informations : connaître les nœuds, les noyaux, les périphériques PCIe par socket.
  • Voies d'E/S regrouper les données : Coupler NIC, NVMe et App dans le même nœud.
  • salons au lieu de deviner : P95/ P99, suivi des accès à distance et du débit.

Comprendre la topologie de la NUMA

Avant de déplacer des charges de travail, je lis les Topologie du serveur : combien de nœuds NUMA existent, combien de cœurs et combien de RAM dépendent de chaque nœud. Je regarde également quels périphériques PCIe - tels que les cartes réseau ou les disques SSD NVMe - sont connectés à quel socket, car cela détermine les chemins d'interruption et l'accès à la mémoire, ainsi que l'utilisation des ressources. Latence de l'utilisateur. Un nœud fournit un accès local à la mémoire à courte distance ; tout ce qui dépasse cette distance coûte du temps et de la bande passante. Plus la machine évolue avec plusieurs sockets, plus l'accès à distance a un impact sur les temps de réponse et consomme de l'énergie. Débit. Pour une entrée en matière compréhensible dans la logique du matériel, je m'aide d'un manuel compact Vue d'ensemble des nœuds NUMA, Le système de gestion de l'information de l'entreprise permet de prendre en compte les limites des nœuds et d'éviter les erreurs de répartition.

Dans la pratique, je commence par un bref inventaire de la topologie et je le documente afin de pouvoir en déduire plus tard des décisions d'affinité compréhensibles. Commandes utiles :

# noyaux et affectation NUMA
lscpu -e=CPU,Core,Socket,Node

# Aperçu du matériel NUMA
numactl --hardware

# Associer les périphériques PCIe à leur nœud NUMA
lspci -nn | grep -E "Ethernet|Non-Volatile"
for d in /sys/bus/pci/devices/* ; do echo -n "$d : " ; cat $d/numa_node ; done

Ce qui est important, c'est que tu PCIe-Root-Complex et les slots de périphériques aux sockets. Deux ports de la même carte réseau peuvent être attribués à des nœuds différents ; cela influence l'endroit où les queues RX/TX et les IRQ sont les mieux placés. Il en va de même pour NVMe : les contrôleurs modernes possèdent plusieurs files d'attente que tu devrais lier aux noyaux à proximité des nœuds afin d'éviter que le DMA ne déclenche des sauts de nœuds.

Utiliser correctement la mémoire CPU Affinity

Avec l'affinité mémoire CPU, j'associe fermement les processus aux zones centrales et je force l'allocation de mémoire la plus locale possible, afin que Fils de discussion ne pas constamment passer par le bord du nœud. Sous Linux, je définis les CPUs via systemd ou cgroups et je les combine avec des politiques de mémoire, de sorte que la RAM soit créée de préférence sur le même nœud, et À distance reste minimisée. Les services critiques - frontaux API, caches en mémoire, bases de données - en profitent immédiatement, car les temps d'attente sur les contrôleurs de mémoire deviennent plus rares et les occurrences de cache plus fréquentes. Des limites d'épinglage trop strictes peuvent toutefois limiter l'ordonnancement, c'est pourquoi je sécurise chaque adaptation avec des benchmarks et observe les valeurs P95/ P99 pour des effets perceptibles sur Utilisateur-expérience de travail. Une introduction compacte à Affinity dans l'hébergement aide à démarrer : Affinité et sensibilisation à la NUMA fournissent les outils nécessaires pour un placement propre.

Ce qui est décisif, c'est le Principe de la première toucheLa mémoire est créée sur le nœud qui écrit en premier dans la page. Initialise donc de grands tas ou tampons sur les noyaux cibles du nœud dans lequel le service sera exécuté plus tard - idéalement avec une politique CPU et mémoire déjà définie (par ex. via systemd-Unit ou numactl). Si tu démarres à froid sur le nœud 0, puis que tu déplaces les threads sur le nœud 1, une grande partie des pages reste à distance. Pour les tas de gros runtimes, il vaut la peine de „pré-toucher“ pendant le bootstrap, afin que les pages pourrissent localement et restent ensuite chaudes.

Sensibilisation à la NUMA dans la pile d'hébergement

Un système d'exploitation conscient de la NUMA, un hyperviseur adapté et des applications avec thread-inning déploient ensemble leur plein potentiel. Potentiel. Le système d'exploitation privilégie le placement local lorsque des ressources libres sont disponibles dans le nœud, tandis que l'hyperviseur alloue les VM de manière à ce que les vCPU et les vRAM ne dérivent pas, et Localité est préservée. Dans l'application, je sépare les pools de travail par nœud et je garde les files d'attente locales au lieu d'exploiter des pools globaux de manière transversale. J'organise les processus de base de données, les démons de cache et les instances de serveur web nœud par nœud, afin que les hotpaths restent courts et que l'accès aux données ne soit pas limité. Jitter diminue. La cohérence et la prévisibilité sous charge augmentent ainsi, ce qui influence directement la planifiabilité des SLA en euros et permet d'économiser un surprovisionnement coûteux.

Au niveau d'Ingress, je veille à ce que Affinité avec les nodes des sessions, par exemple par un routage collant ou un hachage cohérent (par exemple sur l'IP du client ou le jeton de session), afin que les demandes reviennent à „leur“ travailleur local et à la mémoire cache du nœud. Pour les services stateful, je planifie des répliques par nœud et j'équilibre les accès en lecture au niveau local ; j'égalise les chemins d'écriture via la réplication asynchrone ou le batching afin d'éviter le ping-pong inter-nœuds.

Planifier les services nœud par nœud

Je regroupe les couches d'une pile de manière à ce que chaque niveau ait une référence claire au node, et Chemins rester court. Une séparation classique : Web/ API par nœud, App-Worker à côté, plus le cache local ; la base de données se trouve également à proximité du nœud, si l'empreinte de la RAM y est compatible et IO-ne soit pas interrompu. Je déplace les tâches de reporting, les sauvegardes ou les traitements par lots vers des nœuds moins critiques afin que les demandes interactives ne soient pas affectées. J'évite les grandes instances monolithiques, car elles dépassent souvent les limites des nœuds et génèrent ainsi une charge distante qui est Performance estompé. Des instances plus petites et répliquées par nœud fournissent souvent un meilleur débit au quotidien, car elles respectent les règles du NUMA et lissent les pics.

Pour la planification des capacités, je compte marge séparément par nœud : tampon CPU pour les rafales, tampon RAM contre OOM et marges propres pour le cache de page. J'évite ainsi que le noyau ne se déplace involontairement à distance. Pour le basculement, je définis des chemins de commutation clairs : si un nœud tombe en panne, les instances de remplacement peuvent certes s'exécuter de manière croisée, mais je limite leur concourance jusqu'à ce que le nœud d'origine soit restauré - la latence globale reste ainsi stable.

Définir l'affinité CPU : Méthodes et pièges

Pour l'attribution des noyaux, j'utilise systemd avec CPUAffinity ou cgroups avec cpuset.cpus, afin que les services aient des valeurs fixes. Domaines clés sont préservés. Lors de l'épinglage, je fais attention aux paires hyper-threading, car deux threads logiques d'une unité physique partagent des ressources et peuvent se ralentir mutuellement si je les combine malencontreusement et Pointes génèrent. Les chemins de latence - la terminaison TLS, le recours à l'API, les lecteurs de cache - reçoivent des noyaux exclusifs, tandis que les logs, la compression ou les sauvegardes se déplacent vers d'autres pools. Des pools trop étroits sans tampon provoquent des files d'attente, c'est pourquoi je tiens compte de la marge de manœuvre et je vérifie les changements de contexte, la longueur de la file d'attente et la qualité de la mémoire. IRQ-répartition de la latence. Je déduis de l'observation si j'ouvre plus largement les noyaux ou si je les concentre davantage jusqu'à ce que la répartition de la latence chute proprement et que les pics du P99 deviennent plus silencieux.

Pour réduire encore plus la gigue, j'utilise sélectivement des commutateurs du noyau comme nohz_full et rcu_nocbs pour des noyaux de latence exclusifs, les isoler des services système et ne placer délibérément des IRQ que sur des CPU prévus à cet effet. J'utilise le service „irqbalance“ avec précaution : soit je le configure de manière ciblée, soit je le désactive s'il contrecarre ton affinité manuelle avec les IRQ. J'utilise SCHED_FIFO/SCHED_RR avec parcimonie et uniquement avec des limitations, afin d'éviter l'inversion de priorité ou la starvation.

Politiques de mémoire et masques NUMA

En ce qui concerne la politique de stockage, je fais une distinction entre l'allocation locale préférentielle, l'entrelacement sur plusieurs nœuds et les masques NUMA fixes via cpuset.mems, afin que RAM va là où les threads fonctionnent réellement. Pour les services interactifs, je définis généralement „preferred“, ce qui permet au système d'allouer localement et de ne dévier qu'en cas de pénurie, ce qui À distance-Les accès sont limités. Les tâches d'analyse ou de streaming profitent parfois de l'entrelacement, car la bande passante est répartie sur les nœuds et la pression sur un contrôleur diminue. Les masques fixes offrent un contrôle, mais exigent de la discipline dans la planification des capacités, afin d'éviter que des événements OOM non souhaités n'explosent dans un nœud, et Services peuvent perturber. Le tableau suivant attribue des politiques courantes à des scénarios typiques et aide à prendre une décision rapide.

Politique Effet Charges de travail typiques Risque
Preferred (local) RAM en priorité dans le nœud local, option de secours en cas de pénurie Web/ API, caches, bases de données OLTP Légère dérive à pleine charge sur d'autres nœuds
Interleave Répartition uniforme sur des nœuds sélectionnés Streaming, analyse, grands scans Latence plus élevée pour les accès individuels
Masque fixe NUMA Lien strict avec des nœuds de mémoire définis Services strictement encapsulés, tests déterministes Risque d'OOM en cas de mauvaise planification du budget

Garde un œil sur les commutateurs à l'échelle du système : zone_reclaim_mode influence si un nœud nettoie agressivement sa propre mémoire avant d'allouer à distance - ce qui est souvent indésirable pour les chemins de latence. Pages transparentes volumineuses (THP) peuvent déclencher des migrations de pages ou générer des décrochages ; pour les services sensibles à la latence, je choisis généralement „madvise“ et utilise des hugepages statiques lorsque c'est judicieux, afin d'augmenter les occurrences TLB et de réduire les pics de défaut de page.

Lier les chemins du réseau et des E/S à proximité du nœud

J'aligne les queues de NIC (RX/ TX) de telle sorte que leurs IRQ pointent vers les noyaux du nœud approprié et que le traitement des paquets se fasse là où les App calculent. Il en va de même pour les SSD NVMe ou les contrôleurs RAID : les threads d'E/S doivent être exécutés sur le nœud auquel le périphérique est connecté par PCIe, afin que les chemins DMA restent courts et que le système ne soit pas endommagé. Bottlenecks ne se produisent pas. Sous Linux, j'ajuste les masques d'affinité IRQ et je les associe aux pools d'unités centrales de mes services afin de créer un chemin continu. En cas de micro-bursts provenant du réseau, par exemple de nombreux handshakes TLS, cette proximité est directement payante, car les chemins de copie sont plus courts et les caches CPU restent chauds, et Contexte ne bascule que rarement. Il en résulte un flux de données cohérent du paquet à l'application jusqu'au stockage, sans sauts de nœuds inutiles.

Leviers concrets dans la pile réseau : RSS pour la distribution de matériel sur les files d'attente, RPS/RFS pour la commande de l'unité centrale côté logiciel et XPS pour la sélection TX. Avec ethtool, j'attribue aux queues RX des groupes de base qui fonctionnent dans le même nœud que tes travailleurs. Pour le stockage, je mise sur blk-mq-Les contrôleurs NVMe offrent plusieurs files d'attente de soumission/complétion que je mets à l'échelle et affine ≤ nombre de cœurs par nœud. Vérifie régulièrement si les interruptions (cat /proc/interrupts) tirent là où se trouvent les cœurs de ton app - tu peux reconnaître une dérive à l'augmentation des octets distants malgré une charge stable.

Structurer l'architecture de l'application en fonction de la NUMA

Au niveau de l'application, je crée mes propres pools de travail pour chaque nœud NUMA, je garde les files d'attente locales et j'évite les points de verrouillage globaux afin que Fils de discussion ne pas faire de sauts croisés. Je configure le sharding de sessions et de données de manière à ce que les partitions chaudes restent là où les travailleurs demandeurs sont en cours d'exécution et que les partitions froides restent là où les travailleurs sont en cours d'exécution. Temps ne se perde pas dans le trafic inter-nœuds. Pour les caches, j'ai plus souvent recours à des réplicats plutôt qu'à une instance centrale, afin que les lecteurs rencontrent des copies locales des nœuds. Dans Netty, Tokyo, libuv ou les clients DB, j'attache des boucles d'événements à des noyaux fixes et je fais attention à la proximité de l'IRQ afin de limiter les changements de tâches et de permettre aux clients de se concentrer sur leur travail. Caches mieux viser. Cette disposition réduit les effets de ping-pong et rend les temps de réaction plus constants au cours de la journée.

Un levier sous-estimé sont Allocateur et des options d'exécution : Les allocateurs compatibles NUMA (jemalloc/tcmalloc) réduisent la contention des threads croisés et gardent les pages plus proches des noyaux parents des threads. Dans les piles JVM, des options telles que la conscience NUMA et la pré-tache aident les phases de défaut déterministes ; en .NET, j'aligne les threads GC près des nœuds et je fais attention au GC du serveur pour lisser les temps d'arrêt. En Go, je dimensionne GOMAXPROCS par pool de nœuds et je tiens les ordonnanceurs de goroutine à l'écart des noyaux de latence qui fonctionnent à proximité de l'IRQ.

Gérer judicieusement l'auto-équilibrage de la NUMA

Les mécanismes d'équilibrage automatique NUMA du noyau peuvent aider à lisser la charge distribuée, mais je vérifie toujours s'ils ne dépassent pas mes capacités. Affinity de l'activité de l'utilisateur. Dans les services à latence critique, je désactive ou j'étrangle le déplacement automatique s'il arrache des threads à leur mémoire locale et Pointes est généré. Pour les tâches d'analyse ou le traitement par lots, j'ai tendance à laisser l'équilibrage activé, car il permet d'augmenter la bande passante sans dégrader l'interaction. Une introduction pratique aux stratégies d'équilibrage me fournit des points de départ supplémentaires : Comprendre l'équilibrage NUMA montre quand l'automatisme porte et quand il faut attribuer manuellement. En fin de compte, je décide en fonction des données par classe de service, au lieu d'appliquer aveuglément un préréglage global et d'utiliser les données de la classe de service. Objectifs de manquer.

Lorsque l'équilibrage est activé, j'observe les taux de migration, les pics de défauts mineurs/majeurs et le steal CPU par nœud. Si des pages sont déplacées de manière cyclique, je contrebalance avec un épinglage plus ferme, un pré-touch et des masques de mémoire plus étroits. Dans les charges de travail avec de longs balayages séquentiels, l'équilibrage peut en revanche harmoniser la charge, à condition qu'aucun chemin de latence interactif ne soit concerné.

Monitoring : mesurer, comparer, décider

Sans mesure, le réglage reste un jeu de devinettes, c'est pourquoi j'effectue un suivi de la charge CPU par cœur et par nœud, de l'utilisation de la mémoire par nœud et de la part de la mémoire utilisée par les nœuds. À distance-accès aux données. Pour l'expérience de l'utilisateur, les latences P95/ P99 comptent nettement plus que les valeurs moyennes, car les valeurs aberrantes marquent l'impression SLA et Coûts poussent vers le haut. Je réalise des profils de charge proches de la réalité avec des caches froids et chauds, car les deux mondes présentent des goulots d'étranglement différents. Après chaque modification, je documente les paramètres, la date du test et les résultats, afin de pouvoir revenir en arrière plus tard en toute sécurité. Connaissances ne soit pas perdue. En outre, si l'on met en corrélation les métriques des applications - longueurs de file d'attente, retours, collecte de déchets - avec les valeurs du système, on identifie plus rapidement les causes et les effets.

Aides pratiques dans l'analyse :

  • numastat (par système et par processus) pour local vs. À distance-conclusion
  • /proc/interrupts et temps de SoftIRQ par CPU pour dérive d'IRQ
  • événements perf et statistiques de l'ordonnanceur pour la profondeur de la file d'attente, les changements de contexte, les échecs LLC
  • fio/iperf/wrk avec des pools de travail spécifiques aux nœuds pour des comparaisons reproductibles

L'évaluation se fait par nœud : Je m'attends à ce que les histogrammes de latence soient très proches les uns des autres. Si un nœud s'éloigne vers le haut, je recherche d'abord une charge d'IRQ mal répartie, une dérive dans le cache de la page ou des tas qui ont été alloués au mauvais nœud lors de l'échauffement.

NUMA dans les VM et les conteneurs

En virtualisation, le placement de vCPU et de vRAM sur un nœud commun compte pour que les charges de travail invitées ne s'effilochent pas et Latence est en train de monter en puissance. Je dimensionne la RAM de manière à ce qu'elle tienne dans le nœud local et j'évite les grosses VM qui s'étendent sur plusieurs nœuds, et Dérive déclencher le processus. Pour les conteneurs, j'utilise des contrôleurs cpuset afin que les groupes de pods travaillent de manière cohérente sur un nœud et que le stockage soit créé localement. Je place de préférence les invités chargés d'E/S sur le nœud avec une connexion directe au stockage, afin de réduire les trajets DMA et d'éviter les erreurs. IRQ-bruit, en réduisant le bruit. Ainsi, même les hôtes de virtualisation denses restent prévisibles et portent plus de projets sur le même matériel.

Je fais attention à vNUMA-Exposé : l'invité doit voir la même structure de nœuds que celle fournie physiquement par l'hyperviseur. L'épinglage vCPU et la liaison vRAM vont de pair ; je déplace si possible les ajouts à chaud pendant les fenêtres de maintenance, car sinon les nouvelles pages atterrissent à distance. Dans Kubernetes, je règle la QoS sur „Guaranteed“, le gestionnaire de CPU sur „static“ et le placement conscient de la topologie, afin que les pods ne se déplacent pas sur les nœuds. Pour les SR-IOV/VF, j'attribue les VF au nœud physique approprié et je lie les files d'attente IRQ aux ensembles de CPU des pods ou des VM qu'ils desservent.

Préparer le First-Touch, le Warmup et le Heaps de manière ciblée

De nombreuses erreurs de performance surviennent lors LancementPendant la phase d'échauffement, les tas se développent là où les premières requêtes atterrissent - souvent de manière centralisée sur un nœud. C'est pourquoi je procède à des échauffements contrôlés par nœud : je démarre les instances avec un masque CPU/mémoire défini, j'exécute des requêtes de préchargement ciblées et j'initialise les caches en parallèle par nœud. Pour les services JVM, j'active la pré-tache du tas ; pour les bases de données, je segmente les pools de tampons par nœud. Cela permet de réduire les migrations de pages ultérieures et de veiller à ce que les premières requêtes ne marquent pas la répartition de la mémoire de manière aléatoire.

Réglage du noyau/BIOS pour des latences constantes

Sous le capot, j'ajuste la politique d'alimentation et d'interruption :

  • Gouverneur CPU sur „performance“, limiter les C-States profonds, utiliser les C-States de package avec prudence pour Jitter de réduire.
  • Ne pas limiter la fréquence de la mémoire ; les profils d'énergie équilibrés réduisent souvent la fréquence de la mémoire. Débit en charge.
  • Éviter la modulation Spread-Spectrum/Clock si la cohérence est plus importante qu'une économie d'énergie minimale.

Au niveau du noyau, je garde les unités centrales de maintenance séparées des noyaux de latence, je minimise les interruptions de temporisateur sur les noyaux chauds (nohz_full) et je parque les tâches d'arrière-plan (compaction, Kswapd) de préférence sur les noyaux système d'un nœud qui n'emprunte pas de chemins de latence.

Dépannage et anti-patterns typiques

  • Symptôme: La latence du P99 saute après les déploiements. Cause: heaps/caches first-touch sur le mauvais node. Solution: Warmup/Pre-Touch sous Affinity cible, puis ouvrir l'équilibreur de charge.
  • Symptôme: temps de SoftIRQ élevé sur les „mauvais“ CPU. Causeirqbalance répartie sur les nœuds. Solution: Fixer l'affinité IRQ, mettre le nœud RPS/RFS/XPS en conformité.
  • SymptômeOOM dans un nœud, bien que la RAM système soit libre. Cause: Masque NUMA strict sans tampon. SolutionCorriger la capacité ou l'utiliser de manière préférentielle, établir des alertes par nœud.
  • Symptôme: Débit irrégulier avec NVMe. Cause: Mauvais mappage de la file d'attente, file d'attente partagée cross-node. Solution: files blk-mq/NVMe par nœud, threads d'E/S pinnnen.

Liste de contrôle pratique

  • Enregistrer la topologie : Nœuds, cœurs, RAM, périphériques PCIe par socket.
  • Dessiner une coupe de service : Quels sont les chemins Latence-critique, quel batchig ?
  • Définir l'affinité CPU/Mémoire par classe ; respecter la première touche au démarrage.
  • Lier les IRQ/Queues à proximité des nœuds ; vérifier les queues RSS/RPS/XPS et NVMe.
  • Surveillance sur P95/P99, accès à distance, file d'attente d'exécution, distribution d'IRQ.
  • Contrôler l'équilibrage automatique de manière ciblée ; choisir THP/zone_reclaim_mode de manière appropriée.
  • Dans les VM/conteneurs, maintenir la cohérence de vNUMA, de l'épinglage vCPU et de la liaison vRAM.
  • Tester de manière itérative, documenter, reculer en cas de dérive et ajuster plus finement.

Bilan succinct et feuille de route pour le tuning

C'est là que le rendement est le plus élevé, Fils de discussion et la mémoire, de raccourcir les chemins d'E/S et de les répartir avec précaution. Je commence par une analyse topologique, je planifie les services nœud par nœud, je définis l'affinité CPU et mémoire, je connecte le réseau/le stockage de manière appropriée et j'observe les valeurs P95/P99 en me concentrant sur À distance-accès au réseau. Ensuite, j'affine la taille des pools, les masques IRQ et les politiques jusqu'à ce que les pics de latence diminuent et que le débit augmente. Pour les VM et les conteneurs, je vérifie le placement séparément, car l'hyperviseur a beaucoup d'influence et Frontières agissent différemment. En répétant ce processus et en le documentant, on obtient des performances nettement plus élevées du serveur NUMA Locality et de la mémoire CPU Affinity - souvent à un prix inférieur à celui de la mise à niveau de matériel supplémentaire en euros.

Derniers articles