NUMA awareness: почему сервер теряет производительность
Современный многопроцессорный сервер — это не одна большая машина, а несколько вычислительных узлов, каждый со своей локальной памятью процессора. Пока приложение работает в пределах одного узла, обращение к памяти идёт по короткому пути, и задержка предсказуема. Как только поток начинает читать данные из чужого узла, запрос уходит по межпроцессорной шине: задержка растёт в разы, а пропускная способность делится между всеми, кто обратился туда же.
Коварство в том, что операционная система по умолчанию распределяет память «по справедливости»: узел страницы определяется первым обращением, а планировщик вправе перенести процесс на другое ядро. Получается деградация под нагрузкой, которую почти не видно на тестовом стенде — на десятке соединений всё гладко, а на пике время отклика растёт ступенями. Ищут причину в коде приложения, в диске или в сети, хотя виновник — топология памяти, о которой заранее никто не договаривался.
NUMA awareness — это согласованная настройка планировщика, средств виртуализации и самого приложения, при которой вычисления держатся рядом со своими данными. Потоки закрепляются за узлами, память выделяется на нужном узле, а межпроцессорный трафик становится осознанным исключением, а не постоянным фоном. Отдельная часть работы — миграция страниц: автоматическое выравнивание иногда помогает, а иногда бесконечно гоняет одни и те же страницы между узлами, съедая именно тот ресурс, который пытались сэкономить.
Дальше разбираем, как мы снимаем реальную топологию конкретного сервера и что меняем в конфигурации, чтобы отклик перестал зависеть от того, на какой узел попал поток.
Эффект от настройки размещения памяти на многопроцессорном сервере
Эффект от настройки виден не по среднему времени отклика, а по его разбросу и по поведению сервиса на пике. Пока процессы и их страницы памяти распределены по всем узлам, часть запросов уходит на удалённый контроллер памяти — среднее остаётся приемлемым, поэтому проблему долго не замечают, а хвост распределения выдаёт её сразу. Ниже — три среза, по которым мы измеряем результат.
Разброс времени отклика
Пока приложение получает память с любого узла, задержка запроса зависит от того, куда его положил планировщик, — отсюда рваный хвост распределения и 95-й перцентиль, который в разы выше медианы. Привязка процессов к узлам и политика выделения локальных страниц сжимают разброс: запросы, которые обслуживались в 3–5 раз дольше остальных, укладываются в узкий коридор, а соседние замеры перестают расходиться.
Стабильность и равномерность потоков под нагрузкой
Потоки раскладываются по ядрам внутри узла, и каждый работает со своей локальной памятью — конкуренция за межсокетную шину перестаёт быть узким местом. Пропускная способность растёт линейнее: при удвоении числа одновременных задач время ответа увеличивается заметно слабее, а не кратно. Для владельца сервиса это выглядит как «сервис перестал проседать в часы пик», хотя код приложения не менялся.
Устранённый класс отказов
Самый дорогой эффект — уходит класс отказов, при котором процесс не получает локальную память: узел тратит ресурс на вытеснение страниц, и запросы ждут не вычислений, а доступа к памяти. Доля таких отказов падает до нуля, а топологию и политики мы фиксируем в конфигурации — рекомендации производителя платформы (Intel) по размещению памяти по узлам переносим в шаблоны, чтобы предсказуемость отклика сохранялась при росте нагрузки и добавлении новых служб.
Подходы к NUMA awareness: сравнение режимов и ограничений
На многопроцессорном сервере обращение к памяти своего сокета дешевле, чем к памяти соседнего, и разница проявляется на хвостовых задержках, а не на среднем отклике. Кто делит страницы между узлами — ядро с автоматическим размещением или вы вручную — определяется политикой. Реальных вариантов четыре, от полного отказа от настройки до жёсткой привязки.
Сравниваем по критериям, которые обычно и решают: предсказуемость отклика, устойчивость к росту числа потоков, стоимость сопровождения и поведение при смене оборудования.
| Вариант | Когда подходит | Ограничения |
|---|---|---|
| Без настройки, автоматическое размещение | Стенды и сервисы без требований к хвосту задержек | Балансировщик переносит страницы под нагрузкой; отклик плавает тем сильнее, чем больше потоков |
| Мягкое предпочтение узла | Большой общий рабочий набор, потоки распределены по всем сокетам | Локальность не гарантирована: часть обращений всё равно уходит на удалённый узел |
| Привязка процессов к узлам | СУБД, брокеры, кэши с ровным откликом | Нужны замеры топологии и сопровождение; при перекосе потоков часть ядер простаивает |
| Жёсткая привязка процесса и памяти | Выделенный сервер под один сервис, чувствительный к задержкам | Нет запасного пути: узел заполнен — размещение отклоняется, перенос на другое железо ломает конфигурацию |
Порядок работ — от слабого режима к строгому: сначала мягкая политика и замеры, затем привязка процессов, и только потом жёсткая. Если начать с последней, получается обратная задача: узлы недогружены, свободная память есть, но недоступна.
Порядок внедрения: от схемы топологии до передачи регламента
Внедрение идёт поэтапно: каждое изменение подтверждается замером и остаётся обратимым. Начинаем с аудита топологии — без карты узлов, ядер и привязки устройств ввода-вывода любая правка превращается в угадывание. Дальше переносим выводы в схему размещения, настраиваем, проверяем под нагрузкой и передаём регламент эксплуатации.
-
Аудит топологии. Собираем карту узлов и ядер, связей между ними, привязки сетевых карт и дисковых контроллеров. Фиксируем базовые замеры: задержку доступа к локальной и удалённой памяти, распределение потоков по узлам.
-
Схема размещения. Определяем, какие сервисы привязываем к узлам жёстко, какие получают только предпочтение, а какие оставляем свободными. Отдельно проговариваем, где нужна изоляция — иначе мягкая привязка незаметно станет жёсткой.
-
Настройка. Переносим схему в конфигурацию: правила привязки процессов и обработки прерываний, распределение памяти, параметры планировщика. Каждое изменение — отдельным шагом с точкой отката.
-
Нагрузочная проверка. Гоняем профиль, близкий к рабочему: смешанные запросы, пиковые всплески, длительный фон. Смотрим не только на среднюю пропускную способность, но и на хвостовые задержки.
-
Разбор отклонений. Сверяемся с базовыми замерами и объясняем каждое расхождение: где сработала локальность, где привязка сузила ресурсы. Спорные правила убираем, а не оставляем «на всякий случай».
-
Регламент эксплуатации. Передаём карту узлов, действующие правила привязки, список того, что нельзя менять без перезамера, и порядок проверки после обновления ядра или замены железа.
Что заказчик получает на руки: схемы, профили, регламенты
После настройки NUMA awareness работа заканчивается не отчётом, а набором артефактов, с которыми ваша команда живёт дальше — без нас. Всё складывается в репозиторий конфигураций и в документацию по серверу, структура и названия согласуются с вашими инженерами на этапе сдачи.
- Карта топологии. Схема размещения узлов памяти и ядер: видно, какие ядра ходят за памятью в соседний узел и где теряется пропускная способность. По ней планируется посадка новых сервисов без повторных замеров.
- Профили привязки. Готовые наборы для базы данных, кэша и балансировщика: какие процессы закреплены за какими ядрами и при каком числе потоков. Профиль применяется шаблоном, а не собирается заново.
- Политики доступа. Кто вправе менять привязку, какие правки требуют прогона на стенде и что запрещено трогать на боевом сервере. Закрывает отказы вида «поправили ядра руками».
- Документация по серверу. Паспорт узла: состояние до и после настройки, реперные замеры по памяти и задержкам, связь конфигурации с составом железа и режимами системы.
- Регламент обслуживания. Порядок плановых работ: как сохранить и восстановить профили, что проверять после замены железа, порядок приёмки нового сервера в тот же профиль.
- Скрипты проверки. Автотест, что привязка применилась и держится после перезапуска сервисов, с отчётом на одну страницу для дежурного инженера.
Кейс: как убрали деградацию отклика на серверах с двумя процессорами
Клиент — сервис, где на каждом сервере стоит по два процессора, а база работает под управлением PostgreSQL. На пиковой нагрузке отдельные запросы стали отвечать в разы дольше остальных: процессы ходили за данными в память соседнего процессора, и очередь на обслуживание росла быстрее, чем сама нагрузка.
Что показали замеры
Доля обращений к удалённой памяти доходила до 34%, и задержки концентрировались именно на этих запросах: разброс времени отклика между самыми быстрыми и самыми медленными операциями отличался почти на порядок. При выходе на пик сервис не замедлялся равномерно, а начинал сбрасывать часть запросов по превышению времени ожидания — то есть деградация отклика выглядела как обрыв, а не как плавное снижение.
Что изменили и что получили
Мы привязали процессы базы и набор соединений к локальным узлам памяти, задали политику размещения страниц и отключили автоматическую миграцию, из-за которой страницы переезжали между процессорами в самый неподходящий момент. Дополнительно развели по разным узлам сервисы, конкурировавшие за одну и ту же память.
Доля удалённых обращений упала до 3-4%, разброс времени отклика сократился примерно в шесть раз, а пиковая пропускная способность выросла почти вдвое — без замены оборудования. Деградация отклика на пике, ради которой запускался проект, больше не воспроизводится ни на стенде, ни в рабочей среде.
Что делать, если сервер уже настроен вручную и без документации?
Унаследованная конфигурация почти всегда многослойная: часть привязок задана параметрами загрузки ядра, часть — в юнитах systemd, часть — командами, которые когда-то выполнили вручную и не зафиксировали. Первый шаг — ничего не менять, а снять текущую схему размещения как она есть: какие процессы закреплены за узлами, как распределены прерывания и что реально видит ядро, а не что записано в старых заметках.
Срез делается до любых правок и сохраняется как эталон для сравнения: сведения о топологии и текущих весах узлов, список привязок по процессам, настройки групп процессоров, параметры загрузки ядра. Рядом — метрики локальности: доля обращений к «чужому» узлу и время выполнения задач, которые в локальную память не попадают. Без этого эталона потом невозможно доказать, что стало лучше, а не просто иначе.
Отдельно разбираем, что именно компенсировала ручная настройка. Часто она лечила симптом — перекос прерываний на один узел или неудачный выбор узла планировщиком — и при снятии привязок дефект возвращается. Поэтому проверка перед изменениями включает сверку задуманного с работающим: правило может не применяться вовсе, если процесс перезапускается из другого юнита или контейнер игнорирует унаследованную маску.
Обратная совместимость держится на порядке работ: меняем по одному слою, начиная с наименее связанного с нагрузкой, и сохраняем готовый откат к снятому срезу. Первым идёт пилотный узел, где сравниваются те же метрики локальности и задержки под контрольной нагрузкой. Только после устойчивого результата правило переносится на остальные машины.
В итоге конфигурация из набора ручных догадок превращается в описанную и воспроизводимую: схема размещения задокументирована, каждое изменение проходит через сравнение с эталоном, а возврат к прежнему состоянию возможен в любой момент.
Технические сомнения: совместимость, риски, ограничения
Привязка процессов к локальным узлам памяти работает и на физическом сервере, и внутри виртуальной машины: гипервизор публикует гостю топологию, а ядро гостя передаёт её дальше — в менеджеры ресурсов и контейнеры. Граница применимости простая: если VMware или другой гипервизор маскирует топологию либо сам размазывает нагрузку по всем ядрам, выигрыш внутри гостя будет заметно меньше, чем на «голом железе».
Поэтому мы начинаем с проверки: видит ли система несколько узлов и совпадает ли их нумерация с реальными сокетами — при несовпадении любая привязка уводит процессы не туда, куда планировалось.
Влияние на уже работающие сервисы зависит от способа настройки. Смена политики распределения памяти на уровне ядра применяется к новым процессам и не трогает текущие, а привязка через менеджер ресурсов требует аккуратного перезапуска — под неё мы выделяем окно и обкатываем на одном узле кластера, сверяя задержки до и после. Каждое изменение оформляется обратимо: профиль отключается одной правкой конфигурации, и сервис возвращается к прежнему режиму без пересборки окружения.
Совместимо ли это с виртуализацией и контейнерами?
Да, при условии, что топология не скрыта. В контейнерах привязка задаётся на уровне менеджера ресурсов и наследуется процессами; в виртуалках — только если гостю видны отдельные узлы. Если топология скрыта, метод не применяем и говорим об этом прямо, а не изображаем настройку.
Придётся ли останавливать работающие сервисы?
Не всегда. Изменения политики на уровне ядра подхватывают новые процессы, и сервис продолжает работать, постепенно переходя на новую схему. Перезапуск нужен там, где управление памятью идёт через менеджер ресурсов — такие работы планируем в окно обслуживания.
Как откатываются изменения, если станет хуже?
Конфигурация хранится в системе управления версиями, откат — возврат предыдущего профиля и перезапуск затронутых сервисов. Перед выводом на весь кластер мы оставляем эталонные замеры, чтобы сравнить поведение после отката и убедиться, что вернулись ровно к исходному состоянию.
Обсудим задачу: пришлите схему серверов и профиль нагрузки
Чтобы понять, что даст NUMA awareness именно на ваших серверах, не нужен доступ к рабочему контуру — достаточно исходных данных о железе и профиле нагрузки. Чем полнее картина, тем меньше допущений в расчёте и тем реальнее сроки работ. Вот что полезно приложить к описанию задачи.
- вывод
lscpuиnumactl --hardwareс каждого сервера — топология узлов, раскладка памяти по каналам, расстояния между узлами; - схема размещения сервисов: что живёт в контейнерах, что в виртуальных машинах, где включена миграция и как настроены лимиты;
- профиль нагрузки — какие процессы чувствительны к задержке, как нагрузка меняется по часам, где пики и как долго держатся;
- текущие привязки и параметры ядра: unit-файлы с CPUAffinity, политики NUMA-балансировки, настройки планировщика;
- метрики под нагрузкой: промахи локальной памяти, доля обращений к удалённому узлу, время отклика сервиса на пике;
- описание инцидентов: в какой момент отклик растёт, что уже пробовали и что дало эффект;
- окна обслуживания и требования к откату — можно ли перезагружать узлы, есть ли горячий резерв.
Присылайте то, что есть: полный набор не обязателен, недостающее мы перечислим отдельно и подскажем, как снять. После разбора схемы и профиля нагрузки мы собираем модель распределения процессов и памяти, отмечаем, где привязка даст выигрыш, а где упрётся в лимиты, и возвращаемся с оценкой и планом — этапы, ожидаемый эффект по задержкам и утилизации, сроки работ. Опишите задачу, и обсудим, с чего начать.







