Введение


При работе в сфере 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
Поделиться с друзьями
-->

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


  1. u_story
    08.06.2016 19:02
    +1

    Спасибо за интересную статью.

    Можете немного пояснить:
    1. какая версия jvm использовалась?
    2. почему проверяли только эти 2 сборщика мусора?
    3. почему не использовали другие параметры для сравнения? Например, CMSInitiatingOccupancyFraction.


    1. absorbb
      08.06.2016 19:21
      +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. Использовали некоторые. В статью попали только те, которые оказали заметный эффект на результат.


      1. tolkkv
        08.06.2016 22:38
        +2

        Попробуйте JDK 9 EA. Шипилев говорит туда вошло дочерта вссего для G1, что не вошло в 8u


        1. CyberSoft
          10.06.2016 16:43
          +1

          Надеюсь, в 9 не будет такой проблемы с G1: habrahabr.ru/company/jugru/blog/280420/#comment_8831960


          1. tolkkv
            12.06.2016 16:22

            А баг в итоге завели?


      1. u_story
        09.06.2016 00:20

        Я имел в виду встроенные, например, UseParallelOldGC.

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


      1. qwwdfsad
        14.06.2016 16:10
        +1

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


  1. apangin
    08.06.2016 20:02
    +5

    Я правильно понял из таблицы, что при хипе в 12GB максимальный размер паузы для CMS-5 составил 3 секунды? При том, что promotion failure и concurrent mode failure не было? Тогда это очень странно. У нас на сервере, обрабатывающем 15000 rps, с 54GB хипом максимальная пауза за 500 мс не выходит. Что логи говорят, на какую фазу больше всего времени уходит?


    1. m0nstermind
      08.06.2016 20:25
      +2

      также интересно, использовалиcь ли какие-то ключи специфичные для CMS? Из статьи только видно что указывался NewRatio


    1. absorbb
      09.06.2016 12:33

      Пауза 3 сек за 16 часов была только одна. Без failures. Возможно, сыграл роль какой-то внешний фактор.
      Распределение времени по фазам можно увидеть на графиках.
      Для корректности сравнения укажу модель процессора на этом сервере: Intel® Xeon® CPU E3-1246 v3 @ 3.50GHz


      1. apangin
        09.06.2016 13:52
        +3

        А, судя по графику, это сборка в ParNew. И там ещё несколько точек около 1 секунды. Для сборки в молодом поколении (размер которого 2GB) это тем более много.

        Такое бывает по разным причинам: либо чрезмерное количество Weak/Soft-ссылок, либо множество ссылок из старого поколения в новое, либо сильная фрагментация старого поколения, либо ресайз хипа, если его размеры не фиксированы, либо дисковая активность, попавшая на период сборки… В общем, я к тому, что делать выводы об эффективности GC без должного анализа — преждевременно. Точно так же, как и по результатам бенчмарка судить о производительности приложения.


  1. AlexeySuvorov
    08.06.2016 20:42

    Просто инетресно а Azul не пробовали? https://www.azul.com/products/zing/pgc/


    1. dmart28
      08.06.2016 21:23

      Нет, хотя конечно в курсе про их C4 сборщик и его возможности. На самом деле результаты Hotspot более чем подходящие для нашего кейса. Zing обычно используется в областях с намного более жесткими требованиями к latency, например HFT.


  1. izloy1
    09.06.2016 00:59
    -3

    1) действительно, почему не azul
    2) почему java? (на самом слабом инстансе digitalocean «серверок» на golang показал рекорд в 9 микросекунд на разумный ответ)


  1. YourChief
    09.06.2016 12:01

    А нельзя ли как-то вручную управлять сборкой мусора и принимать запросы на другой инстанс, пока один собирает мусор?


    1. gurinderu
      09.06.2016 12:05
      +1

      Я так думаю им не нужно управлять сборкой мусора, а просто запустить несколько нод на конкурентной основе, т.к это аукцион


  1. chabapok
    09.06.2016 13:29
    -1

    «При таких показателях на один Full GC в течение 16 часов можно закрыть глаза»

    Вы ж так не пугайте. Уже из каментов я понял, что речь о FullGC который выполняется 3сек. А читая статью, я решил что у вас GC реально 16 часов работал. Долго думал как же это можно так загадить 4.5гига, чтобы они так долго чистились, и что ж это за такая задача, в которой можно простить 16часов сборки мусора.


  1. shishmakov
    09.06.2016 13:54
    -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]


    1. apangin
      09.06.2016 14:03
      +6

      А зачем их «решать»? Allocation Failure — это самая обычная, самая нормальная причина запуска GC.
      Собственно, сборка мусора для того и придумана, чтобы освобождать память, когда её не получается выделить.


  1. nrcmn
    09.06.2016 21:19

    Очень интересно, насколько зависят ваши результаты от самого приложения, у нас, например, Eden заполняется очень быстро, при этом OldGen растет медленно, поэтому NewRatio мы ставим 1.