Schlechte NUMA-Konfigurierbarkeit für KVM-Gäste.

GhostTyper

New Member
Mar 10, 2025
7
0
1
Germany
Hallo Forum.

Die NUMA-Konfigurierbarkeit für KVM-Gäste ist entweder schlecht oder ich bin zu blöd. Wie dem auch sei, wir finden sicher zusammen heraus, was davon zutrifft. :D Ich bin noch auf 8.4.14, glaube aber, dass sich in dem Bereich nicht viel getan hat.

Wir haben 5 Server mit jeweils 2xEPYC 9375F, also 2 NUMA-Nodes pro Server, wenn wir die einzelnen CCDs ignorieren.

Relevante Konfigurationsparameter sind: affinity, numa und numa0. Wenn ich also eine VM habe, die in eine NUMA-Node (RAM sowie weniger als NUMA-Node CPU Kerne -2 oder so) passt ist das an für sich kein Problem. Ich setze affinity auf die Cores der ersten NUMA-Node, setze numa auf 1 und in numa0 hostnodes auf 0, memory auf den RAM der VM und policy auf bind und Zack läuft die VM (je nach Last) c.A. 35% schneller im Vergleich dazu, wenn ich das nicht tun würde.

Wenn ich nun aber eine VM habe, die zwei NUMA-Nodes braucht, weil die Anwendung mehr Ressourcen benötigt als eine NUMA-Node (also eine CPU und/oder der RAM der an ihr angebunden ist) zur Verfügung stellen kann habe ich ein Problem. affinity setzt nämlich die Zugehörigkeit für alle Threads des KVM-Prozesses, was bedeutet, dass ich dem Linux-Scheduling gnadenlos ausgeliefert bin und ich im Prinzip wieder die oben gewonnen ~35% Performance verliere, da die vCPU Threads von KVM auf beiden Host-CPUs umherscheduled werden.

Vielleicht eine kurze Zwischenfrage: Übersehe ich hier etwas oder kann man das tatsächlich nicht besser konfigurieren?

Follow-Up: Ich möchte VMs gerne lastabhängig ähnlich zu drs bei VMware (nur natürlich viel primitiver) verschieben. Dazu habe ich ein Script geschrieben, welches aus CPU%, Netzwerk und Disk I/O einen Score bildet und dann Server+NUMA-Nodes versucht "gleich" (sodass der kumulierte Score nachher möglichst gleich über alle NUMA-Nodes des Clusters ist) aufzufüllen und zwar mit so wenig wie möglich verschiebe Operationen.

Dabei gibt es folgende Herausforderungen:
  1. Ich kann über einen API-Key, der alles darf keine affinity setzen. Ich muss dazu einen root-Login machen und kann mit dem Ticket diese Einstellung ändern. (An für sich kein Problem, nur eine Umständlichkeit.)
  2. Wenn ich die Konfiguration ändere gehen die Einstellungen in [PENDING]. Das ist in dem Fall schlecht, denn ich möchte diese Einstellung wenn es auch nicht gleich geht spätestens nach einer Migration übernommen wissen. Also dachte ich, dass ich mich mit dem Script per SSH einlogge und die Datei direkt auf der Disk ändere und dann die Migration anstoße. Das funktioniert auch gut, solange vorher numa schon auf 1 war und anschließend migriert wird.
Ich hoffe ich habe genug Kontext für die folgenden Fragen geliefert:
  1. Gibt es ohne hookscript eine Möglichkeit die physischen logischen CPUs vCPUs zuzuordnen, sodass ich Konfigurationen bei denen eine VM mindestens 2 NUMA-Nodes abdecken muss effizient konfiguriert werden können?
  2. Über sehe ich etwas? Kann ich über die API eine Konfiguration auch ohne [PENDING] übernehmen - zur Not reicht es diese in die Config-Datei zu schreiben?
  3. Gibt es eine Roadmap auf der evtl. bessere NUMA-Konfigurierbarkeit oder zumindest das Live-Setzen der affinity und bisherigen NUMA-Konfiguration steht?
  4. Gibt es schon Community-Lösungen für diese Aufgabe (drs), welche das auch gut bei großen VMs die mehrere NUMA-Nodes überdecken nachrüstet?
