Сборка мусора в Go
Один и тот же кусок работы: 169 МиБ выделено, 12,5 МиБ живёт постоянно. При GOGC=100 сборщик отработал 16 раз, при GOGC=400 - дважды. Спрашивают, какой сборщик в Go, что он останавливает и почему в контейнере процесс убивал OOM (out of memory) killer.
Стержень: сборщик конкурентный и трёхцветный, мир останавливает на десятки микросекунд, а ручек ровно две - GOGC и GOMEMLIMIT.
// Формулировки: «какой сборщик в Go?», «что регулирует GOGC?», «почему процесс убил OOM killer?»
Что значит конкурентный
Слово тут не для красоты, его видно в цифрах. На моём прогоне: 16 сборок, суммарная остановка мира 0,88 мс, худшая единичная - 80 микросекунд. Средняя, если поделить: 0,88 / 16 = 55 микросекунд на сборку.
Так выходит, потому что основную работу - обход графа объектов - сборщик делает одновременно с программой. Мир останавливают только на коротких переходах между фазами. Пока идёт обход, за изменениями следит барьер записи: программа перевесила указатель - барьер пометил объект, чтобы живое не сочли мусором.
Трёхцветная разметка это и есть сам обход. Белое ещё не смотрели, серое нашли, но внутрь не заглядывали, чёрное обошли целиком вместе со ссылками. Кончились серые - всё оставшееся белым и есть мусор.
// Память сборщик не уплотняет: объекты остаются по своим адресам, поэтому указатель, который ты держишь, не протухнет. Фрагментацию гасит аллокатор, раскладывая объекты по размерным классам.
- барьер записи
- ловит изменения указателей во время обхода
- трёхцветная разметка
- белое, серое, чёрное - стадии обхода объекта
GOGC: во сколько раз разрешено раздуться
Тот же замер, две настройки. GOGC=100, значение по умолчанию: 16 сборок на 169 МиБ выделенного. GOGC=400: две сборки на те же 169 МиБ. Восьмикратная разница в числе сборок, а худшая пауза не сдвинулась - 80 микросекунд против 69.
Читается ручка так: на сколько процентов куче разрешено вырасти над объёмом живых данных, прежде чем стартует сборка. 100 - значит удвоение: живёт 12,5 МиБ, сборка запускается около 25 МиБ. 400 - значит впятеро: старт около 62 МиБ. Реже сборки, больше памяти в пике, и обмен этот всегда именно такой.
// Значение по умолчанию не надо искать в документации: debug.SetGCPercent(100) возвращает предыдущее значение. У меня вернуло 100.
- GOGC
- прирост кучи над живыми данными до следующей сборки
- цена настройки
- реже сборки - выше пик памяти, и наоборот
GOMEMLIMIT и боль контейнеров
Спросил у рантайма текущий лимит памяти: debug.SetMemoryLimit(-1) вернул 9223372036854775807. Это максимум знакового 64-битного числа, примерно 9,2 эксабайта. Проще говоря, ручка выключена.
Пока она выключена, сборщик считает только от живых данных и про лимит контейнера не знает вообще. Живёт 300 МиБ, GOGC=100 разрешает дорасти до 600, а контейнеру выдали 512 - и процесс сносит OOM killer, хотя мусор уже готов к уборке. GOMEMLIMIT из версии 1.19 задаёт мягкий потолок: подходя к нему, сборщик работает чаще и жертвует процессором ради памяти.
// Мягкий - значит программа не упадёт, даже если живых данных больше лимита. Она уйдёт собирать почти непрерывно, и на графиках это ровное плато памяти при вставшем колом процессоре. Диагноз ставится по паре графиков сразу, по одному памяти его не видно.
- GOMEMLIMIT
- мягкий потолок памяти для рантайма, с 1.19
- мягкий потолок
- не падение при превышении, а непрерывная сборка
Как отвечать: «Какой сборщик мусора в Go и как им управлять?»
Сборщик конкурентный, трёхцветный, с пометкой и подметанием. Основной обход графа объектов идёт одновременно с программой, а за изменениями указателей во время обхода следит барьер записи. Мир останавливают только на коротких переходах между фазами: я замерял на своём прогоне - 16 сборок, суммарная остановка 0,88 мс, худшая единичная 80 микросекунд. Поэтому про Go корректнее говорить не о паузах, а о накладных расходах, размазанных по процессорному времени. Память сборщик не уплотняет, объекты остаются по своим адресам. Ручек две. GOGC задаёт, на сколько процентов куче разрешено вырасти над живыми данными до следующей сборки, по умолчанию сто, то есть удвоение. На том же замере при GOGC=400 сборок стало две вместо шестнадцати, а длина пауз не изменилась - платишь памятью в пике. GOMEMLIMIT из версии 1.19 задаёт мягкий потолок памяти, и он закрыл давнюю боль контейнеров: без него сборщик считал от живых данных и про лимит cgroup не знал, поэтому процесс убивал OOM killer при готовом к уборке мусоре. Потолок именно мягкий: программа не упадёт, но при нехватке будет собирать почти непрерывно, и это видно как всплеск процессора при ровной памяти.
Сильный ответ: назван тип сборщика и роль барьера записи, паузы даны числом с замера, обе ручки объяснены через их цену, а GOMEMLIMIT привязан к конкретной эксплуатационной аварии. Отдельно ценится оговорка про мягкость потолка - её обычно не знают.
На чём валят
- −Рассказывают про «долгие паузы сборщика»: на замере худшая остановка вышла 80 микросекунд.
- −Считают, что сборщик уплотняет память и двигает объекты. Он их не двигает, поэтому адреса стабильны.
- −Крутят GOGC в контейнере, не зная про GOMEMLIMIT, и ловят OOM killer на ровном месте.
- −Принимают невозвращённые системе страницы за утечку: RSS (resident set size) после пика падает отложенно.
- −Уверены, что при живом сборщике утечек не бывает. Бывают, просто они логические.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.
- Какой сборщик мусора в современном Go?A)Конкурентный tricolor mark-sweep с очень короткими паузамиB)Поколенческий (generational), как в JVM, с молодым и старым поколениямиC)Уплотняющий (compacting): перемещает объекты и дефрагментирует кучуD)Stop-the-world на всё время сборки — программа замирает целиком
показать ответ и разбор
+A)Конкурентный tricolor mark-sweep с очень короткими паузами// разбор: GC Go — конкурентный трёхцветный mark-and-sweep: маркировка живых объектов идёт в основном параллельно с работой программы, а stop-the-world паузы сведены к долям миллисекунды (две короткие фазы). Он НЕ поколенческий и НЕ уплотняющий — объекты не двигаются. Расплата за низкие паузы: больше работы CPU и памяти, чем у compacting-GC.
- Что регулирует GOGC (по умолчанию 100) и какой компромисс задаёт?A)Число потоков GC: чем больше, тем быстрее сборка и меньше памятиB)Жёсткий лимит памяти процесса в мегабайтахC)Насколько куча вырастет до следующей сборки: выше — реже GC, но больше RAMD)Приоритет служебной горутины GC в планировщике относительно прочих горутин
показать ответ и разбор
+C)Насколько куча вырастет до следующей сборки: выше — реже GC, но больше RAM// разбор: GOGC задаёт, на сколько процентов куча может вырасти относительно живого объёма после прошлой сборки, прежде чем запустится следующая (100 = удвоение). Больше GOGC — GC реже, выше throughput, но выше пиковая память; меньше — GC чаще, память ниже, но больше CPU на сборку. Это ручка «память против CPU». С 1.19 есть ещё soft memory limit GOMEMLIMIT.
- Что задаёт переменная GOMEMLIMIT, появившаяся в Go 1.19?A)Жёсткий предел, при превышении которого рантайм завершает программуB)Максимальный объём, который рантайм резервирует у системы при стартеC)Размер стека, до которого может вырасти отдельная горутинаD)Мягкий предел памяти, к которому сборщик подстраивает частоту работы
показать ответ и разбор
+D)Мягкий предел памяти, к которому сборщик подстраивает частоту работы// разбор: GOGC управляет сборкой в процентах прироста и не знает, сколько памяти доступно, — в контейнере с жёстким лимитом это приводило к тому, что процесс убивал OOM killer. GOMEMLIMIT задаёт целевой потолок: приближаясь к нему, сборщик работает чаще, жертвуя процессором ради того, чтобы влезть. Предел мягкий: если живых данных больше, рантайм не убьёт программу, но будет собирать мусор почти непрерывно.
- Почему сборщик мусора в Go называют конкурентным и что при этом всё-таки останавливает мир?A)Он работает параллельно с программой, а мир встаёт лишь на короткие фазы вокруг маркировкиB)Он работает в отдельном процессе, поэтому останавливать программу не требуетсяC)Он останавливает только те горутины, которые активно выделяют памятьD)Он полностью останавливает программу, но делает это редко и потому незаметно
показать ответ и разбор
+A)Он работает параллельно с программой, а мир встаёт лишь на короткие фазы вокруг маркировки// разбор: Основная работа — трёхцветная маркировка — идёт одновременно с программой, за корректностью следит барьер записи. Полная остановка нужна лишь на коротких переходах между фазами и измеряется десятками-сотнями микросекунд. Именно поэтому в Go говорят не о «паузе сборки», а о накладных расходах: они размазаны по процессорному времени, а не собраны в один провал.
- Сборщик мусора освобождает неиспользуемые объекты. Значит ли это, что память процесса вернётся операционной системе?A)Да, сразу после каждого цикла сборки мусораB)Да, но только для объектов крупнее одной страницы памяти операционной системыC)Не обязательно: рантайм какое-то время держит освобождённое у себяD)Нет: возврат памяти системе в Go не реализован
показать ответ и разбор
+C)Не обязательно: рантайм какое-то время держит освобождённое у себя// разбор: Освобождённое сперва возвращается во внутренние пулы рантайма — иначе следующая аллокация снова шла бы к ядру. Неиспользуемые страницы отдаются системе отложенно, фоновым процессом. Поэтому RSS процесса после всплеска нагрузки падает не мгновенно, и «утечкой» это не является: смотреть надо на профиль кучи, а не на график памяти из монитора.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.