====== Proxmox: привязка CPU к виртуальным машинам ======
Не всегда очевидно, зачем вообще нужна привязка CPU к виртуальным машинам, особенно если речь идёт о небольших развертываниях - там этот параметр чаще всего просто игнорируют. Но в реальном продакшене использование CPU affinity становится действительно важным для повышения производительности виртуалок.
В рабочих окружениях мы обычно имеем дело с серверами на 32 и больше ядер. Чтобы уместить такое количество ядер в одном сокете, производители используют сложные схемы построения CPU. А для работы с большими объёмами оперативки - Non-Uniform Memory Access (NUMA). Эта архитектура ускоряет доступ к памяти за счёт разделения её на несколько узлов, где у каждого узла есть свой кусок памяти и свои ядра процессора.
Чтобы было понятнее, возьмём для примера архитектуру AMD Zen 4 (EPYC 9004). Она изначально рассчитана на работу с кучей ядер и поддерживает огромные объёмы памяти. В такой системе NUMA делит ресурсы на отдельные узлы, где у каждого узла есть собственная память и выделенные CPU-ядра.
{{https://fatalex.cifro.net/lib/plugins/ckgedit/fckeditor/userfiles/image/proxmox/0tur50yxuqgujsrgzdt7.png?nolink&}}
AMD Zen 4 architecture
В этом примере у нас есть 4 NUMA-узла. Каждый узел напрямую подключён к своему набору слотов памяти. На картинке видно, что у каждого узла по 6 слотов, то есть всего получаем 24 слота (6 слотов × 4 узла).
NUMA-узел включает 8 ядер. Каждое ядро имеет свои L1d и L1i кеши, кеш L2, а также общий L3 кеш для всех 8 ядер.
AMD EPYC 9354 32-Core Processor, информация о NUMA-узлах и кешах:
# lscpu
Caches (sum of all):
L1d: 1 MiB (32 instances)
L1i: 1 MiB (32 instances)
L2: 32 MiB (32 instances)
L3: 256 MiB (8 instances)
NUMA:
NUMA node(s): 4
NUMA node0 CPU(s): 0-7,32-39
NUMA node1 CPU(s): 8-15,40-47
NUMA node2 CPU(s): 16-23,48-55
NUMA node3 CPU(s): 24-31,56-63
Итак, у нас один CPU сокет с 32 ядрами и 64 потоками.
* Чип содержит 4 NUMA-узла.
* Каждый NUMA-узел имеет 8 ядер.
* У каждого ядра - 2 потока.
* Каждый NUMA-узел подключён к своим слотам памяти DDR5 и имеет собственный L3 кеш.
Linux предоставляет утилиту **numactl**, чтобы показать информацию о NUMA-узлах:
# numactl -Hs
available: 4 nodes (0-3)
node 0 cpus: 0 1 2 3 4 5 6 7 32 33 34 35 36 37 38 39
node 0 size: 128654 MB
node 0 free: 12659 MB
node 1 cpus: 8 9 10 11 12 13 14 15 40 41 42 43 44 45 46 47
node 1 size: 129019 MB
node 1 free: 12883 MB
node 2 cpus: 16 17 18 19 20 21 22 23 48 49 50 51 52 53 54 55
node 2 size: 129019 MB
node 2 free: 12683 MB
node 3 cpus: 24 25 26 27 28 29 30 31 56 57 58 59 60 61 62 63
node 3 size: 128977 MB
node 3 free: 13114 MB
node distances:
node 0 1 2 3
0: 10 12 12 12
1: 12 10 12 12
2: 12 12 10 12
3: 12 12 12 10
Если один узел обращается к памяти другого узла, это всегда медленнее, чем доступ к «своей» памяти. В таблице выше видно задержки: 10 - это доступ к памяти своего узла, 12 - к памяти соседних.
Чтобы добиться максимальной производительности, приложение должно выполняться на том же NUMA-узле, где выделена его память. А чтобы выжать максимум из кеша, ему стоит работать на тех же ядрах, где расположены кеши L1 и L2.
По умолчанию планировщик Linux может перемещать процессы между ядрами и NUMA-узлами. Но в таких случаях процесс теряет кеш и скорость работы с памятью. Современный планировщик достаточно умный и старается оставлять процесс на том же NUMA-узле, где выделена память. Но виртуальные машины не имеют информации о том, как устроены NUMA-узлы и ядра на хосте - поэтому для их планировщика данные о «дистанции» памяти и кешах недоступны.
Чтобы избежать NUMA-миграций и поднять производительность VM, можно использовать **CPU affinity**, то есть жёстко привязать виртуалку к конкретным ядрам и NUMA-узлам. Даже если Linux-планировщик будет гонять процесс по разным ядрам, он всё равно останется в рамках одного NUMA-узла, сохраняя быстрый доступ к кешу и памяти.
Известные облачные провайдеры, когда вы выбираете тип виртуальной машины, фактически уже подбирают оптимальное размещение на физическом сервере. По такому же принципу в **Proxmox** есть возможность настроить CPU affinity.
=== Конфигурация VM ===
- Создаём виртуалку с нужной конфигурацией.
- Проверяем архитектуру CPU и NUMA-узлы:
lscpu | grep NUMA
Ожидаемый вывод должен показать NUMA-узлы и ядра:
NUMA node(s): 4
NUMA node0 CPU(s): 0-7,32-39
NUMA node1 CPU(s): 8-15,40-47
NUMA node2 CPU(s): 16-23,48-55
NUMA node3 CPU(s): 24-31,56-63
У нас 4 NUMA-узла, в каждом по 8 ядер и 8 потоков.
Если нужно «привязать» виртуалку к конкретному NUMA-узлу, например к первому, то используем affinity 0-7,32-39.
Файл конфигурации VM в Proxmox должен содержать параметры numa, cpu и affinity:
cores: 16
cpu: cputype=host
affinity: 0-7,32-39
numa: 1
numa0: cpus=0-15,hostnodes=0,memory=114688,policy=bind
Здесь мы создаём виртуалку с 16 ядрами и 112 ГБ памяти.
* ''cpu: host'' - используем тип CPU «host» для максимальной производительности.
* ''affinity: 0-7,32-39'' - привязка VM к ядрам NUMA-узла 0.
* ''numa: 1'' - включаем поддержку NUMA внутри виртуалки.
* ''numa0'' - указываем, что у VM есть NUMA-узел 0 с 16 ядрами, привязанный к NUMA-узлу 0 на хосте, и с памятью 114688 MB, выделенной в том же NUMA-узле.
На стороне VM видим один NUMA-узел с 16 ядрами:
# lscpu | grep NUMA
NUMA node(s): 1
NUMA node0 CPU(s): 0-15
=== Итог ===
Использование CPU affinity для виртуальных машин в продакшене реально важно.
Это позволяет приложению внутри VM не прыгать между разными NUMA-узлами. Оно остаётся в рамках одного NUMA-узла и использует один и тот же L3 кеш. При этом планировщик Linux внутри виртуалки тоже старается держать процессы на тех же CPU-ядрах, чтобы эффективно использовать кеши L1 и L2.