Danke.

PS: Ja, ich brauche wirklich eine so große VM und ja die Software in dieser ist NUMA aware.
PPS: numa_balancing ändert da nicht viel und ist keine Lösung.
 
Ich glaube du machst das alles zu kompliziert.
Bei vSphere DRS hast du auch keine Cores angepinnt sondern das dem Scheduler überlassen.
Mach das doch bei KVM genauso.
Alle normalen VMs werden wenn möglich immer je von einer CPU bedient.
Hast du eine ganz große VM dann aktivierst du vNUMA und gibst der VM 2 Sockets.
Deine NUMA Aware Anwendung erkennt dann die beiden CPUs und wählt dann die richtige aus.
Übrigens gibts da schon was fertiges fürs Balancing. proxLB macht das was DRS bis vSphere7 getan hat.
Mit vSphere8 hat sich DRS geändert und man hat einen Score der die Latenz der VM beurteilt und nicht die Auslastung des Hosts.
Das gibts bisher nur aus dem Hause VMware.
 
  • Like
Reactions: Johannes S
Hey Falk,

danke für den Tipp zu proxLB, ich werde mir das angucken.

Allerdings kann ich "Alle normalen VMs werden wenn möglich immer je von einer CPU bedient" nicht bestätigen.

Ich untermauer' das mal mit Benchmarks für Speicherzugriff (leerer Host, getestet mit likwid-bench, VM mit 8 vCPUs und 64GiB RAM):

Gedankennuma_balancingaffinitynumahostnodesload (MB/s)stream_mem (MB/s)
Die Worst-Case-Baseline (Memory und CPUs auf unterschiedlichen NUMA-Nodes.)00-3111108798.89129484.39
10-3111108874.16129727.41
0-11132030.29198979.65
1-11223454.97237322.70
Nur NUMA zu aktivieren scheint schlechter zu sein als es nicht zu aktivieren, bei sonst fehlender Konfiguration.0-1-267856.26380375.15
1-1-232157.76287570.17
Das ist die "gar nichts konfiguriert" Situation.0-0-239944.94304983.40
1-0-304077.70360406.59
Ab hier sehen wir im Prinzip gleich gute Ergebnisse weil ungebundener Speicher normalerweise first touch allokiert wird, hier also Speicher immer bei den in affinity festgelegten CPUs liegt.00-3110359336.63407190.39
10-3110359264.93406696.50
00-311-359149.97404689.63
10-311-361463.18408233.35
00-310-358511.18406472.15
10-310-363356.08409195.52

Was die Geschwindigkeit also signifikant verbessert ist es die affinity auf die passende NUMA-Node (also hier Socket) zu setzen - dann geht durch Strategien wie first touch Allokation eigentlich alles automatisch und wir können im Gast sogar den NUMA-Overhead vermeiden. (Solange die VM in eine NUMA-Node passt.)

Wenn man Proxmox einfach machen lässt ist Speicherzugriff (in meinem Fall) mindestens 10% schlechter, je nach Konfiguration des Hosts (numa_balancing) sogar noch mehr.

Für VMs die über 2 NUMA-Nodes gehen müssen sehen wir im Prinzip ähnliche Verhältnisse bei den Zahlen, nur dass man hier halt keine Chance mehr mit affinity hat, weil diese nicht pro nodeX festlegbar ist. Es fällt also alles mit "höchstem" Durchsatz weg.
 
Also die gar nichts konfigurieren Situation ist eher der Normalzustand und reicht für 95% der Fälle aus.
Wenn man einzelne kritische VMs hat, kann man natürlich Tuning betreiben.
Ich empfehle seit jahren nur noch Single Socket Server zu nutzen und die Anzahl der Cores reicht bei AMD eh immer aus. Da spart man sich viel Stress mit NUMA.
Wenn du zwei 128Core CPUs benötigst, weil die 256 Core CPU zu wenig Taktrate hat, dann ist das eher ein Einzelfall und da darf man auch mal ins Tuning investieren. Das jetzt für alle VMs zu machen, finde ich persönlich etwas drüber.
 
