Статьи

Сборка мусора: увеличение пропускной способности

Полная Змея Вдохновение для этого поста пришло после того, как я наткнулся на определение « Свинья в Питоне » в глоссарии управления памятью . По-видимому, этот термин используется для объяснения ситуации, когда GC неоднократно продвигает крупные объекты из поколения в поколение. Эффект от этого, по-видимому, аналогичен эффекту питона, который проглатывает свою добычу целиком только для того, чтобы обездвижиться во время пищеварения.

В течение следующих 24 часов я просто не мог получить картину удушья питонов из моей головы. Как говорят психиатры, лучший способ избавиться от своих страхов — это рассказать о них. Итак, поехали. Но вместо питонов остальная часть истории будет о настройке сборки мусора. Я обещаю.

Остановки сбора мусора хорошо известны тем, что они могут стать узким местом в производительности. Современные JVM поставляются с современными сборщиками мусора, но, как я понял, найти оптимальную конфигурацию для конкретного приложения по-прежнему сложно. Чтобы даже иметь шанс вручную подойти к проблеме, нужно было бы понять точную механику алгоритмов сбора мусора. Этот пост может помочь вам в этом, так как я собираюсь использовать пример, чтобы продемонстрировать, как небольшие изменения в конфигурации JVM могут повлиять на пропускную способность вашего приложения.

пример

Приложение, которое мы используем для демонстрации влияния ГХ на пропускную способность, является простым. Он состоит всего из двух потоков:

  • PigEater — имитирует ситуацию, когда питон продолжает есть одну свинью за другой. Код достигает этого путем добавления 32 МБ байтов в java.util.List и ожидания 100 мс после каждой попытки.
  • PigDigester — имитирует процесс асинхронного переваривания. Код реализует пищеварение, просто обнуляя этот список свиней. Поскольку это довольно утомительный процесс, после каждой эталонной очистки эта нить спит в течение 2000 мс.

Оба потока будут работать в цикле while, продолжая есть и переваривать, пока змея не наполнится. Это происходит примерно при 5000 съеденных свиней.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package eu.plumbr.demo;
 
public class PigInThePython {
  static volatile List pigs = new ArrayList();
  static volatile int pigsEaten = 0;
  static final int ENOUGH_PIGS = 5000;
 
  public static void main(String[] args) throws InterruptedException {
    new PigEater().start();
    new PigDigester().start();
  }
 
  static class PigEater extends Thread {
 
    @Override
    public void run() {
      while (true) {
        pigs.add(new byte[32 * 1024 * 1024]); //32MB per pig
        if (pigsEaten > ENOUGH_PIGS) return;
        takeANap(100);
      }
    }
  }
 
  static class PigDigester extends Thread {
    @Override
    public void run() {
      long start = System.currentTimeMillis();
 
      while (true) {
        takeANap(2000);
        pigsEaten+=pigs.size();
        pigs = new ArrayList();
        if (pigsEaten > ENOUGH_PIGS)  {
          System.out.format("Digested %d pigs in %d ms.%n",pigsEaten, System.currentTimeMillis()-start);
          return;
        }
      }
    }
  }
 
