После глубокой рекурсии память, занятая стеками горутин, долго не уменьшается. Каким механизмом Go решает, когда уменьшать выросший стек?
Go уменьшает выросший стек не после каждого возврата из функции, а во время подходящей сборки мусора. Если фактически используемая часть стека значительно меньше его текущей ёмкости, рантайм переносит активные кадры в меньший стек и освобождает старое хранилище. Поэтому после глубокой рекурсии память может сохраняться до следующего GC, а сам запуск сборки не гарантирует немедленного уменьшения RSS процесса.
Стек горутины должен быть достаточно гибким: фиксированный большой стек расточителен для миллионов горутин, а слишком маленький ограничивает глубину вызовов. Поэтому Go использует динамические стеки, которые увеличиваются по мере необходимости и уменьшаются, когда прежний пиковый размер больше не нужен.
Современный стек горутины является непрерывным участком памяти. При расширении его содержимое копируется в новый участок, а ссылки на объекты и указатели внутри стека корректируются с учётом нового расположения.
Горутина может кратковременно выполнить глубокую рекурсию, обработать большой вложенный документ или пройти длинную цепочку вызовов. После возврата к обычной работе активных данных мало, но ёмкость стека остаётся рассчитанной на прежний пик.
Немедленное уменьшение стека после каждого возврата было бы дорогим: оно добавляло бы копирование и проверки на горячий путь. С другой стороны, полное отсутствие shrink-механизма приводило бы к накоплению неиспользуемой памяти у долгоживущих горутин.
Go проверяет возможность уменьшения стека в процессе обработки стеков сборщиком мусора. Рантайм оценивает, насколько занята текущая ёмкость; если стек существенно недоиспользуется и находится в состоянии, допускающем операцию, его содержимое копируется в меньший стек.
Это не освобождает отдельные локальные переменные по одной. Стек — единая область, содержащая кадры вызовов, параметры, локальные значения и служебные данные. После возврата кадры исчезают логически, но сама зарезервированная ёмкость может сохраняться до проверки shrink-условий.
При копировании рантайм должен сохранить корректность указателей внутри стека и ссылок из стека на объекты кучи. Поэтому операция выполняется в контролируемой точке, когда состояние горутины можно безопасно просканировать и преобразовать.
Уменьшение стека не обязательно происходит на каждом цикле GC. Рантайм применяет эвристики, чтобы не уменьшить стек сразу перед новым расширением. Если горутина снова быстро войдёт в глубокий вызов, чередование shrink и grow создаст лишние копирования и ухудшит производительность.
Стек горутины не следует полностью отождествлять с heap. Он учитывается рантаймом отдельно, но его содержимое сканируется сборщиком, а ссылки из него могут удерживать объекты кучи живыми. Кроме того, освобождённые рантаймом страницы не обязаны немедленно исчезнуть из RSS: память может оставаться в кэше аллокатора или не возвращаться операционной системе сразу.
Принудительный вызов GC иногда помогает запустить проверку стеков, но не является гарантией немедленного уменьшения памяти и обычно не подходит как регулярная оптимизация. Основной критерий — длительность жизни горутины и характер её пикового использования стека.
В сервисе долгоживущие рабочие горутины периодически разбирали сильно вложенные сообщения. После каждого пика heap возвращался к обычному уровню, но память стеков оставалась повышенной.
Рассматривались три варианта:
Выбранным решением обычно становится устранение ненужной глубокой рекурсии и ограничение входной глубины. Если рекурсию нельзя убрать, тяжёлую операцию можно выполнять в отдельной управляемой горутине. Это предпочтительнее регулярного принудительного GC, потому что снижает сам пик потребления, а не пытается компенсировать его постфактум.
Нет. Возврат освобождает кадр логически, но не обязан уменьшать ёмкость стека. Немедленное сжатие было бы слишком дорогим и могло бы вызвать постоянное чередование расширения и уменьшения. Проверка выполняется позднее, обычно в рамках работы сборщика мусора и при наличии достаточного недоиспользования.
Нет. Сборка мусора может создать возможность для shrink, но конкретный стек должен удовлетворять условиям рантайма. Даже после уменьшения стека освобождённая память может остаться внутри аллокатора Go или быть не сразу возвращена операционной системе, поэтому RSS способен почти не измениться.
Сборщик должен сканировать активные стеки, чтобы найти ссылки на объекты кучи. Большая ёмкость не означает, что весь стек содержит полезные живые значения, но высокий фактический объём активных кадров увеличивает объём работы сканирования. Поэтому глубокие стеки могут повышать стоимость маркировки, а их последующее уменьшение сокращает будущую стоимость только после успешного shrink.