Hey Falk,

ich stimme Dir da ganz generell zu. Vor allem weil die Performance nachher keine 10+% schlechter ist, sondern nur der Speicherzugriff und wahrscheinlich ist es auch nicht wirtschaftlich, wenn ich mich weiter damit beschäftige.

Aber Wirtschaftlichkeit ist nicht das einzige Kriterium, sondern auch einfach der Ansporn dieses Verhalten so Automatisch es geht zu verbessern. Für kleine VMs geht das gut, für große VMs gibt es halt technisch (außer wahrscheinlich hook-scripts) keine Möglichkeit das gut zu machen. (Oder ich kenne sie nicht.)

Schade ist halt, dass VMware ESXi das seit vielen Jahren (nach meinen Tests) selbst unter schwierigen Bedingungen automatisch besser macht.

Du bist übrigens nicht die einzige Person, die von NUMA-Systemen in Verbindung mit Proxmox abrät und ich verstehe jetzt auch warum. Aber es gibt Gründe für NUMA-Systeme abseits der Taktrate, nämlich z.B. doppelte Speicherbandbreite, doppelten Cache, die bei der Nischen-Anwendung dieser großen VMs nützlich sein können.
 
Genau für diese Nischenanwendungen sind diese Server sinnvoll. Aber der Mittelständler mit wenigen Admins, nimmt lieber das simple Setup. vSphere macht einiges richtig, dafür bekomme ich bei DB Servern mit Proxmox deutlich mehr I/O hin.
Für den Standardworkload sind beide Systeme super. Nur die Preisgestaltung von Broadcom ist da so ein Ding.
 
  • Like
Reactions: Johannes S
Du bist übrigens nicht die einzige Person, die von NUMA-Systemen in Verbindung mit Proxmox abrät und ich verstehe jetzt auch warum. Aber es gibt Gründe für NUMA-Systeme abseits der Taktrate, nämlich z.B. doppelte Speicherbandbreite, doppelten Cache, die bei der Nischen-Anwendung dieser großen VMs nützlich sein können.
Ich habe mir alles noch einmal durchgelesen. Ist deine Anwendung in der großen VM die über 2 NUMA Nodes geht auch NUMA Aware?
Wenn du NUMA aktivierst, wird die Information über die NUMA Nodes an die VM weiter gegeben und die Applikation muss entscheiden über welche CPU welcher Speicherbereich angesprochen wird. Bei MS SQL funktioniert das sauber und man kann da nachvollziehen, dass da fast kein Traffic über den QPI Link geht.
 
Hey Falk,

ja, die Software ist NUMA-Aware, hab' das extra eingebaut. Es ist nur sehr, sehr schwierig einen guten reproduzierbaren NUMA-Benchmark zu machen, denn ein synthetischer Benchmark ist zu Perfekt. "Echte" Anwendungsfälle verursachen auch (wenn auch hoffentlich weniger) traffic auf der remote node was die Messung vor allem bei numa_balancing verfälscht. Ich unternehme trotzdem den Versuch (und ohne numa_balancing).

Wird numa: 1 gesetzt, aber keine dedizierte NUMA-Konfiguration vorgenommen (siehe unten), dann sehen wir, dass Proxmox 2 NUMA-Nodes anlegt. Die Threads der vCPUs werden aber dadurch leider ähnlich wie beim "kleine VM-Fall" nicht an logische CPUs gebunden:

Code:
root@node3:~# cat /run/qemu-server/150.pid
4008545
root@node3:~# ps -T -p 4008545
    PID    SPID TTY          TIME CMD
