Память - это ресурс, который никогда не прощает небрежности. Особенно когда речь идёт о серверах с десятками гигабайт RAM, где база данных ворочает сотнями тысяч транзакций в секунду. Казалось бы, поставил больше памяти - и живи спокойно. Но именно здесь кроется одна из самых коварных ловушек производительности: при стандартной конфигурации Linux ядро управляет памятью через страницы размером 4 КБ, и на сервере с 128 ГБ RAM это означает более 33 миллионов таких страниц. Каждое обращение к памяти требует трансляции виртуального адреса в физический - и процессор вынужден держать в TLB-кэше миллионы записей, которые туда попросту не помещаются.
Hugepages - это не просто "большие страницы". Это принципиально иной подход к тому, как приложение взаимодействует с физической памятью, и понять этот подход - значит получить в руки один из самых мощных инструментов оптимизации серверных систем.
Как TLB-промахи незаметно разрушают производительность нагруженных систем
Translation Lookaside Buffer - небольшой, но очень быстрый кэш внутри CPU, который хранит последние трансляции виртуальных адресов в физические. Когда процесс обращается к памяти, сначала проверяется TLB. Если нужная запись там есть - трансляция происходит мгновенно. Если нет - случается TLB miss - и процессор вынужден идти в таблицу страниц, а это десятки наносекунд задержки на каждый промах.
Теперь умножьте это на миллионы обращений в секунду в PostgreSQL или Oracle. Результат - ощутимое снижение пропускной способности, которое сложно поймать обычным профилировщиком, но которое копится и бьёт по latency. Администраторы порой неделями гоняют EXPLAIN ANALYZE, меняют индексы и перепишут запросы - а виновник сидит на уровне железа и тихо ворует миллисекунды.
Один TLB-слот для страницы размером 2 МБ покрывает ровно в 512 раз больше памяти, чем слот для страницы 4 КБ. На практике это означает, что TLB-кэш процессора начинает работать принципиально эффективнее: одни и те же физические ресурсы кэша накрывают несравнимо большие рабочие наборы данных. Именно это и даёт прирост производительности - не магия, а чистая математика адресации.
Linux предоставляет два механизма работы с hugepages: статические hugepages через hugetlbfs и динамические через Transparent Huge Pages (THP). Они устроены по-разному, работают по-разному, и отношение к ним у разных баз данных - прямо противоположное. Понять разницу между этими двумя механизмами - значит перестать применять их наугад и начать делать осознанный выбор.
Статические hugepages - надёжность ценой предсказуемости
Статические hugepages выделяются ядром при загрузке системы или через sysctl и резервируются в физической памяти заранее. Ключевое свойство - они никогда не свопируются на диск. Для базы данных это означает жёсткую гарантию: SGA Oracle или shared_buffers PostgreSQL останутся в RAM при любой нагрузке на систему. Никаких сюрпризов в три часа ночи, никаких всплесков latency из-за того, что страница ушла в swap и вернулась обратно.
Прежде чем что-либо настраивать, нужно понять текущее состояние системы. Самая информативная точка входа - это /proc/meminfo, который отражает реальную картину использования памяти прямо сейчас:
grep -i huge /proc/meminfo
Типичный вывод на правильно настроенной системе выглядит так:
HugePages_Total: 4096
HugePages_Free: 512
HugePages_Rsvd: 450
HugePages_Surp: 0
Hugepagesize: 2048 kB
Здесь каждая строка говорит о своём. HugePages_Total - весь выделенный пул. HugePages_Free - страницы, которые зарезервированы, но ещё не "потрогал" ни один процесс. HugePages_Rsvd - страницы, которые процесс зарезервировал через mmap, но физически ещё не использует. Если HugePages_Free стремится к нулю при работающей базе - пул исчерпан, и новые процессы начнут падать обратно на обычные страницы 4 КБ, причём совершенно тихо, без каких-либо предупреждений в логах.
Чтобы рассчитать необходимое количество страниц для PostgreSQL, нужно взять пиковое потребление памяти процессом postmaster во время реальной рабочей нагрузки, а не в момент простоя - иначе пул окажется занижен именно тогда, когда он нужнее всего:
# Найти PID постмастера
ps aux | grep postmaster | head -1
# Получить пиковое потребление памяти в КБ
grep VmPeak /proc/<PID>/status
# Рассчитать количество страниц (размер hugepage = 2048 КБ)
echo "8589934 / 2048" | bc
# Результат: ~4194 страниц, добавляем 10% запаса
После расчёта задать количество hugepages можно на лету, без перезагрузки - ядро попытается выделить их из доступной физической памяти прямо сейчас:
sysctl -w vm.nr_hugepages=4614
Это изменение временное и исчезнет после перезагрузки. Чтобы оно сохранялось постоянно, его вносят в конфигурационный файл sysctl, который подхватывается при каждом старте системы:
echo 'vm.nr_hugepages = 4614' >> /etc/sysctl.d/30-hugepages.conf
sysctl --system
После того как пул создан, PostgreSQL нужно явно направить на его использование. В postgresql.conf за это отвечает один параметр, но его значение имеет принципиальное значение для продакшна:
huge_pages = on # on, off или try
huge_page_size = 2MB # или 1GB при поддержке процессора
Параметр try означает "используй, если доступны, иначе работай без них". Звучит гибко, но на нагруженном сервере это потенциальная ловушка: если hugepages внезапно стали недоступны после обновления конфигурации, база молча запустится без них, и никто не заметит деградацию производительности. Значение on жёстче - база откажется стартовать, если hugepages недоступны, что в данном случае является правильным поведением: лучше явная ошибка, чем скрытая проблема.
Прочь от THP - почему базы данных требуют его отключения
Transparent Huge Pages появился в ядре Linux как попытка дать преимущества hugepages без ручной настройки. Ядро само решает, когда и где использовать страницы по 2 МБ, объединяя обычные страницы в hugepages через фоновый демон khugepaged. Для десктопных систем и простых серверных приложений это вполне работает. Для баз данных - нет, и вот почему.
Прежде всего стоит убедиться, в каком режиме THP сейчас работает на системе - это занимает секунду и сразу даёт понимание ситуации:
cat /sys/kernel/mm/transparent_hugepage/enabled
# Вывод: always [madvise] never
# Активный режим обозначен квадратными скобками
THP в режиме always означает, что ядро агрессивно пытается использовать hugepages для любой анонимной памяти, включая память базы данных. Чтобы собрать непрерывный физический блок 2 МБ, ядру нужно провести дефрагментацию - и этот процесс может блокировать выделение памяти приложением на сотни миллисекунд, а иногда и на секунды. Для базы данных, где каждая миллисекунда latency на счету, это неприемлемо.
PostgreSQL управляет shared_buffers через пул страниц по 8 КБ с непредсказуемым паттерном доступа: страница 1, потом страница 50000, потом страница 23. Ядро пытается объединить соседние 8-килобайтовые страницы в hugepages, тратит ресурсы на дефрагментацию - а выигрыш оказывается минимальным, потому что THP работает лучше всего именно с последовательным, однородным доступом к памяти, которого у базы данных нет.
Oracle прямо документирует отключение THP как обязательное требование перед установкой. MongoDB, Apache Cassandra и большинство других баз данных придерживаются той же позиции. Отключить THP немедленно, без перезагрузки:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
Проблема этого способа - изменение не переживёт перезагрузку. Чтобы THP оставался отключённым постоянно, создают отдельный systemd-сервис, который выполняется при каждом старте системы раньше, чем запустится сама база данных:
# /etc/systemd/system/disable-thp.service
[Unit]
Description=Disable Transparent Huge Pages
After=sysinit.target local-fs.target
Before=mongod.service postgresql.service
[Service]
Type=oneshot
ExecStart=/bin/sh -c "echo never > /sys/kernel/mm/transparent_hugepage/enabled"
ExecStart=/bin/sh -c "echo never > /sys/kernel/mm/transparent_hugepage/defrag"
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
После создания файла сервис нужно зарегистрировать и запустить:
systemctl enable --now disable-thp.service
Проверить, что дефрагментация THP не вызывает задержки прямо сейчас, поможет статистика ядра. Это полезно сделать ещё до отключения THP, чтобы понять масштаб проблемы и убедиться после - что она ушла:
grep -i thp /proc/vmstat | grep -E 'stall|fail|collapse'
Высокое значение compact_stall в этом выводе - прямой признак того, что THP активно причиняет задержки прямо сейчас, пока вы читаете эту строку.
Hugepages для JVM - когда большие страницы действительно работают
Java-приложения находятся в принципиально иной ситуации, и именно здесь рекомендации расходятся с советами для баз данных. JVM выделяет heap одним большим непрерывным куском памяти, который никуда не перемещается на протяжении всей жизни приложения. Это идеальный кандидат для hugepages: большой, однородный, долгоживущий блок, доступ к которому носит относительно предсказуемый характер.
OpenJDK поддерживает два механизма. Первый - hugetlbfs через флаг -XX:+UseHugeTLBFS, который монтирует heap через специальную файловую систему hugepages напрямую. Второй - THP через флаг -XX:+UseTransparentHugePages, который вызывает madvise() для области heap, сигнализируя ядру о готовности использовать большие страницы. Прежде чем выбирать между ними, стоит убедиться, что конкретная JVM поддерживает эти флаги:
java -XX:+PrintFlagsFinal 2>&1 | grep -E 'HugePages|LargePage'
Для Cassandra, Spark и других JVM-приложений с heap от 8 ГБ и выше оптимальная стратегия - режим madvise на уровне системы в паре с флагом -XX:+AlwaysPreTouch. Настройка уровня системы переводит THP в избирательный режим: по умолчанию выключен для всего, но включается для тех процессов, которые явно этого попросят через madvise():
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
После настройки системного уровня JVM-приложение запускают с соответствующими флагами. Флаг -XX:+AlwaysPreTouch здесь критически важен - он заставляет JVM физически "прощупать" каждую страницу heap при старте, инициируя реальное выделение памяти до начала работы:
java -XX:+UseTransparentHugePages \
-XX:+AlwaysPreTouch \
-Xms16g -Xmx16g \
-jar application.jar
Без -XX:+AlwaysPreTouch ядро откладывает фактическое выделение физической памяти до первого обращения к каждой странице. THP при этом работает хуже, потому что страницы выделяются хаотично, по мере надобности, и ядру сложнее объединить их в hugepages. С флагом - heap занят полностью с первой же секунды, THP получает предсказуемую картину памяти и работает значительно эффективнее.
Режим defer+madvise для параметра defrag заслуживает отдельного внимания. В этом режиме дефрагментация памяти при выделении hugepage происходит не синхронно - то есть приложение не стоит и ждёт, пока ядро перетасует страницы. Вместо этого оно получает обычную страницу прямо сейчас, а khugepaged позднее, в фоне, спокойно собирает соседние страницы в hugepage без какого-либо влияния на latency приложения.
Мониторинг после настройки - доверяй, но проверяй
Настройка hugepages без последующего контроля - это работа вслепую. Применить изменения и считать задачу решённой - ошибка, которую совершают даже опытные администраторы. После применения конфигурации нужно убедиться, что память действительно используется так, как задумано, а не работает по старому сценарию из-за какой-нибудь мелкой детали в конфигурации приложения.
Базовая проверка - снова /proc/meminfo, но теперь при живой нагрузке:
grep -E 'HugePages|AnonHuge|ShmemHuge' /proc/meminfo
Если AnonHugePages активно растёт при запущенном JVM-приложении - THP работает и выделяет hugepages для heap. Если HugePages_Free остаётся высоким при работающей базе данных - либо база не использует hugepages (нужно проверить конфигурацию), либо пул выделен с избыточным запасом.
Измерить TLB-промахи до и после настройки позволяет perf stat. Это особенно ценно сделать в паре - снять показания до изменений и сравнить с результатами после. Разница часто оказывается наглядной:
perf stat -e dTLB-loads,dTLB-load-misses,iTLB-loads,iTLB-load-misses \
-p $(pgrep postgres | head -1) -- sleep 30
Соотношение dTLB-load-misses к dTLB-loads после включения hugepages на нагруженных системах снижается с 5-10% до 0.5-1%. Именно эта разница трансформируется в снижение latency запросов и рост TPS - не за счёт изменения кода или схемы, а за счёт того, что процессор перестал тратить такты на бесплодные походы в таблицу страниц.
Ещё одна деталь, которую легко пропустить при настройке на NUMA-системах с несколькими процессорными узлами: hugepages нужно распределять явно по каждому узлу. Без этого ядро распределит их неравномерно - скорее всего, навалит всё на первый узел - и база данных на втором NUMA-узле будет использовать обычные страницы, не получая никакого выигрыша:
# Проверить распределение hugepages по NUMA-узлам
cat /sys/devices/system/node/node*/hugepages/hugepages-2048kB/nr_hugepages
# Задать явно для каждого узла
echo 2048 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 2048 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
Итоговая стратегия для тех, кто хочет результат
Честный ответ на вопрос "что лучше - THP или статические hugepages?" звучит так: зависит от приложения, и любой, кто даёт универсальный совет без оговорок, либо упрощает, либо не разобрался в теме. Стратегии расходятся принципиально - и это нормально.
Для продакшн-серверов PostgreSQL, Oracle, MySQL, MongoDB: THP отключается полностью через never, статические hugepages выделяются с запасом 10-15% от пиковой нагрузки, база настраивается на принудительное использование hugepages. После запуска проверяется, что HugePages_Rsvd соответствует ожиданиям, а compact_stall в /proc/vmstat не растёт.
Для Java-приложений с большим heap - Spark, Cassandra, Tomcat: THP переводится в режим madvise, дефрагментация настраивается через defer+madvise, JVM запускается с флагами -XX:+UseTransparentHugePages и -XX:+AlwaysPreTouch. Контроль - через AnonHugePages в /proc/meminfo.
Правильная работа с hugepages - это не разовая настройка, которую сделал и забыл. Это осознанный выбор стратегии управления памятью, который требует понимания того, как именно конкретное приложение работает с памятью. Сервер с корректно настроенными hugepages и отключённым THP ведёт себя предсказуемо: latency не скачет, фоновая дефрагментация не крадёт процессорное время, таблицы страниц не раздуваются до гигабайт при сотнях подключений. Разница между "работает" и "работает правильно" здесь измеряется не процентами - а порядками.