Как отслеживать утечки горутин в Go приложении
Что проверяют
Понимаете ли вы, что горутина утекает, когда навсегда блокируется и её некому разбудить, и умеете ли найти такие горутины на живом сервисе и в тестах. Следом обычно просят показать код с утечкой и исправить его.
Как отвечать
Сначала типичные причины:
- отправка без получателя: результат пишется в небуферизованный канал, вызывающий ушёл по таймауту, и отправитель висит вечно. Лечится буфером на один элемент;
- чтение из канала, который никто не закроет:
for v := range chбезclose(ch)у производителя; - нет выхода по отмене: цикл с
selectбез ветки<-ctx.Done(); - незакрытое тело ответа: без
resp.Body.Close()в HTTP-клиенте остаются висеть горутины транспорта.
Как обнаружить:
- метрика:
runtime.NumGoroutine()или готоваяgo_goroutinesиз клиента Prometheus. Рост без плато под стабильной нагрузкой это сигнал; - профиль горутин: подключаете
net/http/pprofи открываете/debug/pprof/goroutine?debug=1, там горутины сгруппированы по стеку с количеством. Тысячи одинаковых стеков наchan sendсразу показывают место. Сdebug=2видно, сколько минут каждая горутина ждёт; - сравнение:
go tool pprof -base old.pb.gz new.pb.gzпоказывает, какие стеки прибавились между двумя снимками; - тесты: библиотека
goleak, вызовgoleak.VerifyNone(t)в конце теста илиgoleak.VerifyTestMain(m)для всего пакета падает, если остались лишние горутины.
Профилактика: у каждой горутины есть владелец и понятный момент завершения, контекст передаётся первым аргументом, группы задач запускаются через errgroup с контекстом.
Где спотыкаются
Считают утечкой только бесконечный цикл. Чаще горутина не крутится, а спит на канале и не тратит процессор, поэтому по CPU её не видно, только по памяти и количеству.
Открывают pprof на публичном порту. Обработчики регистрируются в DefaultServeMux, поэтому их выносят на отдельный внутренний порт.
Путают ожидание результата с завершением: <-done в main не спасает, если горутина внутри запустила ещё одну и не дождалась её.