[Перевод] Сбор мусора в Голанге (1)

Go

вводить

Статьи в этой серии следующие

  1. Сбор мусора в Голанге (1)
  2. Сбор мусора в Голанге (2): Go Traces
  3. Сборка мусора в Голанге (3): вперед

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

Начиная с версии go 1.12, язык go использует одновременный трехцветный маркер без генерации и последовательный сборщик. Если вы хотите узнать, как помечать и очищать, см.эта статья. Реализация сборщика мусора golang обновляется и развивается с каждым выпуском. Поэтому, как только будет выпущена следующая версия, любые детали реализации уже не будут точными.

В общем, этот пост не будет вдаваться в подробности фактической реализации. Я поделюсь с вами некоторыми особенностями поведения сборщика и объясню, как с ними бороться, независимо от деталей реализации и будущих изменений. Это сделает вас лучшим разработчиком golang.

Куча не контейнер

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

поведение коллектора

Когда начнется сбор, сборщик завершит три этапа работы. Два из этих этапов будут производитьStop The World(STW), а другой этап также вызывает задержку и приводит к снижению пропускной способности приложения. Три этапа:

  • Mark Setup - STW
  • Marking - Concurrent
  • Mark Termination -STW

Подробное описание каждого этапа смотрите ниже.

Mark Setup -STW

Когда сбор начинается, первое, что нужно сделать, это открыть барьер записи (Write Barrier).写屏障的目的是在collector和应用程序goroutines并发时候,允许回收器去维持数据在堆中的完整性。

Чтобы включить барьер записи, каждая запущенная горутина приложения должна быть остановлена. Эта активность обычно очень быстрая, в среднем от 10 до 30 микросекунд. При условии, что горутины вашего приложения ведут себя разумно.

Примечание. Чтобы лучше понять приведенную ниже диаграмму расписания, лучше всего прочитать ранее написаннуюголанг планированиестатья
Рисунок 1.1

На рис. 1 показаны 4 горутины приложений, которые выполняются до начала сборки мусора. Чтобы выполнить переработку, необходимо остановить каждую из 4 горутин, и единственный способ сделать это — проверить сборщик и дождаться, пока это сделает горутина.вызов метода. Вызов метода гарантирует, что горутины остановятся в безопасной точке. Что произойдет, если одна из горутин не вызывается, но вызываются другие методы?

Рисунок 1.2

На рис. 1.2 представлен практический пример проблемы. Если горутина P4 не остановится, сборка мусора не запустится. Но P4 выполняетtight loopСделайте некоторые математические расчеты, из-за которых сбор вообще не запускается.

L1:

01 func add(numbers []int) int {
02     var v int
03     for _, n := range numbers {
04         v += n
05     }
06     return v
07 }

L1 предоставляет код, который выполняет горутина P4. В зависимости от размера слайса, горутина может выполняться очень долго, без какой-либо возможности остановиться. Этот код предотвращает запуск сборки мусора. Что еще хуже, другие Ps не могут обслуживать другие горутины, потому что сборщик ждет. Поэтому очень важно, чтобы горутины вызывали методы в разумных пределах.

Примечание. Это то, что команда golang улучшит в версии 1.14, добавив планировщикТехнология упреждающего планирования

Marking -Concurrent

Как только барьер записи включен, коллектор начинает входить в фазу маркировки. Первое, что делает сборщик, — берет 25% доступного ЦП для собственного использования. Сборщик использует горутины для выполнения работы по сбору, то есть он извлекает из приложения соответствующее количество P и M. Это означает, что в программе go из 4 потоков один P будет отведен на работу по переработке.

Рисунок 1.3

На рис. 1.3 показано, как коллектор берет P1 при рециркуляции. Теперь коллекционер может начать процесс маркировки. Фаза маркировки заключается в маркировке используемого значения в памяти кучи обработки. Он проверит все существующие горутины в стеке, чтобы найти корневой указатель на динамическую память. Затем сборщик должен пройти по древовидной карте памяти кучи, начиная с корневого указателя. Во время обработки работы по маркировке на P1 приложение может продолжать выполняться одновременно на P2, P3 и P4. Это означает, что коллектор снижает текущую загрузку процессора на 25%.

