Das Symptom: Datenblatt-Hardware, Mittelklasse-Latenz
Der Ablauf ist immer derselbe. Es wird ein voll bestückter Zwei- oder Vier-Sockel-Server beschafft, fünfstellig, für die rechenintensive Datenbank oder einen dichten Virtualisierungs-Cluster. Auf dem Papier stehen hunderte Kerne und mehrere Terabyte RAM. Im Betrieb liefert die kritische VM dann Antwortzeiten, die niemand erklären kann.
Die CPU-Auslastung sieht harmlos aus. Die Platten sind schnell, das Netz ist leer, der Storage-Layer ist entlastet. Trotzdem steigt die Query-Latenz unter Last überproportional, und die Skalierung bricht ab, sobald man der VM mehr vCPUs gibt. Genau dieses Muster, mehr vCPUs machen es langsamer, ist der Fingerabdruck des Problems.
Der Grund liegt nicht in der Software und nicht im Storage. Er liegt in einer physikalischen Eigenschaft der Hardware, die der Hypervisor per Default nur halbherzig an den Gast weitergibt.
Ein Multi-Socket-Server ist kein einziger Speicherblock
Seit dem Ende des gemeinsamen Frontside-Bus hängt der Arbeitsspeicher nicht mehr an einem zentralen Chipsatz, sondern an Speichercontrollern im Prozessor selbst. Jeder Sockel besitzt seine eigenen Speicherkanäle und damit seinen eigenen physischen RAM-Bereich. Das ist Non-Uniform Memory Access: die Zugriffskosten hängen davon ab, welcher Kern welchen Speicher anfasst.
Greift ein Kern auf die RAM-Bänke seines eigenen Sockels zu, geht der Zugriff direkt in den lokalen Controller. Braucht er Daten, die im Speicher des zweiten Sockels liegen, läuft die Anfrage über den Interconnect zwischen den Prozessoren, bei Intel über UPI, bei AMD über Infinity Fabric. Dieser Umweg kostet zweimal: zusätzliche Latenz pro Zugriff und eine Bandbreitengrenze, die deutlich unter dem liegt, was der lokale Speichercontroller kann.
numactl --hardware sie ausgibt. Lokal ist per Definition auf 10 normiert. Es sind Herstellerangaben zur Topologie, keine gemessenen Nanosekunden: die reale Differenz hängt an Plattform, Speicherbestückung und Last und gehört auf dem eigenen Blech nachgemessen.Und es endet nicht bei der Sockel-Grenze. Moderne CPUs zerlegen sich auf Wunsch selbst: AMD nennt es NPS (Nodes Per Socket, mit NPS1, NPS2 oder NPS4), Intel nennt es Sub-NUMA Clustering. Ein einzelner Epyc im NPS4-Modus präsentiert dem Betriebssystem vier NUMA-Nodes. Ein Zwei-Sockel-System hat dann acht. Wer glaubt, das Thema betreffe nur Vier-Sockel-Maschinen, hat die BIOS-Einstellung nicht gelesen.
# Die eine Ausgabe, die alles entscheidet. Zuerst auf dem HOST ausfuehren.
numactl --hardware
# available: 2 nodes (0-1)
# node 0 cpus: 0 1 2 ... 23 48 49 ... 71
# node 0 size: 257512 MB
# node 0 free: 118204 MB <- wichtig: freier RAM PRO Node
# node 1 cpus: 24 25 ...
# node distances:
# node 0 1
# 0: 10 21
# 1: 21 10
# Kurzform, falls numactl fehlt
lscpu | grep -i numa
Was der Hypervisor daraus macht
Alle gängigen Hypervisor kennen NUMA und versuchen, eine VM auf einem Node zusammenzuhalten. Der ESXi-NUMA-Scheduler weist jeder VM ein Home-Node zu, der Linux-Kernel kennt automatisches NUMA-Balancing, das Seiten hinter den Prozessen herzieht. Das funktioniert, solange die VM in einen Node passt und der Node genug freien Speicher hat.
Kippt eine dieser Bedingungen, wird es unangenehm. Der Scheduler verteilt vCPUs und Speicherseiten über mehrere Nodes, und ab diesem Moment entscheidet die Frage, ob der Gast überhaupt weiß, dass es mehrere Nodes gibt. Weiß er es nicht, sieht das Gast-Betriebssystem einen flachen, gleichförmigen Speicher und platziert seine Threads und Puffer entsprechend ahnungslos. Der Datenbank-Scheduler im Gast optimiert dann sorgfältig gegen eine Topologie, die es so nicht gibt.
Dazu kommen mehrere Mechanismen, die eine einmal saubere Platzierung im laufenden Betrieb wieder zerstören:
- VM breiter als der Node. Eine VM mit 32 vCPUs auf einem Host mit 24 Kernen pro Sockel ist per Konstruktion auf beiden Sockeln. Ohne vNUMA im Gast ist damit rund die Hälfte aller Zugriffe remote.
- Speicher passt nicht. Der Node hat noch Kerne frei, aber keine 192 GB. Der Hypervisor nimmt die Kerne lokal und den Speicher von nebenan.
- Live-Migration. Nach dem Umzug auf einen Host mit anderer Topologie stimmt die dem Gast präsentierte vNUMA-Struktur nicht mehr mit der Realität überein. Der Gast optimiert weiter gegen die alte Karte.
- Memory Ballooning und Page Sharing. Beides bewegt Seiten, ohne die Node-Zugehörigkeit als Primärziel zu behandeln.
- vCPU Hot-Add. Der Klassiker aus der vSphere-Welt: aktiviertes CPU-Hot-Add zwingt die VM historisch in einen einzigen virtuellen NUMA-Node, egal wie breit sie ist. vSphere 8 kann das auflösen, aber nur über die Eigenschaft
exposeVnumaOnCpuHotadd, die ausschließlich per API gesetzt werden kann und nicht im UI auftaucht. Wer sie nicht aktiv gesetzt hat, hat sie nicht.
Messen statt raten
Bevor irgendetwas gepinnt wird, wird gemessen. NUMA-Tuning nach Bauchgefühl macht die Sache regelmäßig schlimmer, weil man dem Scheduler Freiheiten nimmt, ohne den eigentlichen Engpass zu treffen. Es gibt drei Fragen, und für jede eine Ausgabe.
Frage 1: Wie viele Zugriffe landen auf dem falschen Node?
# Hostweite Zaehler. numa_miss und numa_foreign sind die Verlustposten:
# Speicher, der nicht dort allokiert wurde, wo er gewollt war.
numastat
# Pro VM aufschluesseln. Streut eine Zeile ueber beide Node-Spalten,
# liegt der RAM dieser VM verteilt und nicht lokal.
numastat -p $(pgrep -f 'guest=db01')
numastat -c qemu
Frage 2: Sieht der Gast die Topologie überhaupt?
Diese Prüfung wird fast immer vergessen. Sie läuft in der VM, nicht auf dem Host. Meldet der Gast einen einzigen Node, obwohl er über zwei Sockel gespannt ist, ist genau das der Fehler.
# Im Linux-Gast
numactl --hardware
lscpu | grep -i 'numa\|socket'
# Im Windows-Gast (Sysinternals)
# coreinfo64.exe -n
# PowerShell: Get-WmiObject Win32_ComputerSystem | select NumberOfProcessors
Frage 3: Wie viel Speicher ist lokal?
Unter ESXi liefert esxtop im Memory-Screen (Taste m) genau dafür eine Kennzahl. Interessant sind NRMEM (remote), NLMEM (lokal) und vor allem N%L, der Anteil lokalen Speichers pro VM. NMIG zeigt zusätzlich, wie oft der Scheduler die VM zwischen Nodes hin und her geschoben hat.
| Beobachtung | Interpretation | Nächster Schritt |
|---|---|---|
| N%L nahe 100 | VM läuft lokal | NUMA ist nicht euer Problem, weitersuchen |
| N%L um 50, VM breit | über zwei Nodes gespannt | vNUMA prüfen, sonst schmaler schneiden |
| N%L um 50, VM schmal | Speicher passte nicht in den Node | Node-Belegung entzerren, RAM reduzieren |
| NMIG steigt laufend | Scheduler schiebt die VM | Host überbucht, Konsolidierung prüfen |
| numa_miss wächst | Allokation weicht aus | freien RAM pro Node prüfen |
Die Reparatur: Topologie durchreichen, dann binden
Die Lösung besteht aus zwei Schritten, und die Reihenfolge ist nicht verhandelbar. Zuerst wird dem Gast die echte Topologie gezeigt, danach wird die VM an die passenden physischen Ressourcen gebunden. Wer nur pinnt, ohne vNUMA zu konfigurieren, hat eine VM, die zwar lokal liegt, aber immer noch keine Ahnung davon hat.
Regel 1: VMs so schneiden, dass sie in einen Node passen
Das ist die wirksamste Maßnahme, und sie kostet nichts außer dem Verzicht auf eine runde Zahl im Ticket. Eine VM mit 16 vCPUs auf einem Host mit 24 Kernen pro Sockel läuft komplett lokal. Dieselbe VM mit 32 vCPUs läuft es nie. Dasselbe gilt für den Speicher: die Obergrenze ist der freie RAM eines Nodes, nicht der des Hosts.
Regel 2: Wenn die VM größer sein muss, vNUMA sauber spiegeln
Eine VM, die zwingend breiter als ein Sockel sein muss, bekommt eine virtuelle Topologie, die der physischen entspricht: zwei vNUMA-Nodes zu je der Hälfte der vCPUs und des Speichers, jeder davon an einen echten Host-Node gebunden. Dann kann der Scheduler im Gast, und dann auch die Datenbank darüber, die richtigen Entscheidungen treffen.
<!-- Gast-Topologie: zwei vNUMA-Nodes, die die Hardware abbilden -->
<cpu mode='host-passthrough'>
<topology sockets='2' cores='8' threads='1'/>
<numa>
<cell id='0' cpus='0-7' memory='98304' unit='MiB'/>
<cell id='1' cpus='8-15' memory='98304' unit='MiB'/>
</numa>
</cpu>
<!-- Jeder vNUMA-Node an den passenden Host-Node, strikt -->
<numatune>
<memnode cellid='0' mode='strict' nodeset='0'/>
<memnode cellid='1' mode='strict' nodeset='1'/>
</numatune>
<!-- vCPUs auf die Kerne des jeweiligen Sockels festnageln -->
<cputune>
<vcpupin vcpu='0' cpuset='0'/>
<vcpupin vcpu='1' cpuset='1'/>
<!-- ... -->
<vcpupin vcpu='8' cpuset='24'/>
<emulatorpin cpuset='0-1'/>
</cputune>
Unter Proxmox VE dasselbe in Kurzform. cpus sind die vCPU-IDs im Gast, hostnodes ist der physische Node, policy=bind macht die Bindung strikt statt nur bevorzugt. Zwei Details entscheiden hier ueber Erfolg oder Wirkungslosigkeit, und beide werden regelmäßig übersehen.
Erstens ist numa: 1 allein nur Topologie, keine Platzierung. Ohne explizite numaX-Zeilen erzeugt Proxmox lediglich -numa node,nodeid=…,cpus=…,memdev=…, einen virtuellen Node je Socket, ohne host-nodes und ohne policy. Der Gast bekommt damit eine vNUMA-Karte, die an keine physische Struktur gebunden ist, und optimiert sorgfältig gegen etwas, das es so nicht gibt. Erst hostnodes plus policy=bind macht die Karte wahr.
Zweitens bindet hostnodes nur den Speicher. Es wirkt auf das Memory-Backend, nicht auf die vCPU-Threads. Die darf der Kernel-Scheduler weiterhin auf jeden beliebigen Kern schieben, auch auf den anderen Sockel. Für die CPU-Seite braucht es zusätzlich --affinity, und dort stehen im Gegensatz zu numaX cpus= die Host-CPU-IDs aus numactl --hardware.
# Schmale VM, passt in einen Node: alles auf Sockel 0
qm set 101 --numa 1
qm set 101 --numa0 cpus=0-15,memory=196608,hostnodes=0,policy=bind
# Speicher ist damit gebunden, die vCPU-Threads noch NICHT.
# --affinity nimmt HOST-CPU-IDs: hier die Kerne von Node 0 inkl. SMT-Geschwister.
qm set 101 --affinity 0-23,48-71
# Breite VM, muss ueber beide Sockel: Topologie 1:1 spiegeln
qm set 102 --numa 1
qm set 102 --numa0 cpus=0-15,memory=131072,hostnodes=0,policy=bind
qm set 102 --numa1 cpus=16-31,memory=131072,hostnodes=1,policy=bind
# Sockets/Cores muessen zur vNUMA-Aufteilung passen
qm set 102 --sockets 2 --cores 16
# Gegenprobe
qm config 102 | grep -E 'numa|sockets|cores|memory'
kernel.numa_balancing), und der kennt nur Threads, keine VMs. Die vCPU-Threads einer breiten VM werden einzeln balanciert und driften entsprechend auseinander. Für den Regelfall reicht das. Für die kritischen VMs ist es der Grund, warum man unter KVM expliziter arbeiten muss als unter vSphere.
Regel 3: Hugepages pro Node reservieren
Große Speicherbereiche in VMs profitieren stark von Hugepages, weil der TLB-Druck sinkt. Der Haken: Hugepages werden pro NUMA-Node reserviert. Wer sie nur global setzt, bekommt sie irgendwo, und im schlimmsten Fall zieht sich eine sauber gepinnte VM ihre Hugepages vom falschen Sockel.
# Belegung pro Node ansehen, nicht global
cat /sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages
cat /sys/devices/system/node/node1/hugepages/hugepages-1048576kB/nr_hugepages
# Gezielt auf Node 0 reservieren (hier 192 x 1 GiB)
echo 192 > /sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages
# Automatisches NUMA-Balancing stoert bei strikt gepinnten Gaesten,
# weil es Seiten verschiebt, die bereits richtig liegen.
sysctl kernel.numa_balancing
Unter vSphere
Der ESXi-NUMA-Scheduler ist gut, und in den meisten Fällen ist die richtige Maßnahme, ihn arbeiten zu lassen und lediglich die VM richtig zu dimensionieren. Wo eingegriffen werden muss, sind es diese Stellschrauben:
| Einstellung | Wirkung | Einsatz |
|---|---|---|
| Cores per Socket | bestimmt die vNUMA-Aufteilung | an die physische Kernzahl pro Sockel angleichen |
| numa.vcpu.maxPerVirtualNode | vCPUs pro virtuellem Node | nur, wenn die Automatik danebenliegt |
| numa.nodeAffinity | bindet die VM hart an Host-Nodes | letztes Mittel, blockiert den Scheduler |
| exposeVnumaOnCpuHotadd | vNUMA trotz CPU-Hot-Add | vSphere 8, nur per API setzbar |
| Latency Sensitivity: High | exklusive Kernzuteilung | nur mit vollständiger Reservierung |
Der ehrliche Teil: Pinning hat einen Preis
Wer nur die Vorteile erzählt, verkauft unsauber. Striktes Pinning tauscht Flexibilität gegen Vorhersagbarkeit, und dieser Tausch ist nicht in jeder Umgebung richtig.
| Was ihr aufgebt | Konkret | Wann es trotzdem richtig ist |
|---|---|---|
| Scheduler-Freiheit | der Host kann Last nicht mehr umverteilen | wenige, dafür kritische VMs |
| Konsolidierungsgrad | Node-Fragmentierung lässt Kerne brachliegen | Latenz zählt mehr als Dichte |
| Migrationsfreiheit | Zielhost braucht dieselbe Topologie | homogene Cluster |
| Einfachheit | Konfiguration muss gepflegt werden | es ist dokumentiert und im Monitoring |
Daraus folgt eine unspektakuläre, aber belastbare Praxis: Pinnt selektiv. Die zwei oder drei VMs, an denen die Applikationsleistung wirklich hängt, bekommen eine feste Topologie und werden überwacht. Der Rest des Clusters läuft weiter unter dem normalen Scheduler, der für den Regelfall gut genug ist. Ein Cluster, in dem jede VM gepinnt ist, ist kein optimierter Cluster, sondern ein starrer.
Auch der umgekehrte Fehler existiert: Umgebungen, in denen aus Angst vor NUMA gar keine breiten VMs mehr gebaut werden und stattdessen die Applikation über viele kleine Instanzen verteilt wird. Wenn die Software das nicht sauber kann, verlagert das den Engpass nur ins Netz. Die Frage ist nicht „breit oder schmal", sondern ob die Breite bewusst gewählt und die Topologie ehrlich abgebildet ist.
Betriebsregeln, die den Fehler dauerhaft verhindern
NUMA-Fehler entstehen selten beim Aufbau. Sie entstehen ein halbes Jahr später, wenn jemand einer VM „mal eben" acht vCPUs mehr gibt. Deshalb gehört das Thema in den Betrieb, nicht nur ins Design.
| Regel | Warum |
|---|---|
| NUMA-Node-Größe dokumentieren, nicht Host-Größe | Kerne und GB pro Node sind die echte Obergrenze für eine lokale VM |
| Sizing gegen die Node-Größe, nicht gegen runde Zahlen | 16 vCPU lokal schlagen 32 vCPU verteilt bei speicherlastiger Last regelmäßig |
| vCPU-Aenderung ist eine Architekturänderung | über die Node-Grenze zu wachsen ändert das Leistungsverhalten sprunghaft |
| NPS bzw. SNC im BIOS bewusst setzen und protokollieren | bestimmt, wie viele Nodes es überhaupt gibt, und verändert alle Rechnungen darüber |
| Hosts eines Clusters topologisch identisch halten | sonst ist jede Migration ein Konfigurationsbruch |
| Speicherlokalität ins Monitoring | N%L bzw. numastat zeigen die Regression, bevor es Tickets tun |
| Hot-Add für breite VMs aus | sonst läuft die VM ohne Topologie-Information |
precipice.tech