Администратор настраивает новый сервер. Redis при старте печатает предупреждение: отключите Transparent Huge Pages. PostgreSQL в документации советует включить huge pages, но уточняет - статические, не прозрачные. Oracle рекомендует отключить THP полностью. JVM-приложения, напротив, нередко выигрывают от его включения. Одна функция ядра, противоречивые советы, реальные последствия - и большинство администраторов копируют команду из интернета, не разобравшись, что именно они меняют.
Transparent Huge Pages - это механизм ядра Linux, автоматически объединяющий стандартные страницы памяти размером 4 КБ в большие страницы по 2 МБ. Цель - снизить нагрузку на TLB (Translation Lookaside Buffer) и уменьшить накладные расходы на управление памятью. Звучит как безусловное улучшение. На практике результат зависит от характера нагрузки - и разница может быть разительной в обе стороны.
Зачем нужны большие страницы и как TLB влияет на производительность
Чтобы понять THP, нужно понять, зачем вообще существуют большие страницы. Каждый раз, когда процессор обращается к виртуальному адресу, он должен найти соответствующий физический адрес через таблицу страниц. Эта трансляция дорогая, поэтому процессоры кэшируют недавние результаты в TLB - небольшой и быстрой аппаратной структуре. Проблема в том, что TLB конечен: современный процессор содержит 64-1024 записей в разных уровнях TLB.
При размере страницы 4 КБ каждая запись TLB покрывает 4 КБ памяти. Процесс с 1 ГБ активных данных нуждается в 262 144 TLB-записях для полного покрытия - при реальном TLB в несколько сотен записей промахи неизбежны. Промах TLB вынуждает процессор обходить многоуровневую таблицу страниц в памяти - операция, занимающая десятки наносекунд. На рабочей нагрузке с высокой плотностью случайного доступа к памяти это превращается в измеримый процент процессорного времени.
Страница размером 2 МБ покрывает в 512 раз больше памяти при одной TLB-записи. Тот же 1 ГБ данных требует теперь всего 512 записей. TLB-промахи резко сокращаются, и процессор тратит больше времени на полезные вычисления. Именно эта логика стоит за huge pages в целом и за THP в частности.
# Проверить текущий статус THP
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never - включён для всех процессов
# Статистика THP из счётчиков ядра
grep -E "AnonHugePages|HugePages" /proc/meminfo
# Счётчики событий THP
grep thp /proc/vmstat
Как khugepaged объединяет страницы и почему это не бесплатно
THP работает через два механизма. Первый - прямое выделение при page fault: когда процесс обращается к новой странице, ядро сразу пытается выделить 2 МБ непрерывной физической памяти. Если успешно - создаётся большая страница. Если нет - возвращается к стандартным 4 КБ.
Второй механизм - фоновый демон khugepaged. Он периодически сканирует адресные пространства процессов в поисках соседних 4-КБ страниц, которые можно объединить в одну 2-МБ страницу. Это объединение называется page collapse. Звучит как невидимая оптимизация. В реальности это операция с ненулевой ценой: страницы нужно скопировать в новое физически непрерывное место, старые записи таблицы страниц нужно обновить, TLB нужно сбросить. На нагруженной системе с большим числом процессов khugepaged потребляет заметное количество CPU.
Настройки khugepaged доступны через sysfs:
# Интервал между сканированиями в миллисекундах (по умолчанию 10000)
cat /sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs
# Число страниц, сканируемых за один проход (по умолчанию 4096)
cat /sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan
# Снизить агрессивность фонового объединения
echo 30000 > /sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs
echo 512 > /sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan
Более серьёзная проблема - дефрагментация памяти. Для размещения 2-МБ страницы нужно 512 последовательных физических страниц по 4 КБ. Через несколько часов работы система накапливает фрагментацию физической памяти, и найти 512 непрерывных свободных страниц становится сложнее. Ядро запускает компакцию памяти - перемещение существующих страниц для создания непрерывных блоков. На нагруженном сервере компакция может занимать миллисекунды, вызывая всплески задержек.
Три режима THP и что каждый из них означает
THP поддерживает три режима работы для параметра enabled и отдельно три режима для параметра defrag. Понимание обоих необходимо для осознанной настройки.
# Посмотреть текущие режимы
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag
# Режим always - huge pages для всех анонимных маппингов
echo always > /sys/kernel/mm/transparent_hugepage/enabled
# Режим madvise - только для регионов, запрошенных через madvise(MADV_HUGEPAGE)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# Режим never - полное отключение THP
echo never > /sys/kernel/mm/transparent_hugepage/enabled
Режим always - дефолт в большинстве дистрибутивов. Ядро агрессивно использует huge pages везде, где возможно. Хорошо работает для вычислительных нагрузок с плотным последовательным доступом к памяти. Плохо работает для приложений с множеством небольших разрозненных аллокаций: Redis, выделяющий тысячи объектов по 50-100 байт, при включённом always может расходовать в 2.5 раза больше физической памяти из-за внутренней фрагментации внутри 2-МБ страниц.
Режим madvise - компромисс и лучший выбор для смешанных серверов. THP отключён по умолчанию, но приложения могут явно запросить huge pages для конкретных регионов памяти через системный вызов madvise(addr, len, MADV_HUGEPAGE). JVM с флагом -XX:+UseTransparentHugePages использует именно этот механизм. База данных, управляющая памятью вручную, может оптимизировать только нужные области.
Режим never - полное отключение. Единственный вариант для Redis, Oracle, Apache Cassandra и TiDB согласно их официальной документации.
Параметр defrag управляет дефрагментацией памяти при выделении huge pages:
# always - синхронная компакция при каждом запросе (наибольшие задержки)
echo always > /sys/kernel/mm/transparent_hugepage/defrag
# defer+madvise - компакция откладывается на фон для madvise-регионов
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
# never - не выполнять компакцию ради huge pages
echo never > /sys/kernel/mm/transparent_hugepage/defrag
Диагностика проблем с THP через vmstat и perf
Если сервер испытывает необъяснимые всплески задержек при нормальном потреблении CPU и памяти, THP - один из первых кандидатов для проверки. Ядро ведёт счётчики всех THP-событий в /proc/vmstat:
# Ключевые счётчики для диагностики
grep -E "thp_fault_alloc|thp_fault_fallback|thp_collapse|thp_split|compact_stall" /proc/vmstat
# compact_stall - число раз когда процесс ждал компакцию памяти
# thp_fault_fallback - не удалось выделить huge page, откат к 4KB
# thp_split_page - huge pages разбивались обратно на маленькие
Высокое значение compact_stall - прямое свидетельство того, что процессы периодически ждут завершения компакции памяти. Именно эти ожидания проявляются как задержки. Высокое соотношение thp_fault_fallback к thp_fault_alloc означает, что система не может найти непрерывные блоки памяти для huge pages - симптом фрагментации.
# Степень фрагментации памяти по порядкам
cat /sys/kernel/debug/extfrag/extfrag_index
# Прямые утечки reclaim памяти через sar
sar -B 1 10 | grep pgscand
# pgscand/s > 0 длительное время - признак проблем с фрагментацией
# Измерить TLB-промахи для конкретного процесса
perf stat -e dTLB-loads,dTLB-load-misses,iTLB-loads,iTLB-load-misses \
-p $(pgrep myapp) sleep 10
Если TLB-промахи составляют менее 1-2% от всех обращений, включение THP вряд ли даст измеримый прирост. Если процент высокий и паттерн доступа к памяти преимущественно последовательный - THP может помочь.
Постоянное отключение через systemd и правильная конфигурация для баз данных
Запись в /sys/kernel/mm/ не переживает перезагрузку. Для постоянного применения настройки правильнее всего использовать systemd-юнит, который выполняется до старта баз данных:
# /etc/systemd/system/disable-thp.service
[Unit]
Description=Disable Transparent Huge Pages
After=sysinit.target local-fs.target
Before=mongod.service postgresql.service redis.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 disable-thp
systemctl start disable-thp
Альтернатива через параметры ядра в GRUB - радикальнее, но работает ещё до монтирования файловых систем:
# Добавить в GRUB_CMDLINE_LINUX в /etc/default/grub
transparent_hugepage=never
# Применить
update-grub # Debian/Ubuntu
grub2-mkconfig -o /boot/grub2/grub.cfg # RHEL/Fedora
Для смешанных серверов, где часть приложений выигрывает от THP, а часть проигрывает, режим madvise с defer+madvise для дефрагментации - оптимальный компромисс. Базы данных получают предсказуемую задержку, JVM и вычислительные задачи могут запросить huge pages явно для нужных буферов.
THP - редкий пример настройки ядра, где правильный ответ зависит не от предпочтений, а от измеримых характеристик нагрузки. Нагрузка с плотным последовательным доступом к большим буферам памяти выигрывает. Нагрузка с множеством мелких случайных аллокаций проигрывает - иногда значительно. Прежде чем менять настройку в любую сторону, стоит запустить измерение TLB-промахов и проверить compact_stall - эти числа скажут больше, чем любая общая рекомендация.