Почему Transparent Huge Pages приобрели дурную славу
ЦП кэширует отображения виртуальных адресов в физические в буфере ассоциативной трансляции (TLB). Когда отображение отсутствует в кэше, ЦП фиксирует промах TLB и тратит такты на его поиск. При использовании обычных страниц одна запись TLB охватывает 4 КБ. При использовании больших страниц (huge pages) одна запись охватывает 2 МБ, то есть в 512 раз больше памяти. JVM с большой и активно используемой кучей (heap) получает наибольшую выгоду от больших страниц, так как сталкивается со значительно меньшим количеством промахов TLB и теряет меньше тактов ЦП.
THP заработала свою дурную репутацию из-за двух параметров по умолчанию в старых системах. Ядра Linux до версии 4.6, выпущенной в 2016 году, устанавливали значение defrag в положение always, а несколько корпоративных дистрибутивов поставлялись с настройкой enabled=always:
- Параметр enabled=always предоставлял страницы по 2 МБ каждому анонимному отображению на хосте, независимо от того, запрашивал их процесс или нет. Процесс, обратившийся всего к одному байту большой области, мог получить нагрузку в виде целой страницы на 2 МБ. Именно отсюда появились истории о раздувании памяти.
- Параметр defrag=always заставлял процесс, которому требовалась большая страница, ждать в режиме прямой рекуперации (direct reclaim) и компактизации, пока ядро ее не найдет. Эти ожидания превращались в всплески задержек, и рекомендация «отключить THP» попала в руководства по эксплуатации (документация ядра).
Вот как соотносятся три распространенные конфигурации:
Что беспокоило заказчика
Заказчик запускал THP в режиме always несколькими годами ранее, столкнулся ровно с теми же проблемами и отказался от него. Сопротивление исходило от трех групп: команды вычислений, отвечающей за настройки ядра; команды платформы, отвечающей за группы хостов; и команды баз данных, проводившей бенчмарки. Каждое опасение было обоснованным, и каждое из них формировалось под влиянием старых настроек по умолчанию.
«Мы пробовали это раньше, и это навредило». Действительно, пробовали с режимом always. Настройка по явному запросу, madvise, предоставляет процессу страницы по 2 МБ только для тех областей, которые он запрашивает явно. Большие страницы получают только те отображения, которые их запрашивают.
«Это настройка для всего хоста. А как же все остальное на машине?» Команда платформы отметила, что изменения затронут целые группы хостов, а не только тестируемые узлы. Это справедливо в отношении того, к чему применяется настройка, но неверно в отношении ее поведения. В режиме madvise большие страницы получает только тот процесс, который вызывает madvise(MADV_HUGEPAGE) для отображения. Сервисы на Go не запрашивают это по умолчанию, как и любая JVM. В этом флоте запрашивающим процессом был Azul Prime.
«Разве память не увеличится?» Для сервисов Azul Prime — незначительно. Куча выровнена по границам 2 МБ вместо 4 КБ, поэтому стоит ожидать умеренного увеличения резидентной памяти в диапазоне от 2 до 5% на границах областей кучи. Награда — это производительность ЦП и хвост распределения задержек (tail latency). Сервисы, которые не запрашивают эту функцию, не могут увеличиться в размерах из-за данной настройки, а в проанализированных нами данных они даже уменьшились.
«Наш ARM-флот работает на ядре с поддержкой страниц по 64 КБ. Большие страницы там означают гораздо больший размер». Верно. При базовых страницах в 64 КБ размер большой страницы составляет 512 МБ, поэтому запрос был разделен по архитектурам. Azul Prime не использует путь THP для разделяемой памяти на ARM64. Вместо этого он использует тегированные адреса кучи (-XX:+UseTaggedAddressForJavaHeap) с обычными анонимными THP. Эта опция доступна по запросу начиная с Azul Prime 25.02.0.0 и включена по умолчанию на ARM64 с версии 26.08.0.0. Она поддерживает кучи размером до 512 ГБ; при превышении этого размера Azul Prime автоматически отключает ее и записывает предупреждение в лог.
Запрос для ARM был более узким: madvise для анонимной памяти и defer для дефрагментации, при этом shmem_enabled остался без изменений (документация Azul).
«Развертывание обновлений ядра происходит медленно, и мы не можем делать это для каждого сервиса по отдельности». Тоже верно. Настройки ядра для всего флота внедрялись с 48-часовым периодом «отлежки» (soake) между зонами; владельцы платформ сохраняли переопределения в собственных группах хостов, и изменение требовало одобрения от команд, которые никогда не слышали о возможности такой оптимизации. С инфраструктурной стороны настройку легко внедрить, так как на самих хостах ничего видимого не меняется. Выгода проявляется внутри приложений, которые ее используют.
Что произошло после внедрения
Куча Azul Prime никогда не представляла риска фрагментации. Фрагментация становится проблемой, когда процесс отображает большую область, касается лишь ее небольшой части и получает нагрузку в 2 МБ. Куча Azul Prime на x86_64 имеет противоположную форму. Она выделяется полностью при запуске и, при значении -Xms, равном -Xmx, никогда не возвращается ОС. Azul Prime и так разбивает ее на блоки по 2 МБ, поэтому поддержка ее большими страницами по 2 МБ не добавляет никакой внутренней фрагментации, которой не было бы и так. На x86_64 сборщик C4 также выполняет многократное отображение (multi-maps) этой кучи через разделяемую память Linux, поэтому отдельная настройка shmem_enabled имеет значение: обычные анонимные THP никогда не достигают отображения разделяемой памяти (документация Azul). Azul Prime выполняет запрос самостоятельно. Когда он обнаруживает THP на хосте, он вызывает madvise для кучи Java и некоторых внекучевых (off-heap) структур без необходимости использования флагов JVM. Хост делает большие страницы доступными, а среда выполнения, которая может их использовать, задействует их.
Неожиданностью стала память. На хостах, где все еще работал режим always, Java-сервис, не использующий Azul Prime и поэтому никогда не запрашивавший эту функцию, снизил показатель резидентной памяти примерно на 5% в течение одного-трех дней после развертывания (примерно на 3 ГиБ на контейнер на уровне 99-го перцентиля — с примерно 57 ГиБ до примерно 54 ГиБ) и остался на этом уровне. В режиме always каждое отображение на машине дополнялось до страниц размером 2 МБ. Как только madvise убрал это дополнение, сервисы, которые никогда не просили большие страницы, уменьшились сильнее всего. Ожидалось, что сервисы Azul Prime останутся стабильными, поскольку их кучи сохраняют свои большие страницы. Одно предостережение для тех, кто проверяет это на собственном флоте: на x86_64 утилиты top и ps завышают показатель резидентного набора памяти (RSS, память, которую процесс удерживает в ОЗУ) для Azul Prime, так как куча имеет многократное отображение. Сравнивайте пропорциональный размер набора (PSS, который делит разделяемые страницы между процессами, их использующими; утилита smem сообщает этот показатель) или /proc/meminfo вместо RSS для каждого процесса (известные проблемы Azul).
Тот же сервис JVM в трех дата-центрах: p99 RSS на контейнер снизился на 4,7–5,4% в течение одного-трех дней после переключения THP на хостах с always на madvise. Производственные метрики заказчика, анонимизированы.
Для проявления экономии потребовалось время. Снижение проявилось через один-три дня после развертывания, дата-центр за дата-центром. Изменение enabled влияет только на будущие выделения памяти. Ядро не разбивает уже существующие большие страницы, поэтому экономия приходит по мере того, как старая память перерабатывается через перезапуски и автомасштабирование. Простая проверка: сервисы, перезапущенные до изменения, вернулись к прежнему уровню памяти, а сервисы, перезапущенные после него, вернулись к новому, более низкому уровню.
Что было измерено, а что — нет. Измеренным результатом этого развертывания является память. Бенчмарки заказчика по ЦП и хвосту задержек для сервисов Azul Prime все еще находились в разработке на момент написания этой статьи, поэтому в данной публикации они не приводятся. Что касается масштаба, документация Azul оценивает прирост от страниц по 2 МБ для кучи Azul Prime в диапазоне от 0 до 15% по медианной задержке и нагрузке на ЦП (около 10% в типичных рабочих нагрузках реальных клиентов и мало или вообще ничего на микробенчмарках) (документация Azul).
Как это включить
Это относится к работе Azul Prime без использования Azul Zing System Tools (ZST), которые являются опциональными начиная с Azul Prime 19.07 и требуются только для куч размером более 2,5 ТБ и более старых операционных систем. В режиме ZST Azul Prime использует собственную реализацию больших страниц памяти (large pages), и все описанное ниже к нему не относится. Подробности приведены в документации Azul и в руководстве администратора ядра.
1. Убедитесь, что ядро поддерживает прозрачные огромные страницы для разделяемой памяти (x86_64). Файл должен содержать все шесть политик. Если выводится меньше, ядро слишком старое для этого сценария.
2. На платформе x86_64 установите четыре значения. Включите опцию для анонимной памяти, включите опцию для разделяемой памяти, настройте фоновое уплотнение (compaction) и позвольте khugepaged продолжать работу. В документации Azul в качестве альтернативы упоминается defer+madvise, если другие настройки на хосте этого требуют, но это возвращает синхронное уплотнение для областей, запросивших огромные страницы, включая кучу Azul Prime, поэтому предпочтительнее использовать defer (документация ядра). Не устанавливайте для defrag значение always.
3. На ARM64 пропустите строку для разделяемой памяти. Используйте те же команды без shmem_enabled. Не передавайте флаг -XX:+UseLargePages на ARM64, так как он имеет более высокий приоритет и отключает режим с тегированными адресами.
4. Обеспечьте сохранение настроек после перезагрузки. Используйте sysfsutils на Ubuntu, профиль tuned на RHEL, CentOS, Oracle Linux и Amazon Linux 2, а также oneshot-модуль systemd на Amazon Linux 2023. Сначала зафиксируйте текущие значения хоста. Если некоторые из них уже совпадают, а другие нет, значит, конфигурация этого сервера уже изменялась кем-то другим, поэтому объедините настройки, а не перезаписывайте их. В большинстве дистрибутивов, как и в самом ядре, по умолчанию для defrag установлено значение madvise, что может приостановить работу Java-процесса во время прямого уплотнения, поэтому на каждом хосте задавайте значение defer.
5. В Kubernetes не меняйте ничего в образе. THP — это настройка на уровне ноды (node-level). Поды наследуют ее и не могут задавать ее самостоятельно. Если вы не можете напрямую изменить конфигурацию хоста, надежным решением будет использование привилегированного DaemonSet, который выполняет команды на каждой ноде до запуска подов приложения.
6. Проверьте с помощью проверки (probe). Заголовок лога сборщика мусора (GC) показывает, получила ли куча огромные страницы, а /proc/meminfo показывает их объем. На рабочем хосте x86_64 сумма в ShmemHugePages должна соответствовать суммарному значению -Xmx работающих на нем куч Azul Prime. Пример ниже, взятый из документации Azul, относится к небольшой тестовой JVM:
7. Измеряйте там, где есть реальная польза. Не в дашбордах уровня хоста и не в микробенчмарках, чья горячая куча слишком мала, чтобы страдать от промахов TLB. Смотрите на показатели CPU и p95/p99 латентности для сервисов, которые задействовали эту функцию, используя то же железо и тот же размер кучи, что и до изменений.
Что показывает опыт клиентов
Каждая строка «отключите THP» в инструкциях была написана для определенных условий: ядро, поставлявшееся с параметром always, уплотнение, выполнявшееся при обработке ошибок страниц памяти (page-fault path), и среды выполнения, которые не умели запрашивать огромные страницы. Все три условия изменились.
Клиент отделил то, что доставляло им проблемы (always, синхронное уплотнение, единая конфигурация для всех архитектур), от того, чего они хотели — уменьшения количества промахов TLB на тех JVM, которые могли это использовать. Они внедрили настройку, соответствующую текущему ядру и текущей среде выполнения. Результатом стало уменьшение потребления памяти сервисами, которые ее не задействовали, и куча на базе страниц 2 МБ для сервисов на базе Azul Prime, которые ее использовали.
Часто задаваемые вопросы
Стоит ли включать Transparent Huge Pages для Java-приложений?
THP не является обязательным условием; JVM отлично работают как с включенной, так и с выключенной функцией. Ее стоит включить для JVM с большими кучами, если используется выборочная активация: установите enabled в значение madvise, а defrag в значение defer, чтобы только те среды выполнения, которые запрашивают страницы по 2 МБ, получали их, а выделение памяти не ждало завершения уплотнения в пути обработки страничных прерываний. Azul рекомендует использовать THP для куч Azul Prime объемом от 100 ГБ и более, или в случаях, когда пропускная способность или время запуска ниже ожидаемых без этой функции (документация Azul). Azul Prime автоматически задействует Java-кучу, если это позволяет хост.
В чем разница между THP always, madvise и never?
always связывает каждое анонимное отображение памяти со страницами по 2 МБ, запрашивал это процесс или нет, что увеличивает потребление памяти для процессов, использующих лишь часть области памяти.
never отключает THP, поэтому каждый процесс несет издержки TLB для страниц по 4 КБ.
madvise предоставляет огромные страницы только тем областям памяти, которые процесс помечает флагом madvise(MADV_HUGEPAGE).
Влияет ли включение THP на каждый процесс на хосте?
Настройка применяется ко всему хосту, но при использовании madvise выделение памяти остается опциональным для каждого отдельного отображения. Процессы, которые никогда не вызывают madvise(MADV_HUGEPAGE), продолжают использовать страницы по 4 КБ. Azul Prime задействует эту функцию для своей Java-кучи без необходимости использования флагов JVM, а флаг -XX:-UseTransparentHugePages отключает это поведение для отдельного процесса.
Почему моя Java-куча не использует огромные страницы, несмотря на то, что THP включен?
Параметр enabled охватывает только анонимную память. На архитектуре x86_64 куча Azul Prime мапится через разделяемую память, поэтому для shmem_enabled также нужно установить значение advise. В большинстве дистрибутивов по умолчанию установлено значение never. На ARM64 Azul Prime использует тегированные адреса кучи с обычной анонимной THP, поэтому shmem_enabled не требуется.












