GOGC=off. Часть 1, GOMEMLIMITКороткий ответ: да, но не так, как обычно себе это представляют. Полностью выключенный сборщик мусора почти всегда означает, что программа рано или поздно съест всю память. Работающие варианты сводятся к тому, что GC не выключается совсем, а перестаёт запускаться по росту кучи и начинает запускаться по вашим правилам.
В этой части разберём, что делает
GOGC=off, почему наивный вариант падает, и самый практичный способ жить без сборок, связку с GOMEMLIMIT.➡️ Что делает GOGC
GOGC задаёт, насколько куча должна вырасти относительно живых данных, прежде чем начнётся следующий цикл сборки. При значении по умолчанию 100 сборка стартует, когда куча удвоилась относительно объёма живых объектов после прошлого цикла. При GOGC=200 цикл будет реже и памяти уйдёт больше. При GOGC=off триггер по росту кучи пропадает совсем.То же самое можно сделать из кода:
import "runtime/debug"
func main() {
debug.SetGCPercent(-1) // эквивалент GOGC=off
// ...
}
Важно понимать, что аллокатор при этом никуда не девается. Программа продолжает выделять память, просто освобождать её никто не будет. Каждый временный слайс, каждая строка от
fmt.Sprintf, каждый заголовок HTTP запроса остаются в куче навсегда. Для батча, который живёт три секунды и завершается, это идеально. Для сервиса, который работает неделями, это утечка по определению.➡️ Почему наивный вариант падает
Возьмём типичный HTTP сервис. Даже если ваш собственный код не аллоцирует, стандартная библиотека аллоцирует за вас.
net/http создаёт объекты на каждый запрос, encoding/json создаёт их на каждый декод, драйвер базы создаёт их на каждый ряд. При тысяче запросов в секунду и паре килобайт мусора на запрос вы получаете около двух мегабайт в секунду, то есть примерно семь гигабайт в час. Дальше приходит OOM killer.Поэтому вопрос не в том, можно ли выключить GC, а в том, чем заменить триггер, который вы выключили.
➡️ GOMEMLIMIT как замена триггера
GOMEMLIMIT — самый практичный подход и он появился в Go 1.19. Комбинация
GOGC=off и GOMEMLIMIT даёт сборщик, который не реагирует на рост кучи вообще, но запускается, когда потребление памяти приближается к заданному лимиту.GOGC=off GOMEMLIMIT=4GiB ./myservice
Или из кода:
debug.SetGCPercent(-1)
debug.SetMemoryLimit(4 << 30) // 4 GiB
Что вы получаете. Пока памяти хватает, циклов сборки нет совсем, а значит нет накладных расходов на маркировку и барьеры записи. Как только программа подходит к лимиту, GC включается и удерживает потребление около него. Куча живёт в стабильном состоянии, а не пилой от нуля до удвоения.
Что вы теряете. Если объём живых данных сам по себе дорастает до лимита, GC начнёт запускаться непрерывно и программа уйдёт в смерть от сборки, когда почти всё время уходит на бесполезные циклы. Лимит поэтому ставят с запасом относительно ожидаемого объёма живых данных, а не впритык к памяти контейнера. Разумная практика ставить
GOMEMLIMIT процентов на десять ниже лимита пода, чтобы GC успел среагировать раньше, чем это сделает cgroup.Ещё одна тонкость.
GOMEMLIMIT учитывает всю память рантайма, включая стеки горутин и служебные структуры, но не учитывает то, что выделено вне рантайма, например через cgo или mmap. Если такого много, лимит надо занижать дополнительно.➡️ Про ballast
Раньше был популярен трюк с большим фиктивным слайсом, который выделяли на старте, чтобы поднять порог срабатывания GC. Сейчас он не нужен.
GOMEMLIMIT решает ту же задачу.Итог первой части
GOGC=off сам по себе это не оптимизация, а отложенный OOM. Но GOGC=off вместе с GOMEMLIMIT вполне живёт в проде годами. Сборщик молчит на нормальной нагрузке и вступает в дело только у границы, которую вы задали сами.Во второй части посмотрим на два более жёстких варианта. Ручные вызовы
runtime.GC() в окнах простоя и код, который вообще не аллоцирует после старта.📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoDeep