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

Механизм MBA в процессоре и интерфейс resctrl в ядре

Аббревиатура MBA расшифровывается как Memory Bandwidth Allocation, аппаратный блок управления пропускной способностью памяти из набора технологий Intel RDT, который давно перекочевал и в серверные процессоры AMD EPYC. Каждое обращение к памяти помечается идентификатором класса обслуживания, а контроллер замедляет запросы тех классов, чей лимит исчерпан, вставляя короткие паузы прямо в поток запросов. Лимит задается процентом от максимума и работает как верхняя граница: группа, которая не дотягивает до нее, ничего не теряет, а группа, которая рвется выше, упирается в границу мягко и предсказуемо. Ядро выносит управление на файловую систему resctrl, где каталог означает группу задач, файл schemata описывает лимиты, а поддерево mon_data отдает показания счетчиков. Рядом живут соседние технологии того же семейства: распределение кэша L3 под именем cat_l3, мониторинг заполнения кэша и трафика памяти под именем MBM, так что один интерфейс закрывает и ограничение, и наблюдение. На архитектуре ARM64 сходные возможности обещает блок MPAM, поддержка которого встраивается в тот же интерфейс resctrl, хотя на типичных серверах x86 он пока встречается редко. Классы обслуживания при этом общие для всех ресурсов сразу: группа, получившая номер класса, использует его и для кэша L3, и для шины памяти, поэтому два десятка отдельных групп создать не выйдет. Для типичного сценария из трех-четырех групп с разными лимитами запаса хватает с избытком.

Проверка поддержки процессора и монтирование файловой системы resctrl

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

grep -m1 flags /proc/cpuinfo | tr ' ' '\n' | grep -E '^(mba|mbm_total|mbm_local|cat_l3|cdp_l3)$'

Строка mba в выводе означает готовность к ограничениям, а парные флаги mbm_total и mbm_local добавляют мониторинг трафика. Дальше монтируется сам интерфейс, на большинстве узлов это делается вручную один раз:

mount -t resctrl resctrl /sys/fs/resctrl
ls /sys/fs/resctrl/info/MB

Каталог info/MB содержит параметры, от которых зависит вся последующая арифметика:

cat /sys/fs/resctrl/info/MB/bandwidth_gran /sys/fs/resctrl/info/MB/min_bandwidth /sys/fs/resctrl/info/MB/num_closids /sys/fs/resctrl/info/MB/delay_linear

На типичном двухпроцессорном сервере вывод выглядит так:

10
10
16
1

Шаг изменения лимита равен 10 процентам, минимальный лимит тоже 10, всего доступно 16 классов обслуживания вместе с корневой группой, а единица в delay_linear означает линейную связь между процентом и степенью замедления запросов. У другого железа числа отличаются, поэтому их читают до расчетов, а не после. Отмонтировать интерфейс можно обычной командой umount, но перед этим стоит вернуть всем группам 100 процентов, чтобы лимиты снялись без сюрпризов.

Чтение schemata и расчет процентов для групп задач

Корневая группа после монтирования показывает отсутствие каких-либо ограничений:

cat /sys/fs/resctrl/schemata
MB:0=100;1=100

Строка читается просто: ресурс MB, домен 0 и домен 1, то есть два сокета, каждому отдано 100 процентов. На односокетном узле строка будет одна, а при включенной разбивке сокета на поддомены их станет больше, и нумерация не всегда совпадет с номерами сокетов. Проценты отсчитываются от максимальной пропускной способности, заложенной в платформу, а не от скорости, которую задачи показывают прямо сейчас. Пример: сервер с двумя сокетами, на каждом по 8 каналов DDR4-3200 с пропускной способностью 25.6 ГБ/с, суммарно 204.8 ГБ/с на сокет. Лимит 25 процентов дает группе потолок 51.2 ГБ/с, хотя в одиночку та же задача выжимала бы из подсистемы порядка 160-180 ГБ/с. Разница почти в четыре раза, и именно ее почувствует резервное копирование, зато соседние службы перестанут замечать его вообще. Разберем расклад на числах. Базе данных выделим отдельную группу с 60 процентами, резервному копированию достанется 25, сборочной ферме 10, а корневая группа прикроет все прочие службы своим стопроцентным потолком, который им не мешает. При пике сокета 204.8 ГБ/с это означает потолки 122.9, 51.2 и 20.5 ГБ/с соответственно. Сумма доходит до 195 процентов, и это нормально: одновременно до потолков долбятся обычно одна-две группы, а не все сразу. Порядок выбора лимитов для конкретного узла выглядит так:

  1. Замерить трафик значимых задач без ограничений через счетчики MBM и выписать пиковые значения;
  2. Сложить потребности критичных служб, например базы данных и кеша приложений, и оставить им запас в 20-30 процентов от пика;
  3. Остаток бюджета разделить между второстепенными группами вроде резервного копирования и сборки пакетов, округляя вниз до шага bandwidth_gran;
  4. Проверить замером, что лимит держится, и при необходимости сдвинуть проценты ровно на один шаг.

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

Создание контрольной группы и перенос задач под лимит

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

mkdir /sys/fs/resctrl/bulk
echo "MB:0=25;1=25" > /sys/fs/resctrl/bulk/schemata

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

