Netflix перешел с G1 на Generational ZGC, начиная с JDK 21, из-за значительных преимуществ, связанных с многопоточной сборкой мусора.

Команда Spring АйО подготовила перевод статьи, в которой инженеры стримингового сервиса рассказали о неожиданных и ожидаемых преимуществах Generational ZGC.


В последней LTS версии JDK для сборщика мусора ZGC появился Generational режим.

Netflix перешел с G1 на Generational ZGC, начиная с JDK 21, из-за значительных преимуществ, связанных с многопоточной сборкой мусора. 

Сейчас больше половины наших стриминговых видео сервисов работают на JDK 21 с Generational ZGC, поэтому мы хотим поделиться нашим опытом и результатами. Если вам интересно, как Netflix использует Java, рекомендуем посмотреть доклад Поля Беккера How Netflix Really Uses Java.

Снижение "хвоста" распределения задержки

В наших сервисах, работающих на GRPC и DGS Framework, паузы сборки мусора являются значительным источником задержек в "хвосте" (tail latencies). Подобные задержки особенно заметны клиентам и серверам GRPC, где отмена запросов из-за тайм-аутов взаимодействует с функциями повышения надежности, такими как повторные попытки отправки запросов, "hedging" и резервные сценарии. Каждая ошибка – это отменённый запрос, приводящий к повторной отправке. Снижение этих пауз уменьшает общий трафик сервиса следующим образом:

Количество ошибок в секунду. Предыдущая неделя обозначена белым цветом, текущая доля отмен приведена в фиолетовом цвете. ZGC был подключен на сервисном кластере 16-го ноября

Устранение "шума" пауз также позволяет нам выявить реальные источники задержек по всей цепочке, которые в противном случае скрывались бы в этом "шуме", поскольку пиковые значения пауз могут быть весьма значительными:

Пиковые значения пауз GC с распределением по причинам, для того же сервисного кластера, который приведен выше. Да, паузы связанные с ZGC в самом деле обычно длятся меньше миллисекунды

Скрытый текст

Команда Netflix сталкивалась с подобными паузами, потому что G1 делает compact, а не concurrent sweep, как это делает CMS (Concurrent Mark Sweep) сборщик мусора. Компактизация позволяет избежать сильной фрагментации кучи после нескольких сборок, что является проблемой CMS сборщика мусора. Однако у G1 возникает другая проблема: compact проходит под "Stop-the-World" (STW) паузой. Эта проблема, кстати, решается в более современных GC таких, как ZGC и Shenandoah, где происходит concurrent compaction.

Эффективность

Хотя первоначальные результаты испытаний ZGC были многообещающими, мы предполагали, что его внедрение потребует компромиссов: немного снизится производительность приложений из-за ограничений связанных с чтением и записью, работы, выполняемой в локальных потоках, и того, что сборщик мусора будет конкурировать с приложением за ресурсы. Мы считали, что это приемлемая цена за устранение пауз, так как их отсутствие приносит больше пользы, чем потенциальные потери производительности.

Скрытый текст

У ZGC возникает конкуренция с приложением за вычислительные ресурсы, что приводит к снижению пропускной способности (throughput). Иными словами, когда ZGC выполняет сборку мусора, потокам приложения может достаться лишь около 70% процессорного времени, так как остальные вычислительные ресурсы будут потребляться потоками самого ZGC. Стоит отметить, что у Generational ZGC ситуация с этим обстоит лучше, но не радикально. Подробнее можно прочитать здесь: https://inside.java/2023/11/28/gen-zgc-explainer/

По факту же мы обнаружили, что для наших сервисов и архитектуры подобного компромисса не возникло. При одинаковой загрузке процессора ZGC улучшает как средние задержки, так и задержки на уровне P99 (99-й перцентиль), при этом использование CPU остаётся на том же уровне или даже лучше по сравнению с G1.

Постоянство в скорости запросов, их структуре, времени отклика и скорости выделения памяти в наших сервисах, безусловно, помогает ZGC. Однако мы также убедились, что ZGC способен эффективно справляться с менее предсказуемыми нагрузками (хотя есть и исключения, о которых мы поговорим далее).

Простота в эксплуатации

Владельцы сервисов часто обращаются к нам с вопросами о чрезмерно долгих паузах и с просьбами о помощи c настройкой сервисов для их устранения. У нас есть несколько фреймворков, которые периодически обновляют большие объёмы данных в памяти приложения, чтобы избежать внешних вызовов сервисов ради повышения эффективности. Эти регулярные обновления данных в памяти приложения могут "застать врасплох" G1, что приводит к паузам, значительно превышающим стандартные целевые значения.

Основной причиной, по которой мы ранее не использовали Generational ZGC, было большое количество подобных долгоживущих в памяти приложения данных. В худшем случае, который мы оценивали, non-generational ZGC использовал на 36% больше CPU по сравнению с G1 при той же нагрузке. С переходом на Generational ZGC эта разница сократилась до почти 10% в пользу ZGC.

Половина всех сервисов, необходимых для стриминга видео, используют нашу библиотеку Hollow для метаданных, хранящихся в памяти приложения. Устранение проблем с паузами позволило нам отказаться от методов уменьшения нагрузки на память, таких как array pooling, что привело к освобождению сотен мегабайт памяти.

