Сервер работает годами, и в один момент оперативной памяти перестаёт хватать. Причина может быть в росте нагрузки, появлении новых сервисов или планируемом масштабировании кластера. Главное требование бизнеса - не останавливать сервисы, и здесь горячее добавление памяти становится единственным рабочим вариантом для узлов, которые не могут позволить себе окно обслуживания в полдень. Классический путь - окно обслуживания, выключение, установка планок, включение. Горячее добавление памяти, Memory Hot-Add, позволяет добавить планки без остановки ОС: ядро обнаруживает новое оборудование, поднимает структуры страниц, вводит зоны в баланс, и приложения начинают пользоваться новой памятью сразу. Механизм работает на платформах с поддержкой ACPI и NUMA, требует согласованности BIOS, ядра и пользовательского пространства, и на правильно настроенном узле добавляет терабайты памяти за минуты без единого перезапуска сервиса.
Разберём путь от ACPI до работающей страницы, роль NUMA и movable zone, работу с udev и systemd, мониторинг процесса, перебалансировку зон, борьбу с фрагментацией, типичные подводные камни и практический порядок внедрения.
От уведомления ACPI до структур страниц
Процесс стартует от уведомления прошивки. ACPI таблицы описывают контейнеры памяти, их адреса, размеры и флаги возможности горячего подключения. Таблица PNP0C80 декларирует устройства памяти, метод _HID идентифицирует их, а _CRS возвращает текущие ресурсы, включая флаги hot-pluggable. Метод _OST сообщает прошивке о статусе операции, и именно через этот обратный канал BIOS узнаёт, что ОС успешно приняла память. Дополнительно метод _EJ0 обрабатывает извлечение памяти, и пара _EJ0/_OST формирует полный цикл жизненного управления. На платформах с несколькими сокетами таблицы SRAT и SLIT описывают топологию расстояний между узлами, и корректность этих таблиц критична для того, чтобы ядро создало зоны в правильном NUMA-узле. Когда физическая планка вставляется в слот, контроллер памяти сигнализирует платформе, платформа генерирует SCI, ядро получает событие через ACPI, и драйвер memory_hotplug начинает работу. Последовательность фиксирована: проверка совместимости, резервирование диапазона физических адресов, выделение memmap для новых страниц, инициализация зон, регистрация в NUMA-узле, и finally - перевод страниц в состояние online через sysfs. На каждом этапе возможны ошибки, и ядро пишет подробные сообщения в dmesg, которые становятся основным источником диагностики.
Ключевой момент: новые страницы не становятся доступными мгновенно. Они проходят через состояние offline, где ядро выделило структуры, но страницы ещё не в зоне buddy, затем через online, где они входят в аллокатор и становятся доступны для выделений. Перевод управляется записью в файл:
echo online > /sys/devices/system/memory/memoryXXX/state
и именно этот шаг администратор или автоматизация выполняет после проверки здоровья нового диапазона. Параметр memory_hotplug_default_online в ядре позволяет автоматически онлайнить блоки, но в продакшене его лучше отключить и контролировать процесс явно.
NUMA-узлы и movable zone
На NUMA-системах каждая добавленная память принадлежит какому-то узлу. Прошивка через таблицу SRAT сообщает, к какому узлу относится новый диапазон, и ядро создаёт зоны внутри этого узла. Если узел новый - ядро расширяет нума-топологию, если существующий - добавляет зоны к имеющемуся. Важный нюанс: для того чтобы горячее отключение памяти работало в будущем, новые страницы должны попасть в ZONE_MOVABLE. Параметр загрузки movable_node или настройка kernelcore и movablecore заставляют ядро размещать подвижные страницы в новых диапазонах, и это же требование действует и для горячего добавления. Без этого память не сможет быть извлечена через Memory Hot-Remove, и узел будет вынужден перезагружаться для освобождения слотов.
Проверка принадлежности к movable zone делается через pagetypeinfo. Дополнительно sysctl vm.page_referenced и vm.page_idle позволяют отслеживать активность страниц в новых зонах, и утилита page-types из пакета linux-tools показывает детальное распределение по типам миграции для каждой зоны нового узла.
cat /proc/pagetypeinfo | grep -A20 'Node.*memoryXXX'
и по полю Migratable видно, что страницы действительно попали в подвижную зону. Если новые страницы лежат в Unmovable или Reclaimable - это признак того, что параметры загрузки не сработали, и горячее удаление не сработает.
Автоматизация через udev и systemd
Ручное переключение state в online подходит для единичных операций, но в продакшене процесс автоматизирован. Правило udev реагирует на появление нового memory-блока и запускает скрипт перевода в online:
ACTION=="add", SUBSYSTEM=="memory", RUN+="/usr/local/bin/memory-hotadd-online.sh %k"
Скрипт проверяет, что блок ещё не онлайн, валидирует диапазон через чтение phys_index и size, и пишет online в state. Systemd-юнит можно привязать к тому же событию через systemd-udev, что даёт журналирование, зависимость от.target и перезапуск при ошибке. Для контейнерных сред, где память добавляется на уровне гипервизора, тот же механизм работает внутри гостя: virtio-mem или ACPI-хотплаг подаёт событие, и гостевой udev поднимает память.
Мониторинг добавления памяти в логах выглядит так. Команда dmesg -T | grep -iE 'memory|hotplug|online' показывает хронологию событий с временными метками, а grep -r . /sys/devices/system/memory/memory*/state даёт мгновенный срез состояния всех блоков. В системах мониторинга полезно экспортировать метрику memory_blocks_online_total и memory_blocks_offline_total, чтобы алертинг срабатывал, если какой-то блок застрял в offline более заданного времени. Параллельно метрики vmstat pgactivate и pgdeactivate показывают, как быстро новые страницы втягиваются в рабочие наборы, и счётчик pgmigrate_success отражает успехи компакции в новых зонах. Настройка алертинга на застревание блоков в offline с таймаутом в 30 секунд - хорошая практика для продакшн-кластеров.
Перебалансировка зон и борьба с фрагментацией после добавления
После добавления памяти ядро не перераспределяет существующие выделения автоматически. Новые страницы входят в buddy-аллокатор как свободные блоки максимального порядка, и новые выделения начинают использовать их. Существующие процессы продолжают сидеть в старой памяти, и только новые malloc или страницы кэша забирают новые блоки. Это правильное поведение: насильственное перемещение страниц потребовало бы остановки процессов и копирования гигабайт данных.
Со временем может возникнуть фрагментация: старые зоны забиты долгоживущими выделениями, новые зоны вольны, но аллокатор не умеет переносить страницы между зонами без явного запроса. Для решения есть два пути. Первый, проактивная компакция: запуск компакции в новых зонах перед крупными выделениями огромных страниц через запись в /proc/sys/vm/compact_memory или sysctl vm.compaction_proactiveness. Второй, ручная миграция: перезапуск долгоживущих сервисов или использование MGLRU для естественного вытеснения старых страниц. На практике фрагментация становится проблемой только при интенсивном использовании huge pages, и для обычных рабочих нагрузок естественный оборот страниц достаточно. Параметр vm.compaction_proactiveness в диапазоне 0-100 управляет агрессивностью фоновой компакции, и значение 20-30 обычно достаточно.
Ограничения и подводные камни
Первое ограничение: поддержка прошивкой. Не каждый сервер умеет подавать корректные ACPI-события для горячего добавления, и на некоторых платформах таблицы SRAT не описывают новые диапазоны, что ломает NUMA-привязку. Второе: размер блока памяти. Минимальная гранулярность добавления определяется прошивкой и может быть 128 МБ, 256 МБ или 1 ГБ, и не всегда совпадает с размером планки. Третье: лицензирование ОС. Некоторые редакции Windows Server ограничивают горячее добавление, в Linux таких ограничений нет, но дистрибутивные ядра должны быть собраны с CONFIG_MEMORY_HOTPLUG и CONFIG_MEMORY_HOTPLUG_DEFAULT_ONLINE.
Четвёртая ловушка: виртуализация. В KVM/QEMU горячее добавление памяти гостю работает через virtio-mem или ACPI-хотплаг, но требует настройки память-бэкенда на хосте и поддержки в гостевом ядре. На хосте память для гостя должна быть доступна как резерв, и balloon-драйвер не должен мешать добавлению. Пятая: производительность первого доступа. Первые обращения к новым страницам идут с промахами по TLB и кэшу, и приложения, критичные к задержке первого касания, могут получить всплески. Разогрев новых страниц чтением или memset перед отдачей под нагрузку снимает этот эффект. Шестая: взаимодействие с CMA. Если на узле настроен CMA для DMA-буферов, новые страницы могут не попасть в CMA-резерв автоматически, и размер CMA может потребовать пересчёта через параметр загрузки cma=.
Взаимодействие с CXL-памятью и будущие расширения
На современных серверах с поддержкой CXL-памяти горячее добавление получает новое измерение: память добавляется не только через слоты DIMM, но и через расширители на шине CXL, и те же механизмы ядра работают и для них. Устройство CXL Type 3 регистрируется как NUMA-узел без процессоров, и при подключении горячее подключение памяти через CXL проходит тот же путь ACPI -> memory_hotplug -> online. Отличие в том, что CXL-память имеет более высокую задержку и ограниченную полосу, поэтому размещение страниц в ней требует учёта уровней памяти. Ядро начиная с версии 6.1 поддерживает memory tiering, и новые CXL-диапазоны автоматически получают более низкий уровень, чем локальная DRAM. Это означает, что горячее добавление CXL-памяти не требует изменений в автоматизации онлайна, но требует понимания того, что новые страницы попадут в медленный уровень и будут использоваться в первую очередь для холодных данных. Для приложений с явным управлением памятью это даёт возможность предсказуемо расширять объём доступной памяти, не меняя код, а для систем без явного управления - полагаться на демоушн, который со временем переносит редко используемые страницы в новый CXL-диапазон.
Практический порядок добавления памяти в продакшене
Добавление памяти в строй укладывается в шесть шагов:
- проверить поддержку платформы: ACPI таблицы, SRAT, возможность хотплага в документации вендора; загрузить ядро с CONFIG_MEMORY_HOTPLUG, CONFIG_MOVABLE_NODE, и параметрами kernelcore и movablecore под плановую конфигурацию;
- настроить udev-правило и systemd-юнит для автоматического перевода блоков в online, добавить проверку здоровья диапазона перед онлайном;
- добавить физические планки, зафиксировать появление блоков в /sys/devices/system/memory и пройти перевод в online, проверить dmesg на ошибки;
- проверить NUMA-принадлежность новых зон через numactl -H и pagetypeinfo на предмет Movable, убедиться, что новые страницы попали в подвижную зону;
- запустить эталонную нагрузку и сравнить метрики до и после, зафиксировать отсутствие регрессий, проверить, что новые страницы реально используются аллокатором;
- зафиксировать процедуру в runbookе эксплуатации, включить мониторинг состояния блоков памяти в общую систему алертинга, и провести учение: добавление памяти без документации превращается в ручной квест на следующем расширении.
Итог у Memory Hot-Add рабочий и зрелый: на поддерживаемом железе с настроенным ядром и автоматизации добавление терабайт памяти проходит за минуты без остановки сервисов. Ключ к успеху - в подготовительной работе: правильные параметры загрузки, movable zone для новых страниц, автоматизация онлайна и мониторинг логов. Без этого механизм работает, но превращается в ручной квест на каждое добавление, и именно ручная работа приводит к ошибкам в продакшене. На современных серверах с поддержкой CXL-памяти горячее добавление получает новое измерение: память добавляется не только через слоты DIMM, но и через расширители на шине CXL, и те же механизмы ядра работают и для них, что делает тему ещё более актуальной.