Выбор и настройка Garbage Collector для Highload системы в Hotspot JVM



    Введение


    При работе в сфере RTB (Real Time Bidding) одной из ключевых характеристик является время, затраченное на показ рекламы пользователю, зашедшему на сайт. Оно складывается из нескольких этапов, один из которых – аукцион за рекламное место, проводимый SSP (Supply Side Platform) между несколькими DSP (Demand Side Platform) системами. В этом случае критической величиной является время, за которое DSP успеет ответить своим инвентарем и денежной ставкой за данный показ. Как правило, верхняя граница этого времени составляет примерно 100 миллисекунд. С учетом того, что для оптимальной производительности рекламных кампаний требуется десятки тысяч запросов в секунду, выполнение данного требования может стать весьма нетривиальной задачей.

    Наш Ad Server, отвечающий за основную работу GetIntent DSP разработан на языке Java и работает на стандартной Hotspot JVM, имеющей общеизвестные механизмы сборки мусора (GC). Поэтому наиболее оптимальный вариант лежит в анализе того, как именно происходит работа с памятью, и как следствие выбор наиболее подходящего алгоритма сборки мусора и его оптимальная настройка. Об этом и пойдет речь в данной статье.

    В совокупности, наш ожидаемый результат – максимальный баланс между количеством серверов (чем меньше, тем лучше) и суммарной продолжительностью и частотой GC пауз, во время которых мы можем терять потенциальные показы.

    Как мы тестировали


    Для тестирования использовалось 2 рабочих станции. На первой JVM запускалась с:
    -Xmx4500m
    

    На второй:
    -Xmx12g
    

    Версия JVM: Oracle 1.8.0_66-b17
    Сравнивались сборщики мусора CMS (Concurrent Mark Sweep) и G1 (Garbage First)
    Тестирование производилось в течение 16 часов на нагрузке полностью соответствующей боевой.

    CMS (Concurrent Mark Sweep)


    CMS позволяет значительно сокращать задержки, связанные со сборкой мусора. Однако при его использовании приходится неизбежно сталкиваться с двумя основными проблемами, которые и создают необходимость в дополнительной настройке:
    • Фрагментация памяти
    • Высокий allocation rate

    Положительно повлиять на первый параметр можно путем контролирования promotion rate показателя. Для этого необходимо определить, какой объем объектов попадает в Tenured, а какой «умирает молодым» в Eden области.

    Тестирование производилось со следующими параметрами:
    -XX:+UseConcMarkSweepGC
    -XX:NewRatio=1, 3, 5
    

    для логирования использовались:
    -XX:+PrintGCDetails -XX:+PrintGC -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+PrintGCDateStamps -XX:+PrintCMSInitiationStatistics -XX:PrintCMSStatistics=1
    


    G1 (Garbage First)


    G1 GC выглядит заманчивым выбором для RTB bidder-а, так как его основая цель – выдерживать стабильные и предсказуемые Stop The World (STW) паузы. Это также обуславливает простоту и наглядность его настройки. Фактически надо оперировать только одним параметром – максимально допустимой длительностью STW паузы: -XX:MaxGCPauseMillis
    В нашем случае ради исключения случайных долгих пауз можно пожертвовать небольшой долей throughput.

    Относительно G1 GC, с момента его появления в качестве доступного для экспериментов сборщика мусора, сформировались некоторые предубеждения, главное из которых то, что MaxGCPauseMillis не выдерживается. Также есть озвученная Oracle рекомендация использовать его на достаточно больших размерах heap ( >= 6 Gb).
    Насколько все это актуально мы узнаем после нашего тестирования. Также уделим немного времени такой эксклюзивной для G1 GC функции как String Deduplication.

    Тестирование производилось со следующими параметрами:
    -XX:+UseG1GC
    -XX:MaxGCPauseMillis=100, 60, 40
    

    Дополнительно были проведены тесты с параметром:
    -XX:MaxTenuringThreshold=8
    

    для логирования использовались:
    -XX:+PrintGCDetails -XX:+PrintGC -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+PrintGCDateStamps -XX:+PrintAdaptiveSizePolicy -XX:+PrintReferenceGC
    


    Max Heap Size 4.5Gb


    Сводная таблица распределения Stop The World пауз:

    Однозначным победителем в данной конфигурации выходит CMS с флагом
    -XX:NewRatio=5
    

    Как можно заметить, несмотря на то, что показатель ms/sec паузы у данной конфигурации чуть хуже чем у остальных, она все равно показывает себя как самая стабильная – ~12 ms средняя пауза и практически 98% укладывается в норму – отличный для нас результат. При таких показателях на один Full GC в течение 16 часов можно закрыть глаза.

    График распределения latency для лучших показателей G1 и CMS:

    Анализ результатов CMS


    Мы проэкспериментировали с наборами параметров, в которых размер Eden (-XX:NewRatio) был 1/2, 1/4, и 1/6 от общего размера памяти. Средний promotion rate для этих конфигураций распределился соответствующим образом: 1.7, 2.75 и 2.79 mb/sec, что вполне логично – чем меньше размер Eden, тем больше мусора успевает просочиться непосредственно в Old Gen. Как можно заметить, с определенного момента, размер Eden области начинает слабо влиять на этот показатель. В нашем случае мы можем пожертвовать более высоким promotion rate (как следствие более частые OldGen сборки и большая вероятность фрагментации) ради минимально возможной средней задержкой в течении работы приложения.

    Анализ результатов G1


    Видно, что G1 тесно в столь маленьком heap. Mixed паузы очень часты,
    -XX:MaxGCPauseMillis оказывает маленькое влияние на конечный результат, а конфигурация с желаемой паузой 40ms не смогла обойтись без Full GC.
    Однако есть еще один момент, который нас смутил. По умолчанию G1 выбирает 15 ages для Survivor области. Мы решили посмотреть, действительно ли нам необходимо столько:

    Очевидно странный знак. Начиная примерно с age 8 размер остается всегда примерно на одном уровне; это говорит о том, что это долгоживущие объекты, которые скорее всего в любом случае попадут в Tenured область, а до этого при каждой минорной сборке мы просто переливаем из пустого в порожнее, тогда как могли бы сразу поместить все это в OldGen. Хорошее решение – поставить в значение MaxTenuringThreshold=8.
    Однако, в случае с heap 4.5Gb мы не заметили большой разницы в результатах, поэтому для краткости опустим их. Посмотрим изменится ли что-то на большом heap.

    Max Heap Size 12Gb


    Сводная таблица распределения Stop-the-World пауз:

    Состав представителей G1 немного изменился, т.к. параметр MaxTenuringThreshold=8 (в таблице mtt=8) в данной конфигурации начал приносить заметный результат.
    На большом heap G1 расправил крылья и вышел вперед как по общему распределению пауз, так и по очень короткой максимальной паузе. При этом среднее время затрачиваемое на GC составило меньше 7ms каждую секунду, т.е. меньше 0.7%

    График распределения latency для лучших показателей G1 и CMS:

    Анализ результатов CMS


    Считается, что основная проблема CMS это вопрос масштабируемости. Наше тестирование это подтверждают. Практически все показатели хуже, чем при использовании маленького размера heap. Из плюсов можно отметить, что благодаря большему объему памяти, влияния фрагментации здесь заметно ниже – ни одного Full GC за все время эксперимента.

    Анализ результатов G1


    Результат явно показывает, что G1 действительно гораздо стабильнее на больших объемах памяти; достаточно четко выполняются условия, заданные в настройках. Здесь бесспорный победитель с 40 ms latency. Средняя пауза выросла всего на 3 ms, когда как размер памяти вырос почти в 2.4 раза! Что уж говорить про показатель ms/sec – в два раза лучше.

    G1 String Deduplication


    Поскольку наш биддер работает с текстовым OpenRTB протоколом, пишет множество строковых логов, хранит строковые кеши, и т.д., то вполне логично ожидать большой эффект от этой новой функции. В теории количество сборок мусора должно сократиться при том, что время средней сборки увеличится. Мы добавили этот флаг для конфигурации с MaxGCPauseMillis = 100ms и Xmx=4500m:

    Хоть средняя пауза и находится в указанных пределах, кол-во пауз превышающих 1000ms превысило допустимые пределы. Это видно на графике:

    Попытки установить меньшую длительность паузы приводили к очень сильному росту потребления CPU. От использования данного параметра было решено отказаться.

    Итоги


    Мы провели детальный анализ CMS и G1 сборщиков мусора, основной целью которого было понимание того, как сильно мы можем снизить влияние GC на latency – наиболее критичный показатель для нашей системы.

    Вполне ожидаемый результат – однозначных выводов здесь нет. Для VM с размером памяти 5Gb, вышел победителем CMS с конфигурацией -XX:NewRatio=5; несмотря на большую максимальную паузу, в течении жизни приложения он показывал более стабильный результат, лучший percentile и среднюю задержку. Однако на VM с размером heap 12Gb G1 с большим перевесом опередил CMS, что оправдывает рекомендации Oracle; ms/sec задержка лучше в 1.94 раза, max пауза в 13.3 раза!

    Благодаря этому исследованию мы могли больше не работать вслепую, руководствуясь лишь отдельными рекомендациями и разнородными мнениями; напротив, мы смогли найти идеальный баланс для нашей неоднородной в плане конфигурации системы, получая максимальную стабильность и как следствие прибыль из того, что мы имеем сегодня.

    Авторы статьи — absorbb и dmart28
    Getintent 21,21
    AdTech стартап (RTB, predictive modelling)
    Поделиться публикацией
    Похожие публикации
    Комментарии 21
    • +1
      Спасибо за интересную статью.

      Можете немного пояснить:
      1. какая версия jvm использовалась?
      2. почему проверяли только эти 2 сборщика мусора?
      3. почему не использовали другие параметры для сравнения? Например, CMSInitiatingOccupancyFraction.
      • +1
        Спасибо за отзыв.
        1. java version «1.8.0_66»
        Java(TM) SE Runtime Environment (build 1.8.0_66-b17)
        Java HotSpot(TM) 64-Bit Server VM (build 25.66-b17, mixed mode)
        2. Они широкодоступны и бесплатны.
        3. Использовали некоторые. В статью попали только те, которые оказали заметный эффект на результат.
        • +2
          Попробуйте JDK 9 EA. Шипилев говорит туда вошло дочерта вссего для G1, что не вошло в 8u
        • 0
          Я имел в виду встроенные, например, UseParallelOldGC.

          А можете написать, какие параметры ещё пробовали, т.е. что не повлияло на результат.
          Например, CMSScavengeBeforeRemark или что-то подобное.
          • +1

            В девятке должны поправить проблему со String Deduplication:
            https://bugs.openjdk.java.net/browse/JDK-8158871

        • +5
          Я правильно понял из таблицы, что при хипе в 12GB максимальный размер паузы для CMS-5 составил 3 секунды? При том, что promotion failure и concurrent mode failure не было? Тогда это очень странно. У нас на сервере, обрабатывающем 15000 rps, с 54GB хипом максимальная пауза за 500 мс не выходит. Что логи говорят, на какую фазу больше всего времени уходит?
          • +2
            также интересно, использовалиcь ли какие-то ключи специфичные для CMS? Из статьи только видно что указывался NewRatio
            • 0
              Пауза 3 сек за 16 часов была только одна. Без failures. Возможно, сыграл роль какой-то внешний фактор.
              Распределение времени по фазам можно увидеть на графиках.
              Для корректности сравнения укажу модель процессора на этом сервере: Intel® Xeon® CPU E3-1246 v3 @ 3.50GHz
              • +3
                А, судя по графику, это сборка в ParNew. И там ещё несколько точек около 1 секунды. Для сборки в молодом поколении (размер которого 2GB) это тем более много.

                Такое бывает по разным причинам: либо чрезмерное количество Weak/Soft-ссылок, либо множество ссылок из старого поколения в новое, либо сильная фрагментация старого поколения, либо ресайз хипа, если его размеры не фиксированы, либо дисковая активность, попавшая на период сборки… В общем, я к тому, что делать выводы об эффективности GC без должного анализа — преждевременно. Точно так же, как и по результатам бенчмарка судить о производительности приложения.
            • 0
              Просто инетресно а Azul не пробовали? https://www.azul.com/products/zing/pgc/
              • 0
                Нет, хотя конечно в курсе про их C4 сборщик и его возможности. На самом деле результаты Hotspot более чем подходящие для нашего кейса. Zing обычно используется в областях с намного более жесткими требованиями к latency, например HFT.
              • –3
                1) действительно, почему не azul
                2) почему java? (на самом слабом инстансе digitalocean «серверок» на golang показал рекорд в 9 микросекунд на разумный ответ)
                • 0
                  А нельзя ли как-то вручную управлять сборкой мусора и принимать запросы на другой инстанс, пока один собирает мусор?
                  • +1
                    Я так думаю им не нужно управлять сборкой мусора, а просто запустить несколько нод на конкурентной основе, т.к это аукцион
                  • –1
                    «При таких показателях на один Full GC в течение 16 часов можно закрыть глаза»

                    Вы ж так не пугайте. Уже из каментов я понял, что речь о FullGC который выполняется 3сек. А читая статью, я решил что у вас GC реально 16 часов работал. Долго думал как же это можно так загадить 4.5гига, чтобы они так долго чистились, и что ж это за такая задача, в которой можно простить 16часов сборки мусора.
                    • –2
                      Сталкивались ли вы при работе «UseConcMarkSweepGC» и «UseParNewGC» с логами вида 'GC (Allocation Failure)'? Если да, то как их решали?

                      2015-06-09T12:37:01.670+0300: [GC (Allocation Failure) 2015-06-09T12:37:01.670+0300: [ParNew: 77111K->4149K(78656K), 0.0092402 secs] 161272K->88309K(253440K), 0.0094265 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
                      • +6
                        А зачем их «решать»? Allocation Failure — это самая обычная, самая нормальная причина запуска GC.
                        Собственно, сборка мусора для того и придумана, чтобы освобождать память, когда её не получается выделить.
                      • 0
                        Очень интересно, насколько зависят ваши результаты от самого приложения, у нас, например, Eden заполняется очень быстро, при этом OldGen растет медленно, поэтому NewRatio мы ставим 1.
                        • 0
                          Подскажите, каким инструментом пользовались для построения графиков и сводных табличек по логам GC?

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое