Рассмотрите ситуацию: сразу после цикла GC отдельные аллокации периодически дают задержки, хотя куча не переполнена. Как отложенная очистка span объясняет это поведение?
После маркировки и завершения цикла GC часть span может ещё содержать неосвобождённые слоты. Go выполняет их очистку лениво: когда аллокатору нужен свободный span, он может сначала выполнить sweep его страниц. Поэтому отдельные аллокации получают дополнительную работу и дают всплески задержек даже без роста объёма живой памяти.
Это не означает, что GC «завис» или что память обязательно закончилась. Задержка возникает из-за переноса части работы по очистке с фонового сборщика на путь аллокации.
Go использует конкурентный, в основном неперемещающий сборщик мусора и многоклассовый аллокатор. Полная синхронная очистка всей кучи после каждого цикла увеличивала бы паузы и заставляла приложение ждать завершения обработки всех span.
Ленивая очистка позволяет распределять работу во времени. Фоновый sweeper очищает span, когда это возможно, а аллокатор при необходимости доводит очистку конкретного span до конца самостоятельно.
Пусть сборщик уже определил недостижимые объекты, но свободные слоты в соответствующих span ещё не подготовлены для повторного использования. Следующая аллокация может не найти подходящий очищенный span в локальном кэше текущего P.
Тогда путь аллокации становится менее предсказуемым: вместо быстрого получения готового слота требуется найти span и очистить его. При высокой частоте аллокаций это может проявиться в p95 или p99 задержках, хотя среднее потребление памяти и число живых объектов остаются почти неизменными.
Sweep — это обработка span после маркировки: аллокатор по данным маркировки определяет, какие слоты заняты живыми объектами, а какие можно вернуть в свободное состояние. Для повторного использования span должен быть помечен как очищенный.
Очистка может выполняться фоновым sweeper-механизмом. Однако она не обязана завершить обработку всех span до того, как приложение продолжит интенсивно аллоцировать. Если локальный кэш аллокатора исчерпан, поток приложения может выполнить sweep assist — самостоятельно очистить необходимую часть span и только затем получить свободные слоты.
Это отличается от mark assist. Mark assist помогает с маркировкой во время фазы mark, а sweep assist выполняет последующую подготовку span к повторному использованию. В обоих случаях часть работы GC попадает на путь пользовательской операции, но причины и фазы различаются.
Стоимость зависит не только от размера живой кучи. На неё влияют количество span, требующих очистки, распределение объектов по размерным классам, скорость аллокаций и то, успевает ли фоновый sweeper работать параллельно с приложением.
Для диагностики полезно сопоставлять задержки аллокаций с фазами GC, профилем CPU и метриками пауз. Нельзя автоматически устранять проблему увеличением heap: больший heap может уменьшить частоту циклов, но одновременно увеличить объём работы, который предстоит обработать при последующем sweep.
Практические меры зависят от причины: сокращение лишних аллокаций уменьшает давление на аллокатор, выравнивание всплесков нагрузки даёт sweeper больше времени, а изменение настроек GC может поменять баланс между памятью и CPU. Принудительный вызов GC обычно не является решением: он может добавить синхронную работу и ухудшить задержки.
В сервисе обработки сообщений после каждого крупного всплеска нагрузки p99 задержки отдельных запросов возрастал. Живая куча оставалась примерно постоянной, поэтому первоначально подозревали сетевые операции и блокировки.
Рассматривались два варианта. Увеличение целевого размера кучи уменьшало частоту циклов GC, но повышало пиковое потребление памяти и не устраняло задержки после редких крупных всплесков. Принудительный GC после завершения пачки сообщений заранее выполнял часть работы, но добавлял заметные паузы в критический путь.
Выбранным решением стало уменьшение числа временных объектов в обработчике и ограничение размера пачки. Это снизило скорость аллокаций, позволило фоновой очистке успевать за приложением и уменьшило потребность в sweep assist. В результате p99 улучшился без существенного увеличения лимита памяти.
Обязательно ли sweep полностью завершён к моменту окончания GC-цикла?
Нет. Фазы маркировки и финальной синхронизации не означают, что каждый span уже очищен и готов к немедленному повторному использованию. Часть sweep может продолжаться конкурентно или выполняться по требованию аллокатора.
Может ли sweep assist увеличить задержку при почти неизменной живой памяти?
Да. Живая память показывает, сколько объектов осталось достижимыми, но не показывает, сколько span ещё нужно обработать и насколько быстро приложение запрашивает новые слоты. При большом потоке аллокаций приложение может столкнуться с незавершённой очисткой даже при стабильном размере живой кучи.
Устраняет ли предварительное освобождение ссылок проблему sweep assist?
Не обязательно. Обнуление ссылок может сделать объекты недостижимыми для следующей маркировки, но само по себе не превращает их слоты в готовые свободные слоты span. Для этого требуется соответствующий цикл GC и последующая обработка sweep; кроме того, преждевременное обнуление может изменить логику программы и не должно применяться только ради оптимизации.