Нюма

Оптимизация приложений под архитектуру NUMA

Нюма

Optimizing Applications for NUMA [Eng., PDF 225KB]

Аннотация

NUMA (Non-Uniform Memory Access) – это архитектура совместного доступа к памяти в многопроцессорных системах, в которой время доступа к участку памяти определяется его расположением относительно процессора. Как и в случае с большинством других свойств процессорных систем, невнимание к особенностям архитектуры может привести к ухудшению работы памяти.

К счастью, существует возможность нивелировать проблемы в работе, связанные с характерными особенностями NUMA-архитектур и даже использовать некоторые её преимущества для улучшения работы приложений.

Это касается привязки потоков к процессорам, распределения памяти с использованием неявных методов, а также применения системных API для привязки ресурсов и перемещения страниц между узлами вычислительной системы.

Введение

Возможно, лучший способ понять NUMA – это сравнить ее с похожей архитектурой UMA (Uniform Memory Access). В UMA-системах все процессоры имеют доступ к совместно используемой памяти через общую шину (или другой вид соединения), как показано на рисунке:

Особенность архитектуры UMA состоит в том, что все процессоры используют единый канал доступа к памяти, а следовательно, время доступа одинаково для всех процессоров. Отметим также, что время доступа не зависит и от расположения информации в памяти.

В архитектуре совместно используемой памяти NUMA для каждого процессора существует отдельный локальный модуль памяти, к которому он может обращаться напрямую, что дает значительное преимущество в работе. В то же время, он может обращаться к модулю памяти любого другого процессора при помощи общей информационной сети (см. рис. ниже):

В данном случае время доступа к памяти варьируется в зависимости от местонахождения данных, к которым осуществляется доступ.

Если данные находятся в локальном модуле памяти, доступ осуществляется быстро, в противном случае, для доступа к удаленному модулю требуется большее время.

Преимуществом архитектуры NUMA как иерархической структуры совместно используемой памяти является возможность сокращения среднего времени доступа в результате рационального использования быстрой локальной памяти.

Современные многопроцессорные системы совмещают в себе вышеописанные основные системы, как показано на следующем рисунке:

В этой сложной иерархической системе, процессоры сгруппированы в соответствии с их физическим местоположением на том или ином узле центрального процессора.

Процессоры в узле используют общий доступ к модулям памяти как в архитектуре совместного использования памяти UMA.

В то же время, они получают доступ к удаленным узлам посредством общего соединения, однако работа в данном случае идет медленнее, как в NUMA.

Рекомендации

Два ключевых момента в управлении работой в рамках архитектуры совместного использования памяти NUMA – это привязка потоков к процессорам и правильное размещение данных в памяти.

Привязка потоков к процессорам

Современные сложные операционные системы распределяют потоки между ядрами процессора с помощью планировщика.

Планировщик учитывает состояние системы и различные плановые задания (например, «равновесное распределение между процессорами» или «создание потоков на нескольких процессорах и перевод остальных процессоров в режим сна»), затем в соответствии с этим распределяет потоки между физическими ядрами.

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

Перемещение потоков с одного ядра на другое создает для NUMA проблему, связанную со способом распределения потока и его расположения в локальной памяти.

Так, вначале поток может размещаться в памяти на узле 1, так как он идет на ядре узла 1.

Однако когда в дальнейшем поток переносится на ядро узла 2, сохраненные ранее данные автоматически оказываются в удаленном доступе, и время доступа к памяти значительно возрастает.

Ввод привязки потоков к процессорам. Привязка потоков к процессорам основана на долговременном закреплении точки исполнения протока/процесса на определенных ресурсах системы, несмотря на доступность других устройств.

Посредством использования системных API и изменения некоторых структур ОС (например, маски соответствия – affinity mask), определенное ядро или система ядер могут быть ассоциированы с определенным потоком. Затем планировщик отмечает эту привязку в плане распределения на период времени существования потока.

Например, поток может быть настроен на работу только на ядрах с 0 по 3, которые принадлежат узлу 0 центрального четырехядерного процессора. Планировщик при этом будет выбирать среди ядер с 0 по 3, даже не предполагая перемещения потока на другой узел.

Реализация привязки потоков к процессорам гарантирует, что модули памяти будут оставаться локальными для потоков, которым они необходимы. Однако нельзя не указать на некоторые недостатки данного метода.

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

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

Программистам следует провести тщательную оценку того, подойдет ли использование привязки потоков к процессорам для конкретного приложения и конкретной системы.

Наконец, следует отметить, что соответствующие API, предлагаемые некоторыми системами, помимо прямых команд планировщику позволяют также задавать функции приоритетов и давать планировщикам «рекомендации».

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

