У памяти в сервере незавидная судьба: она как погрузочный двор гипермаркета, к которому со всех сторон одновременно едут фуры. Ядра, видеокарты, сетевые карты и накопители претендуют на одну и ту же шину, и когда какая-то виртуальная машина начинает жрать полосу, соседи по узлу замечают это ростом задержек, а не цифрами в отчётах. Хуже всего, что без специальных счётчиков виновника не видно: загрузка процессора у него может быть скромной, а память он при этом выкачивает по полной. Чудес тут нет, всё уже встроено в сами процессоры Intel и AMD, а Linux выставляет показания наружу через интерфейс resctrl.
Механизм называется Memory Bandwidth Monitoring, у Intel он часть набора RDT, у AMD это Platform Quality of Service. Ядро умеет показывать, сколько байт каждая группа задач прочла и записала из памяти за период наблюдения, причём отдельно видно трафик в дальнюю и ближнюю память. Отсюда прямая дорога к практическим вещам: поиск шумного соседа, честный биллинг в облаке, проверка NUMA-локальности и настройка ограничений полосы.
Дальше разбор устройства счётчиков, работы с ними через resctrl, тонкой настройки событий и типичных ловушек, которые поджидают в продакшене.
Как процессоры считают расход памяти через RMID
Основная идея проста и элегантна. Каждому логическому ядру назначается идентификатор RMID, записываемый в регистр IA32_PQR_ASSOC, а аппаратные счётчики в регистре IA32_QM_CTR суммируют трафик памяти, помеченный текущим RMID. Возможности процессора перечисляются через CPUID, лист 0xF, там же видно ширину счётчика и число доступных RMID. Ядро с включённой конфигурацией CONFIG_X86_CPU_RESCTRL периодически снимает показания и складывает дельты, поэтому переполнение узкого счётчика на пользователе не сказывается.
Событий ровно два, и они отвечают на разные вопросы. mbm_total_bytes считает весь трафик в память независимо от того, в каком NUMA-узле она лежит, а mbm_local_bytes только трафик в память локального домена. Разница между ними это буквально карта удалённых чтений: если total заметно больше local, приложение топает за данными через межпроцессорную шину, и тут уже стоит смотреть раскладку памяти, а не ограничивать полосу. В /proc/cpuinfo поддержка видна по флагам cqm_mbm_total и cqm_mbm_local, на AMD системах набор интерфейса тот же самый.
Число RMID ограничено, и это число задаёт потолок числа одновременно отслеживаемых групп. Файл num_rmids в каталоге info/L3_MON показывает конкретное значение для процессора, и у разных поколений оно заметно отличается.
Мониторинг полосы через файловую систему resctrl
Точка входа выглядит почти обманчиво просто. resctrl монтируется одной командой, после чего вся механика доступна как обычные файлы и каталоги:
mount -t resctrl resctrl /sys/fs/resctrl
cat /sys/fs/resctrl/info/L3_MON/mon_features
llc_occupancy
mbm_total_bytes
mbm_local_bytes
cat /sys/fs/resctrl/info/L3_MON/num_rmids
mkdir /sys/fs/resctrl/p1
echo 5678 > /sys/fs/resctrl/p1/tasks
cat /sys/fs/resctrl/p1/mon_data/mon_L3_00/mbm_total_bytes
779247936
Верхнеуровневые каталоги называются CTRL_MON группами, внутри них можно создавать вложенные mon_groups для тонких подмножеств задач, а показания лежат в mon_data с разделением по доменам: mon_L3_00, mon_L3_01 и так далее. Задачи привязываются записью номера процесса в файл tasks, процессоры записью маски в файл cpus. Приём с переносом текущей оболочки примечателен: команда echo $$ в tasks группы отдаёт её идентификатор, и всё, что запустится из этой оболочки, будет измеряться с рождения, включая короткоживущие утилиты, которые иначе легко упустить.
Показание в файле выражено байтами за период между обновлениями ядра, то есть фактически это мгновенная полоса в байтах в секунду. Для наглядного мониторинга с клавиатуры есть и готовая утилита pqos из пакета intel-cmt-cat: pqos -I показывает возможности процессора, pqos mon рисует полосу по ядрам и группам в реальном времени, и для быстрой диагностики на одной машине этого хватает.
Настройка событий BMEC под конкретный вопрос
Стандартные два события можно пересобрать под задачу, если процессор поддерживает Bandwidth Monitoring Event Configuration. Файлы mbm_total_bytes_config и mbm_local_bytes_config в info/L3_MON хранят битовую маску типов транзакций: чтения локальных и удалённых страниц, нетемпоральные записи по локальной и удалённой памяти, чтения медленной памяти и жертвенные записи грязных строк. По умолчанию total собран из маски 0x7f, то есть всех типов сразу, а local из маски 0x15, куда входят локальные чтения и локальные нетемпоральные записи.
Примеры дают почувствовать гибкость. Запись 0x33 в mbm_total_bytes_config оставляет в событии только чтения обычной памяти, а маска 0x30 в mbm_local_bytes_config собирает чтения медленной памяти, что идеально для диагностики промахов в CXL-устройства:
echo "0=0x33" > /sys/fs/resctrl/info/L3_MON/mbm_total_bytes_config
echo "0=0x30;1=0x30" > /sys/fs/resctrl/info/L3_MON/mbm_local_bytes_config
Есть нюанс, о котором забывают и потом недоумевают: смена конфигурации очищает счётчики обоих событий во всём домене, первый запрос вернёт слово Unavailable, и лишь следующие чтения дадут число. Конфигурация доменоспецифична и затрагивает все ядра домена, так что перестраивать её на живом продакшене стоит осознанно, а не между делом.
Режим mbm_event и распределение счётчиков
В классическом режиме ядро считает, что у каждой группы событий есть свой аппаратный счётчик. На старых процессорах это соответствовало действительности, но число групп легко превосходит число RMID, а на части платформ железо ещё и переиспользует счётчики между обновлениями, отчего показания скачут или возвращают Unavailable. Спасение называется Assignable Bandwidth Monitoring Counters, а в интерфейсе resctrl это режим mbm_event.
Суть режима в ручном назначении счётчиков парам RMID плюс событие. Проверка поддержки и включение делаются записью в один файл:
cat /sys/fs/resctrl/info/L3_MON/mbm_assign_mode
echo "mbm_event" > /sys/fs/resctrl/info/L3_MON/mbm_assign_mode
cat /sys/fs/resctrl/info/L3_MON/num_mb_m_cntrs
0=32;1=32
cat /sys/fs/resctrl/mbm_L3_assignments
mbm_total_bytes:0=e;1=e
mbm_local_bytes:0=e;1=e
Файл mbm_L3_assignments управляет назначением: символ e значит счётчик назначен, подчёркивание значит снят, запись вида mbm_total_bytes:0=_ освобождает счётчик в домене 0, а звёздочка вместо номера домена разворачивает действие на все домены сразу. Файл mbm_assign_on_mkdir включает автоназначение при создании новой группы, что удобно, хотя при нехватке счётчиков часть событий останется без назначений, и это видно в отчётах. На AMD платформах с поддержкой ABMC режим включён по умолчанию, и документация ядра прямо рекомендует его использовать, чтобы счётчики не сбрасывались между чтениями. Бонусом в режиме mbm_event доступны именованные фильтры транзакций в каталоге event_configs: вместо битовых масок пишутся читаемые слова вроде local_reads или dirty_victim_writes_all.
От измерения к ограничению полосы с MBA
Измерение само по себе полдела. Вторая половина называется Memory Bandwidth Allocation: ядро через схему schemata раздаёт группам проценты полосы, а железо душит тех, кто вылезает за лимит. Минимальный процент и шаг регулировки видны в info/MB в файлах min_bandwidth и bandwidth_gran, промежуточные значения округляются вверх до ближайшего шага железа.
Рабочий цикл контроля полосы для новой службы умещается в шесть шагов:
- замерить фактическую полосу службы через выделенную группу resctrl на типовой нагрузке;
- сравнить показания total и local, убедиться, что проблема не в удалённых чтениях;
- завести для службы ресурсную группу с лимитом в schemata, например MB:0=50;
- наблюдать за полосой и задержками службы и соседей после включения лимита;
- поднять или опустить процент с учётом шага bandwidth_gran;
- зафиксировать раздачу лимитов в конфигурации узла, чтобы пережила перезагрузку.
Тут поджидает пара неочевидных вещей. Процент в MBA относится к домену целиком, поэтому десять процентов у одного потока и десять процентов у четырёх потоков означают совершенно разную фактическую полосу. Кроме этого, на части процессоров троттлинг работает на уровне физического ядра, и два потока одного ядра с разными лимитами получат общий минимум, что видно в файле thread_throttle_mode. Если хочется задавать лимит в абсолютных величинах, монтирование с опцией mba_MBps переводит schemata в мегабайты в секунду, например MB:0=1024;1=500, а внутри ядра заработает программный контур обратной связи, который сам подбирает проценты по показаниям MBM, удерживая фактическую полосу ниже заданной. У AMD отдельно есть механизм SMBA для медленной памяти, прежде всего CXL-устройств, там лимитируется именно трафик в медленные домены.
Точность показаний и подводные камни
Счётчикам тоже свойственно ошибаться, и лучше знать об этом заранее. Для серверных Skylake и Broadwell существовали эрраты SKX99 и BDF102, при которых регистр IA32_QM_CTR завышал или занижал системную полосу для части значений RMID. Ядро знает об этом и применяет к показаниям корректирующие коэффициенты из таблицы по числу ядер, где разброс от 0.969650 до 1.454545, так что на исправленных ядрах чинить ничего не нужно, но сравнивать числа с внешними счётчиками стоит с поправкой на эту особенность.
Вторая ловушка связана с разницей уровней измерения. MBM работает на уровне пакета, а троттлинг MBA на части моделей применяется к ядрам, поэтому после включения лимита число в mon_data может не измениться: лимит упирается в другой ресурс. Пример из документации ядра хорошо показывает масштаб: на процессоре с 24 ядрами суммарная внешняя полоса L2 составляет 240 гигабайт в секунду, а внешняя полоса L3 всего 100, и двадцать потоков с лимитом в половину полосы легко упираются именно в потолок L3, хотя формально лимит не исчерпан. Диагностика таких расхождений сводится к сопоставлению числа потоков, доменов и уровней, откуда растут показания.
Для скриптов, которые читают и создают группы, существует ещё одна тонкость: операции из нескольких записей обязаны быть атомарными, иначе два параллельных скрипта поделят одни и те же биты кэша. Способ стандартный, flock на каталог /sys/fs/resctrl, общая блокировка для чтений и эксклюзивная для изменений. Причину любого неудавшегося действия подскажет файл info/last_cmd_status, в котором лежит человекочитаемое описание последней ошибки, например разъяснение про некорректную маску.
Практические сценарии для кластеров и облаков
Ценность MBM раскрывается на ансамблях машин. В облаке счётчики дают основу для честного биллинга по фактическому потреблению памяти, а не по выделенным гигабайтам. В кластерах баз данных монитор помогает поймать шумного соседа до того, как пользователи заметят деградацию: рост total при спокойной загрузке процессора является характерным признаком. Для NUMA-чувствительных служб соотношение local к total работает как компас: падение доли локального трафика означает, что планировщик утащил приложение от его данных, и пора смотреть на привязки и топологию.
Отдельная строка применений связана с планированием. Перед тем как делить кэш и полосу между службами, имеет смысл просто походить мониторингом по группам пару дней и собрать профили фактического потребления. Цифры редко совпадают с ожиданиями: одна, казалось бы, скромная служба может потреблять больше половины полосы, а тяжёлая на вид работа наоборот сидеть в кэше и почти не ходить в память. На свежих системах в resctrl появляется и каталог телеметрии PERF_PKG_MON с пакетными событиями, расширяющими картину за пределы L3.
В итоге связка MBM и MBA превращает полосу памяти из стихии в управляемый ресурс. Измеряешь, ограничиваешь, проверяешь эффект, и спор о том, кто забивает шину, заканчивается за минуты, а не после недели чтений графиков. На серверах, где память давно стала главным бутылочным горлышком, такой уровень контроля уже не роскошь, а гигиена.