MAATRIX / Блог / Transparent Huge Pages удвоили задержки базы, и никто не понимал почему

Transparent Huge Pages удвоили задержки базы, и никто не понимал почему

MAATRIX

В конце августа 2026 года мы разбирали кейс: команда перевезла базу на новый сервер помощнее, а p99-задержки запросов не упали, а выросли почти вдвое. Железо новее, ядер больше — а тормозит сильнее старого. Три дня искали виноватого не там, пока не дошли до параметра ядра, включённого по умолчанию и почти никогда не упоминаемого в чек-листах переезда. Ниже — разбор: что видели в метриках, какие гипотезы отбросили и почему, и что в итоге поменяли.

Что сломалось: жалоба и первые цифры

Сервис — монолит на Django поверх PostgreSQL 15, около 40 ГБ данных, смешанная нагрузка: OLTP вперемешку с фоновыми агрегациями каждые 10 минут. После переезда с сервера на 64 ГБ RAM (2 сокета, NUMA) на сервер с 128 ГБ RAM (тоже 2 сокета, ядра быстрее) ждали улучшения. Получили обратное.

Цифры из мониторинга (Grafana поверх pg_stat_statements и node_exporter): p50 по основным SELECT вырос с ~4 мс до ~5 мс — терпимо, а p99 — с ~40 мс до ~85 мс, уже заметно клиентам. При этом CPU utilization по top держался в районе 35-45% — процессор не выглядел узким местом, iowait почти нулевой (диск NVMe не при делах), а вот system time (%sy в vmstat) вырос заметно сильнее user time, особенно при большом числе одновременных соединений.

Разработчики сразу заподозрили, что при переезде забыли применить тюнинг PostgreSQL под новое железо. Гипотеза логичная, но, как выяснилось, неверная по сути.

Гипотеза 1: забыли тюнинг PostgreSQL — отбросили

Сверили postgresql.conf со старого и нового сервера построчно через diff по обоим файлам. Разница нашлась: shared_buffers и effective_cache_size пропорционально увеличили под новый объём памяти (32GB и 96GB соответственно), max_connections не трогали — стандартный набор для NVMe-сервера, ничего подозрительного. Откатили shared_buffers до прежних 16 ГБ на всякий случай — задержки не изменились. Гипотезу закрыли: дело не в конфиге, а в чём-то ниже уровня самой базы. Если вы не уверены, что параметры тюнинга вообще осмысленны, а не просто скопированы со старого сервера, см. отдельный разбор тюнинга PostgreSQL и частых ошибок — там разобраны типичные промахи именно в этих настройках.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Гипотеза 2: диск или сеть — отбросили быстро

Вторая версия — деградация ввода-вывода: новый NVMe хуже держит нагрузку, либо сеть между приложением и базой даёт джиттер. fio на тестовом разделе показывал даже более высокие IOPS и меньшую latency, чем на старом сервере, iostat -x 1 во время пиковой нагрузки не показывал очередей (avgqu-sz около нуля, %util не выше 15%) — диск не виноват. Сеть локальная, в пределах одного дата-центра, ping держал ту же долю миллисекунды, что и раньше, mtr не показывал скачков — не похоже на историю из разбора скачков задержки из-за соседского бэкапа: там чужой трафик забивал канал скачками в конкретные минуты, а здесь задержка росла плавно вместе с параллелизмом запросов. Сеть и диск сняли с подозрения.

Гипотеза 3: NUMA-дисбаланс — частично права, но не вся картина

Новый сервер — двухсокетный, и если процесс PostgreSQL и его память "размазаны" неудачно между NUMA-узлами, обращения к памяти с другого узла обходятся дороже локальных. Подобный эффект мы уже разбирали в статье про NUMA и когда два CPU медленнее одного. Проверили через numactl --hardware и numastat -m — и не зря: заметная часть памяти PostgreSQL была выделена на node1, а сам процесс (numastat -p $(pgrep -f postgres | head -1)) исполнялся на ядрах node0. Привязали постмастер через numactl --membind=0 --cpunodebind=0 — задержки чуть просели, процентов на 5-7 от общей просадки, не больше. Проблема была явно не только в NUMA — дисбаланс оказался реальным, но второстепенным фактором.

Реальная причина: Transparent Huge Pages и синхронная компакция памяти

Пока крутили NUMA, обратили внимание на аномалию в /proc/vmstat, которую раньше не проверяли: счётчики compact_stall и thp_fault_fallback росли почти линейно с нагрузкой на базу.

grep -E 'compact_stall|compact_fail|thp_fault_fallback' /proc/vmstat
cat /sys/kernel/mm/transparent_hugepage/enabled   # [always] madvise never
cat /sys/kernel/mm/transparent_hugepage/defrag    # [always] defer defer+madvise madvise never

