====== 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.