Я надеюсь, что на этом все закончится, но нет. Что делать, если GOROUTINE GROUTINE GC достигает верхнего предела использования? Что, если другие три горутины приложения заставят Collector завершить работу вовремя? Если это произойдет, новое выделение памяти должно замедлиться, особенно на соответствующей горутине.

Если сборщик определяет, что он должен замедлить выделение памяти, он привлекает горутины приложения для помощи (Assist) для маркировки. это называетсяMark Assist. Время помещения горутины приложения в Mark Assist пропорционально количеству данных, которые она добавит в кучу. Одной из положительных особенностей Mark Assist является то, что он помогает ускорить переработку.

Рисунок 1.4

На рис. 1.4 показано, что горутина, которая ранее работала в приложении на P3, теперь выполняет Mark Assist, чтобы помочь с перезапуском. Надеюсь, другие горутины не будут вовлечены. Приложения, испытывающие нехватку памяти, увидят, что большинство запущенных горутин выполняют небольшую часть работы Mark Assist во время сборки мусора.

Mark Termination -STW

После того, как работа по маркировке завершена, следующим процессом является завершение маркировки. В это время барьер записи закрыт, будут выполнены различные работы по очистке и будет рассчитана следующая цель восстановления. Горутины, которые обрабатывают замкнутый цикл в процессе маркировки, также вызывают повышенную задержку в STW Mark Termination.

Рисунок 1.5

На рис. 1.5 показано, что фаза «Отметить завершение» завершена, и все горутины остановлены. Это действие обычно завершается в течение 60-90 микросекунд. Этот этап можно пройти и без STW, но через STW код будет проще, а повышенная сложность кода не стоит этого небольшого выигрыша.

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

Рисунок 1.6

На рис. 1.6 показано, что после завершения повторного использования все доступные P теперь обрабатывают работу приложения.

Sweeping - Concurrent

После завершения переработки будет еще одно действие, называемое очисткой (Sweeping). Sweeping就是清理内存中有值但是没有被标记为in-use的堆内存。这个活动发生在当应用程序goroutines尝试去在堆中分配新的值的时候。 Sweeping延迟增加到了堆内存分配的开销中,并且和任何垃圾回收的延迟都没有关联。

Ниже приведен образец трассы на моей машине, который имеет 12 аппаратных нитей, выполняющих Goroutines.

Рисунок 1.7

На рис. 1.7 показан частичный снимок трассы. Вы можете видеть, что в коллекции (обратите внимание на синюю строку GC выше) 3 из 12 P используются для обработки GC. Вы можете видеть, что горутины 2450, 1978 и 2696 в это время выполняют Mark Assist вместо своей собственной программной работы. В конце коллекции есть только один P для обработки GC и, наконец, работы STW (Mark Termination).

После завершения восстановления программа снова заработает на полную мощность. Вы можете увидеть много розовых вертикальных полос под этими горутинами.

Рисунок 1.8

На рис. 1.8 показано, что розовая линия представляет собой GOROUTINE для заданий Sweeping, а не собственную процедуру. В эти моменты горутина попытается присвоить новые значения в куче.

Рисунок 1.9

На рис. 1.9 показаны данные трассировки горутины в конце действия Sweeping.runtime.mallocgcВызов пойдет на выделение нового значения в куче.runtime.(*mcache).nextFreeВызов приводит к подметанию. Как только в куче больше нет выделенной памяти для освобождения,nextFreeбольше не увидит.

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

GC percentage

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

В качестве примера представьте, что высвобождение завершается и помечает 2 МБ используемой памяти кучи.

