...

Server NUMA Locality und CPU-Memory Affinity für maximale Hosting-Performance

Server NUMA Locality und CPU‑Memory Affinity bestimmen, wie nahe Threads an ihrem RAM arbeiten und wie konstant Latenzen in Hosting‑Stacks bleiben. Ich zeige praxisnah, wie du mit Topologieerkennung, Affinity‑Strategien und node‑nahen I/O‑Pfaden messbar mehr Durchsatz erzielst und Latenz spürbar senkst.

Zentrale Punkte

Zur schnellen Orientierung fasse ich die Kernbotschaften zusammen, bevor ich die Schritte ausführlich erkläre und mit Beispielen untermauere; damit kannst du direkt erkennen, wo du ansetzen solltest, um Locality und Affinity gewinnbringend zu nutzen. Ich betone klare Zusammenhänge zwischen Threads, Speicher und I/O, damit du Prioritäten sauber ableitest und Entscheidungen triffst. Außerdem benenne ich Szenarien, in denen Interleave Sinn ergibt, ohne deine kritischen Pfade zu verwässern, und zeige, wie du per Monitoring echte Fortschritte belegst und Fehler vermeidest. Für virtualisierte Umgebungen liefere ich Hinweise zur Platzierung von vCPUs und vRAM, damit Gastsysteme nicht quer über mehrere Nodes rutschen und Remote-Zugriffe explodieren. Zum Schluss überführe ich die Erkenntnisse in einen kurzen Fahrplan, damit du strukturiert vorgehst und jeden Schritt messbar absicherst.

  • Locality zuerst: Threads nahe am eigenen RAM halten, Remote vermeiden.
  • Affinity fixieren: Kerne und Speicher per Policy zusammenbinden.
  • Topologie lesen: Nodes, Kerne, PCIe‑Geräte pro Sockel kennen.
  • I/O‑Wege bündeln: NIC, NVMe und App im selben Node koppeln.
  • Messen statt raten: P95/ P99, Remote‑Zugriffe und Durchsatz tracken.

NUMA‑Topologie verstehen

Bevor ich Workloads verschiebe, lese ich die Topologie des Servers: Wie viele NUMA‑Nodes existieren, wie viele Kerne und wie viel RAM hängen an jedem Node. Ich achte außerdem darauf, welche PCIe‑Geräte – etwa NICs oder NVMe‑SSDs – an welchem Sockel angebunden sind, weil das über Interrupt‑Wege und Speicherzugriffe entscheidet und Latenz prägt. Ein Node liefert lokalen Speicherzugriff mit kurzer Strecke; alles darüber hinaus kostet Zeit und Bandbreite. Je größer die Maschine mit mehreren Sockeln skaliert, desto stärker schlägt Remote‑Zugriff auf Antwortzeiten durch und frisst Durchsatz. Für einen verständlichen Einstieg in die Hardware‑Logik hilft mir ein kompaktes NUMA‑Nodes im Überblick, um Node‑Grenzen bewusst zu berücksichtigen und Fehlverteilungen zu vermeiden.

In der Praxis beginne ich mit einer kurzen Topologie‑Inventur und dokumentiere sie, damit ich später Affinity‑Entscheidungen nachvollziehbar ableiten kann. Nützliche Kommandos:

# Kerne und NUMA-Zuordnung
lscpu -e=CPU,Core,Socket,Node

# NUMA-Hardwareübersicht
numactl --hardware