echo 0-7 > /sys/fs/resctrl/bulk/cpus_list

Второй способ идет по идентификаторам процессов и точнее для разовых запусков:

echo 4312 > /sys/fs/resctrl/bulk/tasks

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

for t in /proc/4312/task/*; do echo "${t##*/}" > /sys/fs/resctrl/bulk/tasks; done

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

Измерение реального трафика через счетчики MBM

Лимит без контроля превращается в догадку, поэтому у каждой группы есть собственный каталог мониторинга:

cat /sys/fs/resctrl/bulk/mon_data/mon_L3_00/mbm_local_bytes
sleep 5
cat /sys/fs/resctrl/bulk/mon_data/mon_L3_00/mbm_local_bytes

Пример вывода с загруженного узла:

125729177600
144603545600

Счетчик отдает байты, накопленные с предыдущего чтения файла. Разность составляет 18 874 368 000 байт за 5 секунд, то есть 3 774 873 600 байт в секунду, или около 3.8 ГБ/с. Файл mbm_local_bytes учитывает обращения к локальным контроллерам памяти своего сокета, а mbm_total_bytes добавляет трафик к чужим узлам в многосокетной системе, и расхождение между ними подсказывает, насколько удачно задачи привязаны к памяти по схеме NUMA. Частота опроса ограничена аппаратурой: счетчики обновляются примерно раз в секунду, и более частые чтения дают повторные значения без новизны. Для наблюдения за отдельным процессом внутри группы служат подкаталоги в mon_groups, а соседний файл llc_occupancy показывает, сколько байтов кэша L3 группа держит прямо сейчас. Для регулярного сбора достаточно короткого цикла, который пишет значения в журнал рядом с меткой времени:

while :; do
  printf '%s %s\n' "$(date +%s)" "$(cat /sys/fs/resctrl/bulk/mon_data/mon_L3_00/mbm_local_bytes)" >> /var/log/mbm-bulk.log
  sleep 10
done

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

Программный контроллер mba_sc и лимиты в мегабайтах

Аппаратные проценты грубоваты: при шаге в 10 процентов на сокете с пиком 204.8 ГБ/с ближайшие варианты к желаемым 3.8 ГБ/с уезжают на 20 и на 30.5 ГБ/с. Выход носит имя mba_sc, программная схема, в которой значения schemata задаются не в процентах, а в мегабайтах в секунду:

mount -o remount,mba_sc /sys/fs/resctrl
echo "MB:0=12000;1=12000" > /sys/fs/resctrl/bulk/schemata

Значение 12000 означает цель около 12 ГБ/с на каждый сокет. Грубое правило для перевода простое: желаемую скорость в ГБ/с умножить на 1024, и получится число для schemata. Ядро раз в секунду читает счетчики MBM и двигает аппаратный процент так, чтобы фактическая скорость держалась около цели, поэтому точность оказывается в разы выше шага bandwidth_gran. Переключение меняет смысл всех значений сразу: старые проценты внезапно превращаются в смешные мегабайты, поэтому сразу после перемонтирования стоит переписать schemata каждой группы на осмысленные числа. Обратный переход выполняется перемонтированием без опции и возвратом к процентам. Опцию имеет смысл закрепить в /etc/fstab, иначе после перезагрузки узел вернется к процентной схеме, а скрипты, пишущие мегабайты, начнут сыпать ошибками:

resctrl /sys/fs/resctrl resctrl defaults,mba_sc 0 0

Влияние троттлинга на задержки памяти и частые ошибки настройки

Замедление запросов ощущается приложениями как рост латентности: каждая операция обмена с памятью внутри ограниченной группы получает паузы, и хвосты задержек вытягиваются раньше, чем падает средняя скорость. Поэтому лимит оценивают не только по счетчикам, но и по перцентилям задержки самого приложения или по утилитам вроде mlc, сняв профили до включения ограничений и после них. Около минимума, например при лимите 10 процентов, деградация идет круто, и задача уходит в ступор быстрее, чем обещает арифметика. Наглядный пример с узлом базы данных: до включения лимитов перцентиль 99 простых выборок держался около 12 миллисекунд, при лимите 10 процентов подскочил до 47, а при 30 процентах установился около 18. Если хвосты растут сильнее ожиданий, лимит смягчают на один шаг и повторяют замер, вместо того чтобы отказываться от идеи ограничения. Практика настройки спотыкается о мелочи, которые дешевле проверить заранее. Значения счетчиков, прочитанные чаще раза в секунду, дают мусорные дельты и ломают оценку тренда. Перенос главного процесса без его потоков оставляет половину работы вне лимита, поэтому цикл по каталогу /proc/PID/task обязателен для многопоточных служб. Значение ниже min_bandwidth ядро молча поднимает до минимума, и расчеты уезжают в сторону. Дочерние процессы, запущенные скриптом после привязки по tasks, рождаются в корневой группе, и без привязки по cpus_list они ускользают от контроля. Сумма лимитов сверх 100 процентов при этом законна и безопасна: потолки независимы, и никакая группа не отберет у другой больше, чем позволяет железо. С таким набором правил ограничение памяти становится рядовым инструментом администрирования наравне с nice и ulimit, только действующим там, куда эти механизмы не дотягиваются, на шине к памяти.