Примечание. Память кучи на графике не отражает реальную ситуацию. Память кучи в go обычно беспорядочна и фрагментирована, и у вас не будет этого четкого различия на графике. Эти диаграммы обеспечивают удобную визуализацию модели памяти кучи для легкого понимания.

Рисунок 1.10

На рис. 1.10 показано, что после последней коллекции используется 2 МБ динамической памяти. Поскольку для параметра GC Percentage установлено значение 100 %, следующий запуск сбора необходимо начать, когда или до того, как память кучи увеличится на 2 МБ или более.

Рисунок 1.11

Рисунок 1.11 показывает 2 МБ или более воспоминания. Это вызывает переработку. Один из способов увидеть эти поведения - генерировать трассировку GC для каждого GC.

L2

GODEBUG=gctrace=1 ./app

gc 1405 @6.068s 11%: 0.058+1.2+0.083 ms clock, 0.70+2.5/1.5/0+0.99 ms cpu, 7->11->6 MB, 10 MB goal, 12 P

gc 1406 @6.070s 11%: 0.051+1.8+0.076 ms clock, 0.61+2.0/2.5/0+0.91 ms cpu, 8->11->6 MB, 13 MB goal, 12 P

gc 1407 @6.073s 11%: 0.052+1.8+0.20 ms clock, 0.62+1.5/2.2/0+2.4 ms cpu, 8->14->8 MB, 13 MB goal, 12 P

L2 показывает, как использоватьGODEBUGпеременная для создания трассировки сборщика мусора. L3 ниже показывает трассировки gc, сгенерированные программой.

L3

gc 1405 @6.068s 11%: 0.058+1.2+0.083 ms clock, 0.70+2.5/1.5/0+0.99 ms cpu, 7->11->6 MB, 10 MB goal, 12 P

// General
gc 1404     : The 1404 GC run since the program started
@6.068s     : Six seconds since the program started
11%         : Eleven percent of the available CPU so far has been spent in GC

// Wall-Clock
0.058ms     : STW        : Mark Start       - Write Barrier on
1.2ms       : Concurrent : Marking
0.083ms     : STW        : Mark Termination - Write Barrier off and clean up

// CPU Time
0.70ms      : STW        : Mark Start
2.5ms       : Concurrent : Mark - Assist Time (GC performed in line with allocation)
1.5ms       : Concurrent : Mark - Background GC time
0ms         : Concurrent : Mark - Idle GC time
0.99ms      : STW        : Mark Term

// Memory
7MB         : Heap memory in-use before the Marking started
11MB        : Heap memory in-use after the Marking finished
6MB         : Heap memory marked as live after the Marking finished
10MB        : Collection goal for heap memory in-use after Marking finished

// Threads
12P         : Number of logical processors or threads used to run Goroutines

L3 показывает фактическое значение в GC и его значение. Я доберусь до этих значений в конце, а пока обратите внимание на фрагмент памяти трассировки 1405 GC.

Рисунок 1.12

L4

// Memory
7MB         : Heap memory in-use before the Marking started
11MB        : Heap memory in-use after the Marking finished
6MB         : Heap memory marked as live after the Marking finished
10MB        : Collection goal for heap memory in-use after Marking finished

Строка трассировки GC дает следующую информацию: Используемый размер динамической памяти до начала работы по маркировке составляет 7 МБ. Когда работа по маркировке завершена, используемый размер динамической памяти составляет 11 МБ. Это означает дополнительные 4 МБ выделения памяти в рекламации. Размер живого пространства в динамической памяти после завершения работы по маркировке составляет 6 МБ. Это означает, что используемая память кучи может быть увеличена до 12 МБ до начала следующей коллекции (100% * размер активной памяти кучи = 6 МБ).

Вы можете видеть, что сборщик превысил свое целевое значение на 1 МБ, а используемая память кучи после работы по маркировке составляет 11 МБ вместо 10 МБ. Но это не имеет значения, потому что цель рассчитывается на основе текущей используемой памяти кучи, то есть пространства, помеченного как оставшееся в памяти кучи. время. В этом случае приложение сделало что-то, что привело к использованию большего объема памяти кучи, чем ожидалось, после маркировки.

