Precipice.tech Logo precipice.tech
Start / Blog / Die NUMA-Falle
Deep Dive · Virtualisierung

Euer Hypervisor drosselt die teuerste Hardware im Rack

Ein Zwei-Sockel-Server ist kein großer Rechner. Er ist zwei kleine mit einem Kabel dazwischen. Wer die Platzierung von vCPUs und RAM blind dem Scheduler überlässt, betreibt Datenbanken auf Hardware, deren halbe Speicherbandbreite nie ankommt. Die Reparatur kostet keine Lizenz, sondern eine Stunde Topologie-Arbeit.

Autor Jan Uka Stand KVM/libvirt, Proxmox VE 8, vSphere 8 Lesezeit ~13 min Level Admin / Architekt
01

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.

Für wen das relevant ist Für alles, was speicherlastig ist und über acht vCPUs hinausgeht: Datenbanken, In-Memory-Caches, JVM-Heaps im dreistelligen GB-Bereich, HPC-Jobs, Inference-Server. Ein Webserver mit vier vCPUs und 8 GB RAM merkt davon nichts. Die 512-GB-Datenbank merkt es an jedem Werktag. Warum lokale LLM-Inferenz besonders empfindlich darauf reagiert, steht in Speicherbandbreite schlägt Rechenleistung.
02

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.

LOKALKern und RAM am selben Sockel
10SLIT
REMOTE, 1 HOPüber UPI / Infinity Fabric
21SLIT
REMOTE, 2 HOPSQuad-Socket, diagonaler Node
32SLIT
Relative Zugriffskosten aus der ACPI-SLIT-Matrix, wie 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.
Abb. 1 · Was ein Speicherzugriff kostet, je nachdem wo er landet

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.

shell
# 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
Der häufigste Trugschluss „Wir haben 512 GB im Server." Nein. Ihr habt zweimal 256 GB, und eine VM, die 300 GB anfordert, kann nicht lokal bleiben. Das ist keine Konfigurationsfrage mehr, sondern Physik. Die Größe eines NUMA-Nodes, nicht die Gesamtkapazität, ist die Zahl, gegen die man kritische VMs dimensioniert.
03

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.

Abb. 2 · Dieselbe VM mit 16 vCPU und 192 GB auf einem Host mit 2 Sockeln zu je 24 Kernen und 256 GB (Modus oben umschalten)

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.
Bequemlichkeit mit Preisschild CPU-Hot-Add wird eingeschaltet, weil es einmal im Quartal einen Neustart spart. Bezahlt wird es mit einer Datenbank-VM, die dauerhaft ohne Topologie-Information läuft. Das ist kein guter Tausch. Für breite, speicherlastige VMs gehört Hot-Add aus, oder die vSphere-8-Eigenschaft explizit gesetzt und verifiziert.
04

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?

shell
# 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.

shell
# 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.

BeobachtungInterpretationNächster Schritt
N%L nahe 100VM läuft lokalNUMA ist nicht euer Problem, weitersuchen
N%L um 50, VM breitüber zwei Nodes gespanntvNUMA prüfen, sonst schmaler schneiden
N%L um 50, VM schmalSpeicher passte nicht in den NodeNode-Belegung entzerren, RAM reduzieren
NMIG steigt laufendScheduler schiebt die VMHost überbucht, Konsolidierung prüfen
numa_miss wächstAllokation weicht ausfreien RAM pro Node prüfen
Reihenfolge einhalten Erst Frage 2 beantworten, dann Frage 1. Eine VM, die über zwei Nodes läuft und die Topologie sauber durchgereicht bekommt, ist ein völlig anderer Fall als eine, die dieselbe Verteilung hat und nichts davon weiß. Im ersten Fall optimiert das Gast-Betriebssystem selbst mit, im zweiten arbeitet es gegen die Hardware.
05

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.

libvirt xml
<!-- 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.

proxmox
# 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'
Proxmox hat keinen NUMA-Scheduler Anders als ESXi kennt Proxmox kein Objekt „VM mit Home-Node“, das der Host als Ganzes zwischen Sockeln umziehen und dessen Speicher er hinterherziehen würde. Die Arbeit macht der Linux-Kernel: der Loadbalancer plus Automatic NUMA Balancing (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.

shell
# 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:

EinstellungWirkungEinsatz
Cores per Socketbestimmt die vNUMA-Aufteilungan die physische Kernzahl pro Sockel angleichen
numa.vcpu.maxPerVirtualNodevCPUs pro virtuellem Nodenur, wenn die Automatik danebenliegt
numa.nodeAffinitybindet die VM hart an Host-Nodesletztes Mittel, blockiert den Scheduler
exposeVnumaOnCpuHotaddvNUMA trotz CPU-Hot-AddvSphere 8, nur per API setzbar
Latency Sensitivity: Highexklusive Kernzuteilungnur mit vollständiger Reservierung
numa.nodeAffinity ist scharf Die Einstellung nimmt dem Scheduler jede Ausweichmöglichkeit. Fällt der gewählte Node unter Druck, kann die VM nicht mehr ausweichen, und in bestimmten Konstellationen kollidiert die harte Bindung mit DRS und Migration. Setzt sie nur, wenn die Messung aus Abschnitt 04 sie rechtfertigt, und dokumentiert sie an der VM.
06

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 aufgebtKonkretWann es trotzdem richtig ist
Scheduler-Freiheitder Host kann Last nicht mehr umverteilenwenige, dafür kritische VMs
KonsolidierungsgradNode-Fragmentierung lässt Kerne brachliegenLatenz zählt mehr als Dichte
MigrationsfreiheitZielhost braucht dieselbe Topologiehomogene Cluster
EinfachheitKonfiguration muss gepflegt werdenes 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.

07

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.

RegelWarum
NUMA-Node-Größe dokumentieren, nicht Host-GrößeKerne und GB pro Node sind die echte Obergrenze für eine lokale VM
Sizing gegen die Node-Größe, nicht gegen runde Zahlen16 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 protokollierenbestimmt, wie viele Nodes es überhaupt gibt, und verändert alle Rechnungen darüber
Hosts eines Clusters topologisch identisch haltensonst ist jede Migration ein Konfigurationsbruch
Speicherlokalität ins MonitoringN%L bzw. numastat zeigen die Regression, bevor es Tickets tun
Hot-Add für breite VMs aussonst läuft die VM ohne Topologie-Information
Zum Mitnehmen NUMA ist kein exotisches Tuning-Thema, sondern die Grundeigenschaft jeder Multi-Socket-Plattform und inzwischen auch vieler Single-Socket-Systeme. Die Hardware ist selten das Problem. Das Problem ist die Annahme, ein Server sei ein einziger, gleichförmiger Rechner. Wer die Topologie misst, die VMs dagegen dimensioniert und nur die kritischen Fälle bindet, holt die Leistung ab, die im Datenblatt steht. Ohne eine einzige zusätzliche Rechnung.
Nächster Schritt

Erst messen, dann nachkaufen

Wir sehen regelmäßig Cluster, in denen vor der Beschaffung zusätzlicher Hosts noch zweistellige Prozentpunkte an Leistung in der vorhandenen Hardware liegen. Wenn ihr das Muster aus Abschnitt 01 kennt, schauen wir uns die Topologie eures Clusters an, bevor Budget in neue Server fließt.