Оба параметра стояли на always — дефолт большинства дистрибутивов после установки "как есть". compact_stall — счётчик того, сколько раз ядро синхронно, блокирующе дефрагментировало физическую память, чтобы собрать непрерывный блок в 2 МБ под huge page прямо в момент, когда процесс запрашивал новую память. На старом сервере с меньшей фрагментацией к моменту наблюдения эффект почти не проявлялся. На новом — с большим объёмом памяти, активным shared_buffers и накопленным uptime под нагрузкой — память фрагментировалась сильнее, и ядро чаще запускало синхронную компакцию именно тогда, когда PostgreSQL просил новую страницу под соединение, сортировку в work_mem или расширение внутренних буферов. Эффект известен по работе с PostgreSQL, Redis, MongoDB и любым процессом, интенсивно и нелинейно выделяющим память — рекомендация отключать defrag для баз встречается в документации СУБД не просто так; но конкретные значения compact_stall, которые видели мы, — цифры нашего случая, у вас они будут другими.

Итоговая цепочка: больше памяти и активнее её использование → сильнее фрагментация → чаще синхронная компакция под defrag=always → пауза на каждом выделении памяти → под высоким параллелизмом паузы накладываются друг на друга и дают удвоение p99. NUMA-дисбаланс существовал параллельно, но второстепенный — совпадение по времени с основной проблемой сбило нас с толку на добрые полдня. Это же объясняет низкий CPU utilization: время компакции — это system time на том же ядре, где шёл запрос, общая загрузка по top остаётся умеренной, а хвост распределения (p99) всё равно улетает вверх.

Что изменили после инцидента

Отключили defrag для THP, оставив сами huge pages в режиме madvise (не never) — PostgreSQL умеет явно запрашивать huge pages через параметр huge_pages, и полный отказ от THP лишает этой возможности:

echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/defrag

Для персистентности после перезагрузки — systemd-юнит /etc/systemd/system/disable-thp-defrag.service:

[Unit]
Description=Отключение агрессивного THP defrag для СУБД
DefaultDependencies=no
Before=basic.target

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled'
ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/defrag'

[Install]
WantedBy=basic.target

Дальше — systemctl daemon-reload && systemctl enable --now disable-thp-defrag.service.

Добавили в мониторинг счётчики compact_stall и thp_fault_fallback отдельной панелью с алертом на рост скорости накопления (на производную, не на абсолютное значение — оно естественно растёт с uptime). В чек-лист переезда добавили проверку THP наравне со swappiness и ulimit — раньше там была только память, диск и сеть. NUMA-биндинг агрессивно трогать не стали: 5-7% выигрыша не стоят усложнения эксплуатации, раз основная причина устранена.

Ориентир по типичным рекомендациям для серверных СУБД (не абсолютная истина для любой конфигурации):

ПараметрДефолт дистрибутиваРекомендация для СУБД под нагрузкой
transparent_hugepage/enabledalwaysmadvise
transparent_hugepage/defragalwaysnever (или defer)
vm.swappiness601-10
NUMA-биндинг процессане заданопционально, при явном дисбалансе

Про общую настройку производительности через sysctl и swappiness — отдельная статья, настройка swap и производительности AlmaLinux 9 с нуля, она закрывает соседний участок той же темы.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Нужно ли полностью отключать THP (never), а не переводить в madvise?

Не обязательно. madvise оставляет возможность явно запрашивать huge pages там, где уместно (параметр huge_pages в PostgreSQL 15+), убирая только автоматическую раздачу без спроса. Полное отключение тоже рабочий вариант, но лишает гибкости на будущее.

Почему на старом сервере с 64 ГБ проблема была не видна?

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

Как убедиться, что виноват именно THP, прежде чем менять настройки в проде?

Смотрите на compact_stall в динамике: рост, коррелирующий по времени со всплесками p99, — сильный сигнал. Можно также переключить defrag в never на тестовом окружении с похожей нагрузкой и сравнить хвостовые задержки до и после.

Это специфично для PostgreSQL?

Механизм общий для любого процесса, интенсивно и нелинейно выделяющего память — Redis, MongoDB, MySQL/MariaDB, JVM с большими кучами. Специфика PostgreSQL только в параметре huge_pages для управления huge pages на уровне самой СУБД.

Стоит ли сразу привязывать процесс базы к одному NUMA-узлу?

Только если numastat явно показывает дисбаланс и даёт измеримый эффект в вашем тесте — у нас это было 5-7% сверх основного фикса. Начинать стоит с THP: обычно эффект куда больше при куда меньшей сложности.

Обсудить статью, задать вопрос или начать новую тему

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

Перейти в сообщество →