  static void takeANap(int ms) {
    try {
      Thread.sleep(ms);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Теперь давайте определим пропускную способность этой системы как «количество переваренных свиней в секунду». Принимая во внимание, что свиньи вводятся в питона через каждые 100 мс, мы видим, что теоретическая максимальная пропускная способность этой системы может достигать до 10 свиней в секунду.

Настройка примера GC

Давайте посмотрим, как система ведет себя, используя две разные конфигурации. Во всех ситуациях приложение запускалось с использованием двухъядерного Mac (OS X 10.9.3) с 8G физической памяти.

Первая конфигурация:

  • 4G кучи ( -Xms4g –Xmx4g )
  • Использование CMS для очистки старого (-XX: + UseConcMarkSweepGC ) и Parallel для очистки молодого поколения -XX: + UseParNewGC )
  • Выделил молодому поколению 12,5% кучи ( -Xmn512m ), еще больше ограничив размеры пространств Эдема и Выжившего равными по размеру.

Вторая конфигурация немного отличается:

  • 2 г кучи ( -Xms2g –Xmx2g )
  • Использование Parallel GC для сбора мусора как у молодого, так и у пожилого поколения ( -XX: + UseParallelGC )
  • Выделил 75% кучи молодому поколению ( -Xmn1536m )

Теперь пришло время делать ставки, какая из конфигураций работала лучше с точки зрения пропускной способности (поросят, съедаемых в секунду, помните?). Те из вас, кто вкладывает деньги в первую конфигурацию, я должен вас разочаровать. Результаты в точности обратные:

  • Первая конфигурация (большая куча, большое старое пространство, CMS GC) способна съесть 8,2 свиньи в секунду
  • Вторая конфигурация (в 2 раза меньшая куча, большое молодое пространство, Parallel GC) способна съесть 9,2 свиньи в секунду

Теперь позвольте мне представить результаты в перспективе. Выделяя в 2 раза меньше ресурсов (по памяти), мы достигли на 12% лучшей пропускной способности . Это нечто настолько противоречащее общеизвестным фактам, что может потребоваться дополнительное разъяснение того, что на самом деле происходит.

Интерпретация результатов GC

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

jstat -gc -t -h20 PID 1с

Глядя на данные, я заметил, что первая конфигурация прошла 1129 циклов сбора мусора (YGCT + FGCT), что в общей сложности заняло 63,723 секунды:

1
2
3
4
5
Timestamp        S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT  
594.0 174720.0 174720.0 163844.1  0.0   174848.0 131074.1 3670016.0  2621693.5  21248.0 2580.9   1006   63.182  116     0.236   63.419
595.0 174720.0 174720.0 163842.1  0.0   174848.0 65538.0  3670016.0  3047677.9  21248.0 2580.9   1008   63.310  117     0.236   63.546
596.1 174720.0 174720.0 98308.0 163842.1 174848.0 163844.2 3670016.0   491772.9  21248.0 2580.9   1010   63.354  118    0.240   63.595
597.0 174720.0 174720.0  0.0   163840.1 174848.0 131074.1 3670016.0   688380.1  21248.0 2580.9   1011   63.482  118     0.240   63.723

Вторая конфигурация остановилась в общей сложности 168 раз (YGCT + FGCT) всего на 11,409 секунд.

1
2
3
4
5
Timestamp        S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT  
539.3 164352.0 164352.0  0.0    0.0   1211904.0 98306.0   524288.0   164352.2  21504.0 2579.2   27    2.969  141    8.441   11.409
540.3 164352.0 164352.0  0.0    0.0   1211904.0 425986.2  524288.0   164352.2  21504.0 2579.2   27    2.969  141    8.441   11.409
541.4 164352.0 164352.0  0.0    0.0   1211904.0 720900.4  524288.0   164352.2  21504.0 2579.2   27    2.969  141    8.441   11.409
542.3 164352.0 164352.0  0.0    0.0   1211904.0 1015812.6  524288.0   164352.2  21504.0 2579.2  27  2.969  141  8.441   11.409

Принимая во внимание, что работа, которую необходимо было выполнить в обоих случаях, была эквивалентна в том отношении, что — без видимых долгоживущих объектов, задача GC в этом упражнении по поеданию свиней состоит в том, чтобы просто избавиться от всего как можно быстрее. И используя первую конфигурацию, GC просто вынужден запускаться в ~ 6,7 раза чаще, что приводит к ~ 5,6-кратному увеличению общего времени паузы.

Таким образом, история выполнила две цели. Прежде всего, я надеюсь, что у меня в голове появилась картина удушливого питона. Другим и более важным выводом из этого является то, что настройка GC — это в лучшем случае сложное упражнение, требующее глубокого понимания нескольких основных концепций. Даже с действительно тривиальным приложением, использованным в этом сообщении в блоге, результаты, с которыми вы столкнетесь, могут оказать существенное влияние на планирование пропускной способности и емкости. В реальных приложениях различия еще более ошеломляющие. Так что выбор за вами, вы можете либо освоить концепции, либо сосредоточиться на своей повседневной работе и позволить Plumbr найти подходящую конфигурацию GC в соответствии с вашими потребностями.

Ссылка: Сборка мусора: увеличение производительности от нашего партнера по JCG Никиты Сальникова Тарновского в блоге Plumbr Blog .