4008545 4008545 ?        00:00:44 kvm
4008545 4008546 ?        00:00:00 call_rcu
4008545 4008547 ?        00:00:00 kvm
4008545 4008549 ?        00:00:00 log
...
4008545 4008591 ?        00:00:13 CPU 0/KVM
4008545 4008592 ?        00:00:00 CPU 1/KVM
4008545 4008593 ?        00:00:00 CPU 2/KVM
...
4008545 4008648 ?        00:00:00 CPU 57/KVM
4008545 4008649 ?        00:00:00 CPU 58/KVM
4008545 4008650 ?        00:00:00 CPU 59/KVM
4008545 4008652 ?        00:00:00 vnc_worker
4008545 4008653 ?        00:00:00 kvm-nx-lpage-re
root@node3:~# grep Cpus_allowed_list /proc/4008591/status
Cpus_allowed_list:      0-63
root@node3:~# grep Cpus_allowed_list /proc/4008592/status
Cpus_allowed_list:      0-63
root@node3:~# grep Cpus_allowed_list /proc/4008650/status
Cpus_allowed_list:      0-63

Hier die Benchmark-Messwerte (Kommandozeilen für meine VM siehe unten):

ThreadsTestLOCAL (MB/s)REMOTE (MB/s)REMOTE/LOCALPenalty
N0load229153.88227308.080.992−0.81%
N0stream_mem257462.87259335.101.007+0.73%
N1load237212.92213443.410.900−10.02%
N1stream_mem257202.90268560.951.044+4.42%

Ergebnis: Lokal und Remote Zugriff aus der VM heraus ist "ununterscheidbar". Warum? Weil Proxmox die vCPU-Threads nicht bindet.

Mein Fall, wie man es "richtig" machen würde (aber leider nicht über reguläre Konfiguration erreichbar ist):

NUMA fix konfigurieren und Speicher binden, weil ich für das manuelle Setup zu langsam bin:

Code:
numa0: cpus=0-29,hostnodes=0,memory=131072,policy=bind
numa1: cpus=30-59,hostnodes=1,memory=131072,policy=bind

Nach dem Start der VM (das müsste ein hook_script machen):

taskset -pc 0-29 SPIDs bzw. taskset -pc 30-59 SPIDs für alle Threads der jeweiligen vCPU.

Dann wieder Benchmarks (die gleichen von oben, hier die Kommandozeilen):

Code:
# LOCAL (Threads auf Node0, RAM auf Node0)
likwid-bench -t load       -w M0:48GB:30
likwid-bench -t stream_mem -w M0:48GB:30

# REMOTE (Threads auf Node0, RAM auf Node1)
likwid-bench -t load       -w M0:48GB:30-0:M1
likwid-bench -t stream_mem -w M0:48GB:30-0:M1,1:M1,2:M1

# LOCAL (Threads auf Node1, RAM auf Node1)
likwid-bench -t load       -w M1:48GB:30
likwid-bench -t stream_mem -w M1:48GB:30

# REMOTE (Threads auf Node1, RAM auf Node0)
likwid-bench -t load       -w M1:48GB:30-0:M0
likwid-bench -t stream_mem -w M1:48GB:30-0:M0,1:M0,2:M0

Messwerte:

ThreadsTestLOCAL (MB/s)REMOTE (MB/s)REMOTE/LOCALPenalty
N0load517604.93149089.100.288×−71.20%
N0stream_mem482986.15152136.380.315×−68.50%
N1load493887.56147325.450.298×−70.17%
N1stream_mem483312.58159090.910.329×−67.08%

Ergebnis: Hier sieht man ziemlich genau, wie in der VM die NUMA penalty greift, aber wenn die Software NUMA aware ist auch wesentlich höhere Performance.

Bei MS SQL funktioniert das sauber und man kann da nachvollziehen, dass da fast kein Traffic über den QPI Link geht.

MSSQL hat NUMA support. Aber in Proxmox unter der Konfiguration, wenn man nur NUMA (numa: 1) aktiviert (kein affinity bei < vCPUs als logische CPUs einer NUMA-Node, etc.) sehe ich nicht, wie das sein kann.