# PCIe-Geräte ihrem NUMA-Node zuordnen
lspci -nn | grep -E "Ethernet|Non-Volatile"
for d in /sys/bus/pci/devices/*; do echo -n "$d: "; cat $d/numa_node; done

Wichtig ist, dass du PCIe‑Root‑Complex und Geräteslots den Sockeln zuordnest. Zwei Ports derselben NIC können unterschiedlichen Nodes zugeordnet sein; das beeinflusst, wo RX/TX‑Queues und IRQs am besten landen. Gleiches gilt für NVMe: Moderne Controller besitzen mehrere Queues, die du Node‑nah an Kerne binden solltest, damit DMA keine Node‑Hops auslöst.

CPU‑Memory Affinity richtig nutzen

Mit CPU‑Memory Affinity verknüpfe ich Prozesse fest mit Kernbereichen und erzwinge möglichst lokale Speicherallokation, damit Threads nicht ständig über den Node‑Rand greifen. In Linux lege ich CPUs via systemd oder cgroups fest und kombiniere das mit Memory‑Policies, sodass RAM bevorzugt am selben Node entsteht und Remote minimiert bleibt. Kritische Dienste – API‑Frontends, In‑Memory‑Caches, Datenbanken – profitieren sofort, weil Wartezeiten auf Memory‑Controller seltener werden und Cache‑Treffer häufiger eintreten. Zu harte Pinning‑Grenzen können allerdings Scheduling einschränken, daher sichere ich jede Anpassung mit Benchmarks und beobachte P95/ P99‑Werte für spürbare Effekte auf User-Erlebnis. Eine kompakte Einführung zu Affinity im Hosting hilft beim Start: Affinität und NUMA‑Awareness liefern das nötige Handwerkszeug für saubere Platzierung.

Entscheidend ist das First‑Touch‑Prinzip: Speicher wird auf dem Node angelegt, der zuerst in die Seite schreibt. Initialisiere deshalb große Heaps oder Buffer auf den Zielkernen des Nodes, in dem der Dienst später läuft – idealerweise bereits mit gesetzter CPU‑ und Memory‑Policy (z. B. via systemd‑Unit oder numactl). Startest du kalt auf Node 0, verschiebst dann Threads auf Node 1, bleibt ein Großteil der Seiten remote. Für Heaps großer Runtimes lohnt sich „Pre‑Touch“ während des Bootstraps, damit Seiten lokal faulten und anschließend warm bleiben.

NUMA‑Awareness im Hosting‑Stack

Ein NUMA‑bewusstes Betriebssystem, ein passender Hypervisor und Anwendungen mit Thread‑Pinning entfalten zusammen ihr volles Potenzial. Das OS bevorzugt lokale Platzierung, wenn freie Ressourcen im Node verfügbar sind, während der Hypervisor VMs so zuweist, dass vCPUs und vRAM nicht auseinanderdriften und Locality gewahrt bleibt. In der Anwendung trenne ich Worker‑Pools pro Node und halte Queues lokal, statt globale Pools quer zu betreiben. Datenbankprozesse, Cache‑Daemons und Webserver‑Instanzen ordne ich Node‑für‑Node, damit Hotpaths kurz bleiben und Jitter sinkt. Dadurch steigen Konsistenz und Vorhersagbarkeit unter Last, was die Planbarkeit von SLAs in Euro direkt beeinflusst und teure Überprovisionierung spart.

Auf der Ingress‑Ebene sorge ich für Node‑Affinität der Sessions, etwa durch Sticky‑Routing oder konsistentes Hashing (z. B. auf Client‑IP oder Session‑Token), damit Anfragen wieder bei „ihrem“ Node‑lokalen Worker und Cache landen. Für Stateful‑Dienste plane ich Replikate pro Node und balanciere lesende Zugriffe lokal aus; Schreibpfade entzerre ich über asynchrone Replication oder Batching, um Inter‑Node‑Ping‑Pong zu vermeiden.

Dienste Node‑weise planen

Ich gruppiere die Schichten eines Stacks so, dass jede Ebene einen klaren Node‑Bezug erhält und Wege kurz bleiben. Eine klassische Trennung: Web/ API pro Node, App‑Worker daneben, dazu der lokale Cache; die Datenbank sitzt ebenfalls Node‑nah, wenn der RAM‑Fußabdruck hineinpasst und IO‑Pfad nicht abreißt. Reporting‑Jobs, Backups oder Batch‑Worker verlege ich auf weniger kritische Nodes, damit interaktive Anfragen unbeeinflusst bleiben. Große Monolith‑Instanzen meide ich, weil sie häufig über Node‑Grenzen gehen und somit Remote‑Last erzeugen, die Performance verwischt. Kleinere, replizierte Instanzen pro Node liefern im Alltag oft den besseren Durchsatz, da sie die NUMA‑Regeln respektieren und Spitzen glätten.

Bei der Kapazitätsplanung rechne ich Headroom je Node separat: CPU‑Puffer für Bursts, RAM‑Puffer gegen OOM und eigene Margen für Page‑Cache. So verhindere ich, dass der Kernel ungewollt remote ausweicht. Für Failover definiere ich klare Umschaltpfade: Fällt ein Node aus, dürfen Ersatz‑Instanzen zwar cross‑Node laufen, aber ich begrenze sie in ihrer Concurrency, bis der ursprüngliche Node wiederhergestellt ist – so bleibt die Gesamt‑Latenz stabil.

CPU‑Affinity setzen: Methoden und Fallstricke

Für die Kernzuweisung nutze ich systemd mit CPUAffinity oder cgroups mit cpuset.cpus, damit Services feste Kernbereiche erhalten. Beim Pinning achte ich auf Hyper‑Threading‑Paare, denn zwei logische Threads einer physischen Einheit teilen Ressourcen und können sich gegenseitig bremsen, wenn ich sie unglücklich kombiniere und Spitzen erzeuge. Latenzpfade – TLS‑Terminierung, API‑Ingress, Cache‑Leser – bekommen exklusive Kerne, während Logs, Kompression oder Backups in andere Pools wandern. Zu enge Pools ohne Puffer verursachen Warteschlangen, daher rechne ich Headroom ein und prüfe Kontextwechsel, Runqueue‑Länge und IRQ‑Verteilung. Aus der Beobachtung leite ich nach, ob ich Kerne breiter öffne oder weiter konzentriere, bis die Latenzverteilung sauber abfällt und die P99‑Spitzen leiser werden.

Zur weiteren Jitter‑Reduktion setze ich selektiv Kernel‑Schalter wie nohz_full und rcu_nocbs für exklusive Latenz‑Kerne, isoliere sie von System‑Diensten und platziere IRQs bewusst nur auf dafür vorgesehenen CPUs. Den Dienst „irqbalance“ nutze ich mit Bedacht: Entweder gezielt konfigurieren oder deaktivieren, wenn er deine manuelle IRQ‑Affinity konterkariert. SCHED_FIFO/SCHED_RR verwende ich sparsam und nur mit Be‑Grenzungen, damit es nicht zu Priority‑Inversion oder Starvation kommt.

Memory‑Policies und NUMA‑Masken

Bei der Speicherpolitik unterscheide ich zwischen bevorzugter lokaler Allokation, Interleave über mehrere Nodes und festen NUMA‑Masken via cpuset.mems, damit RAM dorthin fließt, wo Threads tatsächlich laufen. Für interaktive Dienste setze ich in der Regel „preferred“, wodurch das System lokal allokiert und erst bei Knappheit ausweicht, was Remote‑Zugriffe begrenzt. Analytics‑ oder Streaming‑Jobs profitieren manchmal von Interleave, weil Bandbreite über Nodes verteilt wird und Druck auf einen Controller sinkt. Feste Masken bieten Kontrolle, verlangen aber Disziplin bei Kapazitätsplanung, damit keine ungewollten OOM‑Ereignisse in einem Node hochgehen und Dienste stören. Die folgende Tabelle ordnet gängige Policies typischen Szenarien zu und hilft bei einer schnellen Entscheidung.

Policy Wirkung Typische Workloads Risiko
Preferred (lokal) RAM primär im lokalen Node, Ausweichoption bei Knappheit Web/ API, Caches, OLTP‑Datenbanken Leichter Drift bei Volllast auf anderen Nodes
Interleave Gleichmäßige Verteilung über ausgewählte Nodes Streaming, Analytics, große Scans Höhere Latenz für einzelne Zugriffe
Feste NUMA‑Maske Strikte Bindung an definierte Memory‑Nodes Streng kapselte Services, deterministische Tests OOM‑Gefahr bei Fehlplanung des Budgets

Behalte systemweite Schalter im Blick: zone_reclaim_mode beeinflusst, ob ein Node aggressiv im eigenen Speicher aufräumt, bevor er remote allokiert – für Latenzpfade oft unerwünscht. Transparent Huge Pages (THP) können Seitenmigration triggern oder Stalls erzeugen; bei latenzsensiblen Diensten wähle ich meist „madvise“ und nutze wo sinnvoll statische Hugepages, damit TLB‑Treffer steigen und Page‑Fault‑Spitzen sinken.

Netzwerk‑ und I/O‑Pfade Node‑nah binden

Ich richte NIC‑Queues (RX/ TX) so aus, dass ihre IRQs auf Kerne des passenden Nodes zeigen und die Paketverarbeitung dort stattfindet, wo die App rechnet. Gleiches gilt für NVMe‑SSDs oder RAID‑Controller: I/O‑Threads sollten auf dem Node laufen, an dem das Gerät per PCIe hängt, damit DMA‑Wege kurz bleiben und Bottlenecks ausbleiben. Unter Linux justiere ich IRQ‑Affinity‑Masks und verknüpfe sie mit CPU‑Pools meiner Services, damit ein durchgehender Pfad entsteht. Bei Mikrobursts aus dem Netz, etwa vielen TLS‑Handshakes, zahlt sich diese Nähe direkt aus, weil Kopierwege kürzer sind und CPU‑Caches warm bleiben und Kontext seltener kippt. So entsteht ein konsistenter Datenfluss vom Paket zur Anwendung bis zum Speicher, ohne unnötige Node‑Hops.

Konkrete Hebel im Netzwerk‑Stack: RSS für Hardware‑Verteilung auf Queues, RPS/RFS für softwareseitige CPU‑Steuerung und XPS für TX‑Selektion. Mit ethtool ordne ich RX‑Queues Kerngruppen zu, die im selben Node laufen wie deine Worker. Für Storage setze ich auf blk‑mq‑Tuning und Queue‑Mapping je Node; NVMe‑Controller bieten mehrere Submission/Completion‑Queues, die ich ≤ Anzahl Kerne pro Node skaliere und affinisiere. Prüfe regelmäßig, ob Interrupts (cat /proc/interrupts) dorthin feuern, wo deine App‑Kerne sitzen – Drift erkennst du an steigenden Remote‑Bytes trotz stabiler Last.

Anwendungsarchitektur NUMA‑gerecht strukturieren

Auf App‑Ebene setze ich je NUMA‑Node eigene Worker‑Pools auf, halte Queues lokal und meide globale Lock‑Hotspots, damit Threads nicht kreuz und quer springen. Session‑ und Daten‑Sharding richte ich so ein, dass heiße Partitionen dort bleiben, wo die anfragenden Worker laufen und Zeit nicht im Inter‑Node‑Verkehr verpufft. Für Caches greife ich häufiger zu Replikaten statt zu einer zentralen Instanz, damit Leser Node‑lokale Kopien treffen. In Netty, Tokio, libuv oder DB‑Clients pinne ich Event‑Loops an feste Kerne und beachte IRQ‑Nähe, damit Task‑Wechsel begrenzt bleiben und Caches besser treffen. Dieses Layout senkt Ping‑Pong‑Effekte und macht Reaktionszeiten über den Tagesverlauf konstanter.

Ein unterschätzter Hebel sind Allocator und Runtime‑Optionen: NUMA‑fähige Allokatoren (jemalloc/tcmalloc) reduzieren Cross‑Thread‑Contention und halten Seiten näher an Thread‑Heimatkernen. In JVM‑Stacks helfen Optionen wie NUMA‑Bewusstsein und Pre‑Touch für deterministische Fault‑Phasen; in .NET richte ich GC‑Threads Node‑nah aus und achte auf Server‑GC, um Stoppzeiten zu glätten. In Go dimensioniere ich GOMAXPROCS pro Node‑Pool und halte Goroutine‑Scheduler von Latenz‑Kernen fern, die IRQ‑nah arbeiten.

NUMA‑Autobalancing sinnvoll steuern

Automatische NUMA‑Balancing‑Mechanismen des Kernels können helfen, verteilte Last zu glätten, doch ich prüfe stets, ob sie meine Affinity unterlaufen. In latenzkritischen Diensten deaktiviere oder drossele ich automatisches Verschieben, wenn es Threads aus ihrem lokalen Speicher reißt und Spitzen erzeugt. Für Analytics‑Jobs oder breite Batch‑Verarbeitung lasse ich Balancing eher an, weil es Bandbreite heben kann, ohne Interaktion zu verschlechtern. Eine praxisnahe Einführung zu Strategien rund um Balancing liefert mir zusätzliche Ansatzpunkte: NUMA‑Balancing verstehen zeigt, wann Automatik trägt und wann manuell zugewiesen werden sollte. Am Ende entscheide ich datenbasiert je Serviceklasse, statt eine globale Voreinstellung blind zu übernehmen und Ziele zu verfehlen.

Bei aktiviertem Balancing beobachte ich Migrationsraten, Minor/Major‑Fault‑Spitzen und CPU‑Steal pro Node. Werden Seiten zyklisch hin‑ und hergeschoben, kontere ich mit festerem Pinning, Pre‑Touch und engeren Memory‑Masken. In Workloads mit langen, sequenziellen Scans kann Balancing dagegen Last harmonisieren, sofern keine interaktiven Latenzpfade betroffen sind.

Monitoring: messen, vergleichen, entscheiden

Ohne Messung bleibt Tuning ein Ratespiel, daher tracke ich CPU‑Last pro Kern und pro Node, Speicherbelegung je Node und den Anteil an Remote‑Zugriffen. Für Nutzererlebnis zählen P95/ P99‑Latenzen deutlich mehr als Mittelwerte, weil Ausreißer SLA‑Eindruck prägen und Kosten nach oben treiben. Ich fahre realitätsnahe Lastprofile mit kalten und warmen Caches, weil beide Welten andere Engpässe zeigen. Nach jeder Änderung dokumentiere ich Settings, Testdatum und Ergebnisse, damit ich Modifikationen später sicher zurückdrehen kann und Wissen nicht verloren geht. Wer zudem App‑Metriken – Queue‑Längen, Retries, Garbage‑Collection – neben Systemwerten korreliert, erkennt Ursache und Wirkung schneller.

Praktische Hilfen in der Analyse:

  • numastat (system‑ und prozessbezogen) für lokale vs. Remote‑Treffer
  • /proc/interrupts und SoftIRQ‑Zeit nach CPU für IRQ‑Drift
  • perf‑Events und Scheduler‑Statistiken für Runqueue‑Tiefe, Kontextwechsel, LLC‑Miss
  • fio/iperf/wrk mit node‑spezifischen Worker‑Pools für reproduzierbare Vergleiche

Die Auswertung erfolgt je Node: Ich erwarte, dass Latenz‑Histogramme eng beieinanderliegen. Zieht ein Node nach oben weg, suche ich zuerst nach falsch verteilter IRQ‑Last, Drift im Page‑Cache oder Heaps, die beim Warmup auf dem falschen Node allokiert wurden.

NUMA in VMs und Containern

In Virtualisierung zählt die Platzierung von vCPUs und vRAM auf einen gemeinsamen Node, damit die Gast‑Workloads nicht zerfasern und Latenz hochzieht. Ich dimensioniere RAM so, dass er in den lokalen Node passt, und meide große VMs, die über mehrere Nodes reichen und Drift auslösen. Für Container nutze ich cpuset‑Controller, damit Pod‑Gruppen konsistent auf einem Node arbeiten und Speicher lokal entsteht. I/O‑lastige Gäste setze ich bevorzugt auf den Node mit direkter Storage‑Anbindung, um DMA‑Wege kurz zu halten und IRQ‑Lärm zu reduzieren. So bleiben auch dichte Virtualisierungs‑Hosts vorhersagbar und tragen mehr Projekte auf derselben Hardware.

Ich achte auf vNUMA‑Exponierung: Der Gast sollte die gleiche Node‑Struktur sehen, die der Hypervisor physisch bereitstellt. vCPU‑Pinning und vRAM‑Bindung gehören zusammen; Hot‑Add verschiebe ich möglichst während Wartungsfenstern, weil neue Seiten sonst remote landen. In Kubernetes stelle ich auf „Guaranteed“‑QoS, CPU‑Manager‑„static“ und Topology‑bewusste Platzierung, damit Pods nicht über Nodes wandern. Für SR‑IOV/VFs weise ich VFs dem passenden physischen Node zu und binde die IRQ‑Queues an die CPU‑Sets der Pods oder VMs, die sie bedienen.

First‑Touch, Warmup und Heaps gezielt vorbereiten

Viele Performance‑Fehler entstehen beim Start: Heaps wachsen in der Aufwärmphase dort, wo die ersten Requests landen – oft zentral auf einem Node. Ich fahre deshalb kontrollierte Warmups je Node: Starte Instanzen mit gesetzter CPU/Memory‑Maske, führe gezielte Pre‑Load‑Queries aus und initialisiere Caches parallel pro Node. Für JVM‑Dienste aktiviere ich Pre‑Touch des Heaps; für Datenbanken segmentiere ich Buffer‑Pools node‑weise. Das senkt spätere Page‑Migrationen und sorgt dafür, dass die ersten Requests nicht zufällig die Speicherverteilung prägen.

Kernel‑/BIOS‑Tuning für konstante Latenzen

Unter der Haube justiere ich Power‑ und Interrupt‑Politik:

  • CPU‑Governor auf „performance“, tiefe C‑States begrenzen, Package‑C‑States vorsichtig einsetzen, um Jitter zu reduzieren.
  • Memory‑Frequenz nicht drosseln; Balanced‑Energy‑Profile mindern oft Durchsatz unter Last.
  • Spread‑Spectrum/Clock‑Modulation vermeiden, wenn Konsistenz wichtiger als minimale Energieersparnis ist.

Auf Kernel‑Ebene halte ich Housekeeping‑CPUs getrennt von Latenz‑Kernen, minimiere Timer‑Interrupts auf Hot‑Kernen (nohz_full) und parke Hintergrundarbeiten (Kompaktion, Kswapd) bevorzugt auf System‑Kernen eines Nodes, der keine Latenzpfade fährt.

Troubleshooting und typische Anti‑Pattern

  • Symptom: P99‑Latenz springt nach Deploys. Ursache: Heaps/Caches first‑touch auf falschem Node. Lösung: Warmup/Pre‑Touch unter Ziel‑Affinity, danach Lastverteiler öffnen.
  • Symptom: Hohe SoftIRQ‑Zeit auf „falschen“ CPUs. Ursache: irqbalance verteilt über Nodes. Lösung: IRQ‑Affinity fixieren, RPS/RFS/XPS Node‑konform setzen.
  • Symptom: OOM in einem Node, obwohl System‑RAM frei. Ursache: Strikte NUMA‑Maske ohne Puffer. Lösung: Kapazität korrigieren oder „preferred“ nutzen, Alerts je Node etablieren.
  • Symptom: Unregelmäßiger Durchsatz bei NVMe. Ursache: Falsches Queue‑Mapping, Shared‑Queues cross‑Node. Lösung: blk‑mq/NVMe‑Queues pro Node, I/O‑Threads pinnnen.

Praxis‑Checkliste

  • Topologie aufnehmen: Nodes, Kerne, RAM, PCIe‑Geräte je Sockel.
  • Dienst‑Schnitt zeichnen: Welche Pfade sind Latenz‑kritisch, welche batchig?
  • CPU‑/Memory‑Affinity je Klasse setzen; First‑Touch beim Start beachten.
  • IRQ/Queues Node‑nah binden; RSS/RPS/XPS und NVMe‑Queues prüfen.
  • Monitoring auf P95/P99, Remote‑Zugriffe, Runqueue, IRQ‑Verteilung.
  • Autobalancing gezielt steuern; THP/zone_reclaim_mode passend wählen.
  • In VMs/Containern vNUMA, vCPU‑Pinning und vRAM‑Bindung konsistent halten.
  • Iterativ testen, dokumentieren, bei Drift zurückrollen und feiner justieren.

Kurzbilanz und Tuning‑Fahrplan

Die größte Rendite bringt es, Threads und Speicher zusammenzuhalten, I/O‑Wege zu verkürzen und nur behutsam zu verteilen. Ich starte mit Topologie‑Analyse, plane Dienste Node‑weise, setze CPU‑ und Memory‑Affinity, binde Netz/ Storage passend an und beobachte P95/ P99‑Werte mit Fokus auf Remote‑Zugriffe. Danach feile ich an Pool‑Größen, IRQ‑Masks und Policies, bis Latenzspitzen nachlassen und Durchsatz steigt. Für VMs und Container prüfe ich die Platzierung separat, weil der Hypervisor viel Einfluss hat und Grenzen anders wirken. Wer diesen Ablauf wiederholt und dokumentiert, holt aus Server NUMA Locality und CPU‑Memory Affinity messbar mehr Leistung heraus – oft günstiger als zusätzliche Hardware in Euro aufzurüsten.

Aktuelle Artikel