Если вы посмотрите на трассировку GC (1406), вы видите вещи на встрече в течение 2 мм. Как измениться.

Рисунок 1.13.

L5

gc 1406 @6.070s 11%: 0.051+1.8+0.076 ms clock, 0.61+2.0/2.5/0+0.91 ms cpu, 8->11->6 MB, 13 MB goal, 12 P

// Memory
8MB         : Heap memory in-use before the Marking started
11MB        : Heap memory in-use after the Marking finished
6MB         : Heap memory marked as live after the Marking finished
13MB        : Collection goal for heap memory in-use after Marking finished

L5 показывает состояние, в котором эта коллекция началась через 2 мс после начала предыдущей коллекции (6,068 с против 6,070 с), хотя используемая куча памяти достигла только 8 МБ из разрешенных 12 МБ. Обратите внимание, что если коллекционер решит, что лучше начать сбор раньше, он так и сделает. В этом случае он мог начать сбор раньше, потому что приложение находилось под сильным давлением выделения, и сборщик хотел уменьшить задержку Mark Assist во время этого сбора.

Есть еще две вещи, на которые следует обратить внимание: коллекционер достигает поставленных целей. После завершения маркировки используемое пространство кучи составляет 11 МБ вместо 13 МБ, что на 2 МБ меньше. Пространство, помеченное как активное в памяти кучи после маркировки, также составляет 6 МБ.

Кроме того, вы можете получить дополнительные сведения о сборщике мусора, добавивgcpacertrace=1отметка. Это заставит сборщик распечатать внутреннее состояние параллельного кардиостимулятора.

L6

$ export GODEBUG=gctrace=1,gcpacertrace=1 ./app

Sample output:
gc 5 @0.071s 0%: 0.018+0.46+0.071 ms clock, 0.14+0/0.38/0.14+0.56 ms cpu, 29->29->29 MB, 30 MB goal, 8 P

pacer: sweep done at heap size 29MB; allocated 0MB of spans; swept 3752 pages at +6.183550e-004 pages/byte

pacer: assist ratio=+1.232155e+000 (scan 1 MB in 70->71 MB) workers=2+0

pacer: H_m_prev=30488736 h_t=+2.334071e-001 H_T=37605024 h_a=+1.409842e+000 H_a=73473040 h_g=+1.000000e+000 H_g=60977472 u_a=+2.500000e-001 u_g=+2.500000e-001 W_a=308200 goalΔ=+7.665929e-001 actualΔ=+1.176435e+000 u_a/u_g=+1.000000e+000

Выполнение трассировки сборщика мусора может многое рассказать о работоспособности вашего приложения и скорости сборщика.

Pacing

Сборщик имеет алгоритм синхронизации, который определяет, когда должен начаться сбор. Алгоритм основан на цикле обратной связи, который сборщик использует для сбора информации о времени выполнения приложения и нагрузке, оказываемой приложением на кучу. Стресс можно определить как скорость, с которой приложение выделяет память в куче за заданный промежуток времени. Давление определяет, насколько быстро работает рециклер.

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

Существует заблуждение, что замедление сборщика — это способ повысить производительность. Если вы можете отложить следующую коллекцию, вы отсрочите задержку, которую она создает. Но на самом деле повышение производительности заключается не в снижении скорости коллектора.

Вы можете изменить значение GC Percentage, установив его больше 100. Это увеличивает объем памяти, который может быть выделен до начала следующей коллекции. Это замедляет коллектор. Но не думайте об этом.

Рисунок 1.14

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

Попытка непосредственно повлиять на скорость сбора, не улучшает производительность коллектора. Важно сделать больше между коллекциями или между коллекциями, которые вы можете влиять, уменьшая количество распределения кучи работы.

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

Рисунок 1.15

