Кратко о 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 дает заметный прирост.
Помогла ли вам статья?