Tests wie oben sind sehr aufwändig und einer mit MSSQL wäre für mich noch aufwändiger, ich würde das aber aufsetzen mit einem SQL-Benchmark. So überzeugt bin ich, dass Proxmox unbedinkt ein Setting bei numaX braucht um für die Kerne eine Host-Affinity festzulegen.

Die Zahlen oben sind ohne numa_balancing. Die VM hat 256 GiB RAM und 2x30 vCPUs.
 
Ich bin da nicht so tief in der Materie, aber das soll wohl über irgendwelche IDs der Cores gehen bei MS SQL.
Soll ja auch bei HyperV funktionieren, wo man gar nichts tunen kann. ;)
 
Hey Falk,

habe das jetzt Mal mit MSSQL gebenchmarked (Details siehe unten):

vCPU-LayoutNUMA ohne KonfigurationNUMA mit Binding und KonfigurationΔ TPMΔ %Faktor
2×12 vCPUs516863554498+37 635+7.28%1.073×
2×4 vCPUs369665406298+36 633+9.91%1.099×

Wir bekommen also knappe 10% Leistungsunterschied.

Feature-Request

Cool wäre es, wenn man den numaX-Zeilen auch eine Affinity mitgeben könnte, die vor der Ausführung eines Hook-Scripts gesetzt würden:

numa0: cpus=0-3,hostnodes=0,affinity=0-31,memory=81920,policy=bind
^^^^^^^^^^^^^

HammerDB Benchmark

Code:
set MSSQL_SERVER "1.2.3.4"
set MSSQL_PORT 1433
set MSSQL_AUTH "sql"
set MSSQL_UID "sa"
set MSSQL_PASS "geheim"
set TPCC_DB "tpcc"

set WAREHOUSES 1000
set VU 640
set RAMPUP_MIN 10
set DURATION_MIN 20

catch { vudestroy }

dbset db mssqls
dbset bm TPC-C

diset connection mssqls_server $MSSQL_SERVER
diset connection mssqls_tcp true
diset connection mssqls_port $MSSQL_PORT
diset connection mssqls_authentication $MSSQL_AUTH
diset connection mssqls_uid $MSSQL_UID
diset connection mssqls_pass $MSSQL_PASS
diset connection mssqls_encrypt_connection true
diset connection mssqls_trust_server_cert true

diset tpcc mssqls_dbase $TPCC_DB
diset tpcc mssqls_count_ware $WAREHOUSES
diset tpcc mssqls_total_iterations 10000000
diset tpcc mssqls_driver timed
diset tpcc mssqls_rampup $RAMPUP_MIN
diset tpcc mssqls_duration $DURATION_MIN
diset tpcc mssqls_allwarehouse true
diset tpcc mssqls_timeprofile false
diset tpcc mssqls_keyandthink false
diset tpcc mssqls_async_scale false
diset tpcc mssqls_connect_pool false
diset tpcc mssqls_raiseerror true

vuset logtotemp 1
vuset showoutput 0

loadscript
vuset vu $VU
vucreate
vurun
vudestroy
exit

Relevantes für: NUMA ohne Konfiguration

Code:
cores: 4
machine: pc-q35-9.2+pve1
memory: 163840
numa: 1
sockets: 2

Relevantes für: NUMA mit Binding und Konfiguration

Code:
cores: 4
machine: pc-q35-9.2+pve1
memory: 163840
numa: 1
numa0: cpus=0-3,hostnodes=0,memory=81920,policy=bind
numa1: cpus=4-7,hostnodes=1,memory=81920,policy=bind
sockets: 2

Danach:

Die Threads der vCPU entsprechend binden, z.B.:

Code:
taskset -pc 0-31 3171817
taskset -pc 0-31 3171818
taskset -pc 0-31 3171819
taskset -pc 0-31 3171820
taskset -pc 32-63 3171821
taskset -pc 32-63 3171822
taskset -pc 32-63 3171823
taskset -pc 32-63 3171824

Ganz wichtig generell: Ceph hat zu hohe Latenz um MSSQL hier überhaupt auslasten zu können. Musste hier eine lokale NVMe SSD nehmen.