Размещение данных с помощью неявных методов

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

Однако некоторые операционные системы, напротив, ожидают первого доступа к памяти прежде чем выделить конкретный участок.

Для того чтобы понять плюсы такого формата, рассмотрим многопотоковое приложение с начальной последовательностью действий, включающей распределение памяти с помощью главного контрольного потока, создание различных рабочих потоков и длительную последовательную обработку данных. Хотя может показаться логичным размещать страницы виртуальной памяти локально по отношению к потоку, на самом деле, более эффективным является их размещение локально по отношению к рабочим потокам, которые получают доступ к данным. Соответственно, операционная система фиксирует запрос, отправляемый при первом доступе, и формирует распределение в соответствии с расположением узлов, с которых пришел запрос.

Оба этих подхода (локальность по отношению к первому доступу и локальность по отношению к запросу) показывают, насколько важно для программиста, занимающегося приложениями, знание особенностей NUMA в структуре программы.

Если распределение страниц осуществляется по принципу первого доступа, программист может использовать это, создав изначально продуманную последовательность доступа к данным, которая определит оптимальный для операционной системы способ распределения памяти.

Если распределение ресурсов зависит от первого запроса, программисту следует убедиться, что распределение памяти осуществляется на основании потока, который впоследствии работает с данными, а не начального или контрольного потока.

Многочисленные потоки, обращающиеся к одной базе, лучше всего совмещать на одном узле, чтобы данные, оптимально размещенные для одного потока, были также удобны для использования другими потоками.

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

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

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

Размещение данных с помощью прямых директив

Другим подходом к размещению данных в основанных на NUMA системах является создание системных API, которые четко определяют размещение виртуальных страниц памяти. Примером таких интерфейсов служит библиотека libnuma в Linux.

С помощью этих API программист может соотносить диапазон адресов виртуальной памяти с конкретными узлами, закрепляя расположение определенного пула данных вне зависимости от того, в каком потоке он находится и какой поток первым получает к нему доступ.

Это может быть полезно, например, в схемах, где есть поток, отвечающий за распределение памяти и работающий в соответствии с поведением рабочих потоков.

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

Недостатком этой схемы, конечно же, является необходимость для программиста приложения заниматься распределением памяти и размещением данных.

Неправильное размещение данных может привести к тому, что работа будет идти значительно хуже, чем если бы она осуществлялась в соответствии со стандартными алгоритмами.

Управление памятью в явном виде также предполагает осуществление мелкомодульного контроля привязки задачи к процессору в ходе работы приложения.

Еще одной возможностью, доступной для программистов, работающих с API распределения памяти в NUMA-системах, является перемещение памяти между узлами. В целом, перемещение страниц памяти с одного узла на другой – это дорогая операция, которой стараются избегать.

Однако если приложение одновременно требует длительного времени исполнения и большого количества ресурсов памяти, перемещение данных с целью создания удобной для NUMA конфигурации может быть вполне оправданным.

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

Руководство по применению

Ключевым фактором в определении того, улучшатся ли показатели работы приложения при введении архитектуры NUMA, является размещение данных.

Чем чаще данные могут быть эффективно размещены в памяти, которая является локальной по отношению к процессору, который к ней обращается, тем сильнее улучшится показатели производительности за счет введения архитектуры NUMA.

Благодаря созданию собственной локальной памяти для каждого узла можно избежать проблемы ограниченности пропускной способности и возникновения борьбы за ресурсы, связанной с наличием единой пропускной шины. Теоритически, производительность может улучшаться пропорционально увеличению количества узлов за счет полностью параллельного доступа к памяти.

Напротив, чем больше данных, не являющихся локальными по отношению к узлу, который к ним обращается, тем сильнее работа памяти пострадает от введения NUMA архитектуры.

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

В целом, затраты на доступ к памяти увеличиваются по мере роста удаленности от процессора.

Дополнительные ресурсы

Parallel Programming Community

Drepper, Ulrich. “What Every Programmer Should Know About Memory”. November 2007.

Intel® 64 and IA-32 Architectures Optimization Reference Manual. See Section 8.8 on “Affinities and Managing Shared Platform Resources”. March 2009.

Lameter, Christoph. “Local and Remote Memory: Memory in a Linux/NUMA System”. June 2006.

Источник: https://software.intel.com/ru-ru/articles/optimizing-applications-for-numa

QEMU-KVM и 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 дает заметный прирост.

Источник: https://ivirt-it.ru/qemu-kvm-and-numa/

Поделиться:
Нет комментариев

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

    Ваш e-mail не будет опубликован. Все поля обязательны для заполнения.

    ×
    Рекомендуем посмотреть