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

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

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

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

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

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

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

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

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

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

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

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

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

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

numastat -pm_x000D_
_x000D_
Per-node process memory usage (in MBs)_x000D_
PID Node 0 Node 1 Total_x000D_
---------------------- --------------- --------------- ---------------_x000D_
..._x000D_
1453 (multipathd) 3.02 1.15 4.18_x000D_
1560 (monitor) 0.32 0.56 0.88_x000D_
1561 (ovs-l3d) 0.54 0.81 1.34_x000D_
1584 (monitor) 0.78 0.07 0.85_x000D_
1585 (ovsdb-server) 2.34 0.62 2.96_x000D_
1595 (monitor) 0.80 0.07 0.88_x000D_
1596 (ovs-vswitchd) 65.79 2.94 68.73_x000D_
13622 (qemu-kvm) 2118.69 1810.05 3928.74_x000D_
..._x000D_
14477 (qemu-kvm) 475.31 390.83 866.14_x000D_
.._x000D_
---------------------- --------------- --------------- ---------------_x000D_
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_x000D_
_x000D_
Per-node process memory usage (in MBs) for PID 9170 (qemu-kvm)_x000D_
_x000D_
Node 0      Node 1     Total_x000D_
------ ------ -----_x000D_
Huge          0             0              0_x000D_
Heap         27            0             27_x000D_
Stack         0              0              0_x000D_
Private      4102        5             4107    # внутри ВМ исользованно 4 Gb и почти все в рамках одной ноды_x000D_
-------  ------ ------ -----_x000D_
Total      4129           5              4134

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

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

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

numastat -c qemu-kvm_x000D_
_x000D_
Per-node process memory usage (in MBs)_x000D_
_x000D_
PID                  Node 0    Node 1    Total_x000D_
---------------  ------ ------ -----_x000D_
4006 (qemu-kvm)     686          5        692    # ВМ 1CPU4Gb. без нагрузки_x000D_
4158 (qemu-kvm)     687          5        693    # ВМ 1CPU4Gb. без нагрузки_x000D_
4469 (qemu-kvm)    7295         5         7300    # ВМ 2CPU7Gb. исп. вся память._x000D_
5193 (qemu-kvm)       0         1336      1336    # ВМ 3CPU11Gb. почти без нагрузки._x000D_
---------------  ------ ------ -----_x000D_
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(3CPU11Gb) с 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 дает заметный прирост.

Добавить комментарий