На рис. 1.15 показаны некоторые статистические данные внутри программы go. Синяя версия статистики показывает, что приложение обрабатывает 10 000 запросов без какой-либо оптимизации. Зеленая версия показывает статистику после того, как непроизводительное выделение памяти 4,48 ГБ было обнаружено и удалено из приложения по тому же запросу 10 КБ (уменьшая нагрузку на выделение памяти в куче).

Взгляните на среднюю скорость перезарядки двух версий (2,08 мс против 1,96 мс). Они почти одинаковые, может быть, 2 мс. Разница заключается в объеме работы, которую две версии выполняют между каждой коллекцией. Между каждым сбором заявки количество обработанных запросов менялось с 3,98 до 7,13 раза. Видно, что почти за то же время рабочая нагрузка увеличилась на 79,1%. Видно, что работа восстановления не замедляется с уменьшением выделенной памяти, а сохраняет первоначальную скорость. Суть успеха в том, чтобы делать больше между каждой коллекцией.

Регулировка скорости сбора сборщика и отсрочка затрат на задержку — это не то, как вы повышаете производительность вашего приложения, это просто сокращает время, необходимое для работы сборщика, что, в свою очередь, снижает стоимость задержки. Штраф за задержку, связанный со сборщиком, уже был объяснен, но вот краткое изложение.

Коллекционер Цена задержки

Есть два типа задержек для каждой работы утилизации. Первый - воровство CPU, что означает, что ваше приложение не работает на полном процессоре при переработке. Приложение Goroutines теперь разделяет P с коллектором Goroutines или выполнять Mark Assist.

Рисунок 1.16

На рис. 1.16 показано, что только 75 % ЦП выполняет работу приложений. Поскольку коллекционер берет P.

Рисунок 1.17

На рис. 1.17 только половина ЦП обрабатывает работу приложений. Поскольку P3 выполняет Mark Assist, P1 занят коллектором.

Примечание. Для маркировки обычно требуется динамическая куча размером 4 миллисекунды ЦП/МБ (например, чтобы оценить, сколько миллисекунд выполняется фаза маркировки, это значение равно размеру динамической кучи в МБ, деленному на 0,25*количество ЦП). На самом деле маркировка работает со скоростью около 1 МБ/мс, но обрабатывается только 1/4 процессорного времени.

Второй тип задержки — STW, возникающий в коллекциях. STW — это когда горутины не работают. Все приложение по существу остановлено.

Рисунок 1.18

На рис. 1.18 показано, что все горутины останавливаются во время STW. STW встречается дважды за коллекцию. Если ваше приложение работоспособно, сборщик будет поддерживать время STW менее 100 микросекунд.

Уменьшить задержку сборщика мусора

Чтобы уменьшить задержку GC, нужно определить, какие выделения памяти в вашем приложении являются ненужными, и удалить их, что помогает улучшить сборщик несколькими способами.

Помогите переработчику:

  • минимальная куча поддерживается
  • Найдите лучшую постоянную скорость
  • Каждая коллекция остается в рамках цели
  • Минимизация времени переработки, STW и Mark Assist

Все это помогает уменьшить задержку сборщика, тем самым увеличивая пропускную способность и производительность приложения. Изменение скорости рециркуляции мало что дает. Вы можете повысить производительность, приняв правильные инженерные решения, чтобы уменьшить нагрузку на динамическую память.

Понимание рабочей нагрузки, выполняемой приложением

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

в заключении

Если вы потратите время на сокращение выделения памяти, вы получите прирост производительности. Но вы не можете писать программы с нулевым выделением памяти, поэтому важно понимать и подтверждать продуктивные (полезные для программы) выделения памяти, а не продуктивные (наносящие ущерб производительности) выделения. Затем вы можете доверять сборщику мусора, который поможет вам поддерживать работоспособность и стабильность памяти, а затем поддерживать работу вашей программы.

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

Оригинальная ссылка:Woohoo. AR, но labs.com/blog/2018/1…