QEMU-KVM и NUMA-архитектура

Кратко о NUMA-архитектуре.

Представьте себе довольно типичный двух-процессорный сервер. Можете представить четырех-процессорный, это не принципиально, главное что бы соккетов было больше одного, иначе эта статья для вас не актуальна)

Так вот, у каждого процессора(не ядра а именно процессора), есть встроенный контроллер памяти(либо он совсем близко) и подключенный через него банк памяти к которому у этого процессора есть максимально быстрый доступ. Это собственно и есть NUMA-нода №0.

У других процессоров(при наличии таковых) так же есть встроенный контроллер и «локальная» память и это такие же NUMA-ноды №N в рамках одного сервера.
Проблема NUMA в том, что доступ процессора из ноды 0 к памяти ноды 1 в два раза медленнее чем к своей, локальной памяти.

Так же, обращаясь к памяти из другой ноды, процессор выполняет в два раза больше тактов чем обращаясь к своей памяти.
Кроме этого, общий кеш процессора 2-го и 3-го уровней, становится менее эффективен если процесс выполняется на процессорах нескольких нод.

В общем, в идеале, каждый из процессов должен выполняться в рамках одной какой то ноды и не «прыгать» между ними.

Вот сервер с двумя NUMA-нодами:

numactl --hardware

available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 8 9 10 11           # нода 0, 4 - физ. прцессора + 4 HT
node 0 size: 24564 MB                      # память ноды 0
node 0 free: 22487 MB
node 1 cpus: 4 5 6 7 12 13 14 15      # аналогично нода 1
node 1 size: 20480 MB
node 1 free: 19666 MB
node distances:
node 0 1
0:    10 21                                           # лэтэнси до своей(10) и до другой ноды(21)
1:    21 10


QEMU и NUMA. Как это работает по умолчанию

numastat -c qemu-kvm

Per-node process memory usage (in MBs)
PID                        Node 0      Node 1     Total
--------------- ------ ------ -----
13622 (qemu-kvm)  2121        1810       3931
14477 (qemu-kvm)  475          391         866
--------------- ------ ------ -----
Total                      2596          2201             4797

pid=13622 —  ВМ 1CPU\4Gb. использована вся память(внутри ВМ).
pid=14477 — ВМ 1CPU\4Gb. без нагрузки

Здесь, у виртуалок всего по одному ядру но при этом память размазана по двум нодам.
Дело в том, что vCPU QEMU это процесс ОС и он периодически выполняется на разных физических ядрах(тут как карта ляжет). Таким образом, ОС распределяет всю вычислительную нагрузку по всем ядрам системы.
Память же, выделяется на той ноде на процессоре которой в данный момент выполняется процесс, в итоге получается вот такая печальная картина.
И это еще маленькие виртуалки на совершенно пустой ноде!

статистика для всех процессов в системе:

numastat -pm

Per-node process memory usage (in MBs)
PID Node 0 Node 1 Total
---------------------- --------------- --------------- ---------------
...
1453 (multipathd) 3.02 1.15 4.18
1560 (monitor) 0.32 0.56 0.88
1561 (ovs-l3d) 0.54 0.81 1.34
1584 (monitor) 0.78 0.07 0.85
1585 (ovsdb-server) 2.34 0.62 2.96
1595 (monitor) 0.80 0.07 0.88
1596 (ovs-vswitchd) 65.79 2.94 68.73
13622 (qemu-kvm) 2118.69 1810.05 3928.74
...
14477 (qemu-kvm) 475.31 390.83 866.14
..
---------------------- --------------- --------------- ---------------
Total 2689.75 3276.04 5965.80

Что можно сделать?

Можно привязывать vCPU к физическим ядрам на одной ноде, что приведет к выделению памяти на этой же ноде. Но это требует сложной логики и некого внешнего механизма(скрипта) который будет выполнять привязку средствами cgroups или утилиты numactl.
Есть более простой путь, привязывать виртуалки к нодам а не к ядрам.
Привязать процесс к определенной ноде можно примерно так:

numactl --membind=0 qemu-kvm -name vm1 -m 4096 -smp 4 -drive file=/store/volumes/8f/8f0eec98-ab93-4b10-8ffd-e870a53f9e38,if=virtio,media=disk  -vnc :5 -monitor stdio

В результате получаем виртуалку чей процесс а так же его треды и все что она может породить помещенную в одну ноду:

numastat -c qemu-kvm

Per-node process memory usage (in MBs) for PID 9170 (qemu-kvm)

Node 0      Node 1     Total
------ ------ -----
Huge          0             0              0
Heap         27            0             27
Stack         0              0              0
Private      4102        5             4107    # внутри ВМ исользованно 4 Gb и почти все в рамках одной ноды
-------  ------ ------ -----
Total      4129           5              4134

Есть еще более простой путь, использовать numad.
Это демон, который появился в RHEL 6.3 для решения обсуждаемых здесь проблем и особенно эффективен в контексте виртуализации.l

numad старается максимально эффективно распределять жирные процессы по нодам. В низах использует /cgroup/cpuset/….

Вот результат:

numastat -c qemu-kvm

Per-node process memory usage (in MBs)

PID                  Node 0    Node 1    Total
---------------  ------ ------ -----
4006 (qemu-kvm)     686          5        692    # ВМ 1CPU\4Gb. без нагрузки
4158 (qemu-kvm)     687          5        693    # ВМ 1CPU\4Gb. без нагрузки
4469 (qemu-kvm)    7295         5         7300    # ВМ 2CPU\7Gb. исп. вся память.
5193 (qemu-kvm)       0         1336      1336    # ВМ 3CPU\11Gb. почти без нагрузки.
---------------  ------ ------ -----
Total              8668         1352       10020

Конечно, с очень большими виртуалками это особо не поможет, но зато кучу тех что по меньше разбалансирует и тем самым немного повысит производительность и снизит накладные расходы на обслуживание ВМ.

Про KSM

Вот тут все написано тут.

Выдержка:

If KSM is in use on a NUMA system, change the value of the /sys/kernel/mm/ksm/merge_nodes parameter to 0 to avoid merging pages across NUMA nodes. Otherwise, KSM increases remote memor accesses as it merges pages across nodes. Furthermore, kernel memory accounting statistics can eventually contradict each other after large amounts of cross-node merging. As such, numad can become confused about the correct amounts and locations of available memory, after the KSM daemon merges many memory pages. KSM is beneficial only if you are overcommitting the memory on your system. If your system has sufficient free memory, you may achieve higher performance by turning off and disabling the KSM daemon.

Тесты

Для теста использовалась ВМ m1.xlarge(3CPU\11Gb) с CentOS 6.

На неё была установлена БД Redis а нагрузка генерировалась с помощью redis-benchmark со следующими параметрами:

redis-benchmark -t set,get -n 10000000 -r 100000000 -d 200

Данный тест приводит к 100% утилизации всех трех ядер и памяти ВМ!

 

Результаты теста без numad:

====== SET ======
10000000 requests completed in 167.83 seconds
59582.68 requests per second

====== GET ======
10000000 requests completed in 136.58 seconds
73216.09 requests per second

 

Результаты теста с numad:

====== SET ======
10000000 requests completed in 146.33 seconds
68339.16 requests per second
====== GET ======
10000000 requests completed in 130.54 seconds
76607.22 requests per second

 

Результаты теста с numad и с выключенным hyper threading(чисто ради интереса):

====== SET ======
10000000 requests completed in 208.99 seconds
47849.64 requests per second

====== GET ======
10000000 requests completed in 140.59 seconds
71130.33 requests per second

Все таки включенный hyper threading дает заметный прирост.

  • Алексей

    Спасибо, Александр. Познавательно.