Простота в эксплуатации также обеспечивается за счёт эвристик и настроек ZGC по умолчанию. Для достижения этих результатов не потребовалось явной тонкой настройки. Задержки при выделении памяти встречаются редко, обычно совпадают с резкими всплесками в частоте выделений памяти. К тому же, они короче, чем средние паузы, которые мы наблюдали при использовании G1.

Накладные расходы

Мы ожидали, что потеря compressed references на кучах меньше 32 ГБ из-за использования colored pointers, требующих 64-битных object pointers, станет важным фактором при выборе сборщика мусора.

Однако выяснилось, что, хотя это действительно имеет значение для сборщиков с stop-the-world паузами, для ZGC это не так. Даже на небольших кучах увеличение частоты выделения памяти компенсируется благодаря высокой эффективности и улучшенной работе ZGC. Особую благодарность хотим выразить Эрику Эстерлунду из Oracle за объяснение менее очевидных преимуществ colored pointers для многопоточных сборщиков мусора. Благодаря нему мы оценили ZGC шире, чем изначально планировали.

В большинстве случаев ZGC также стабильно предоставляет приложению больше доступной памяти.

Использованный и доступный объем кучи после каждого цикла работы GC, для того же сервисного кластера, который был приведён выше

ZGC имеет фиксированные накладные расходы в размере 3% от размера кучи, что требует большего количество системной памяти по сравнению с G1. Однако, за исключением нескольких случаев, не было необходимости снижать максимальный размер кучи для обеспечения дополнительного пространства. Эти исключения касались сервисов с повышенными потребностями в системной памяти.

Обработка ссылок в ZGC происходит только во время основной сборки мусора. Мы особенно следили за высвобождением прямых байтовых буферов (direct byte buffers), но пока не заметили какого-либо влияния. Эта разница в обработке ссылок привела к проблеме с производительностью при поддержке JSON thread dump, но это была необычная ситуация, связанная с тем, что фреймворк случайно создавал неиспользуемый экземпляр ExecutorService для каждого запроса.

Transparent Huge Pages

Даже если вы не используете ZGC, вам, вероятно, стоит использовать Huge Pages, и самый удобный способ это сделать — использовать Transparent Huge Pages.

ZGC использует разделяемую память для кучи, однако многие дистрибутивы Linux по умолчанию отключают эту возможность через параметр shmem_enabled, установленный в значение never. Из-за этого ZGC не может использовать Huge Pages с флагом -XX:+UseTransparentHugePages.

В одном из наших сервисов мы изменили только параметр shmem_enabled с "never" на "advise", что значительно снизило нагрузку на процессор:

Развертывание с переходом от страниц размером 4К до 2М. Пропуск на графике — это процесс immutable развертывания, временно удваивающий емкость кластера.

Наша стандартная конфигурация включает:

  • Установку минимального и максимального размера кучи в одинаковые значения.

  • Настройку флагов -XX:+UseTransparentHugePages и -XX:+AlwaysPreTouch.

  • Использование следующей конфигурации для Transparent Huge Pages:

echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
echo advise | sudo tee /sys/kernel/mm/transparent_hugepage/shmem_enabled
echo defer | sudo tee /sys/kernel/mm/transparent_hugepage/defrag
echo 1 | sudo tee /sys/kernel/mm/transparent_hugepage/khugepaged/defrag

Какие задачи не подходят для ZGC?

Не существует идеального сборщика мусора. Каждый из них балансирует между пропускной способностью сборки, задержкой приложения и использованием ресурсов в зависимости от своей цели.

Для задач, где G1 оказался эффективнее ZGC, мы заметили, что они больше ориентированы на пропускную способность с резкими всплесками выделения памяти и долгосрочными задачами, удерживающими объекты неопределённое время.

Как пример — сервис с резкими всплесками выделения памяти и большим количеством долгоживущих объектов. Подобный сценарий оказался подходящим для G1 по минимизации пауз и стратегии обработки old region. G1 мог избежать ненужной работы в циклах сборки мусора, тогда как ZGC с этим справлялся хуже.

Переход на ZGC по умолчанию дал владельцам приложений возможность пересмотреть свой выбор сборщика мусора. Для задач типа batch/precompute, где использовали G1 по умолчанию, многопоточный сборщик мог бы дать лучший результат. В одном крупном вычислительном процессе мы увидели улучшение пропускной способности на 6-8%, что сократило время выполнения на час по сравнению с G1.

Попробуйте сами!

Если бы мы не пересмотрели свои предположения и ожидания, мы могли бы упустить одно из самых значительных изменений, которое мы внесли в наши настройки за последние десять лет. Мы рекомендуем вам попробовать Generational ZGC самостоятельно. Он может оказаться для вас таким же приятным сюрпризом, каким он оказался для нас

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм - Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Ждем всех, присоединяйтесь

Комментарии (2)


  1. excentro
    17.09.2024 12:34
    +1

    Спасибо за статью! Включил ZGC в Intellij IDEA, вдруг быстрее работать станет :)


  1. GreyN
    17.09.2024 12:34

    direct byte buffer - это, скорее, "управляемый" байт-буфер, чем "прямой" байт-буфер