Память - это ресурс, который никогда не прощает небрежности. Особенно когда речь идёт о серверах с десятками гигабайт 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 не скачет, фоновая дефрагментация не крадёт процессорное время, таблицы страниц не раздуваются до гигабайт при сотнях подключений. Разница между "работает" и "работает правильно" здесь измеряется не процентами - а порядками.