Реклама
Перетяжка // Коробка 3.0

Go 1.27 находит утечки горутин в работающем сервисе через pprof

Профиль goroutineleak ищет навсегда заблокированные горутины через анализ достижимости, который уже делает сборщик мусора.

Обложка: Go 1.27 находит утечки горутин в работающем сервисе через pprof

Команда Go 2 сентября рассказала в официальном блоге о новом типе профиля goroutineleak, который вошёл в Go 1.27. Он находит горутины, навсегда заблокированные на каналах и примитивах пакета sync, и делает это на работающем продакшен-сервисе, без тестов и без остановки процесса. Профиль доступен через runtime/pprof и через стандартный HTTP-обработчик net/http/pprof по адресу /debug/pprof/goroutineleak.

Для тех, кто держит Go-сервисы в проде, это закрывает старую дыру в инструментах. Обычный goroutine-профиль показывает, сколько горутин сейчас заблокировано, но не отличает штатное ожидание от горутины, которая уже никогда не проснётся. Раньше такие утечки искали руками по стекам или ловили в тестах пакетом goleak; сам релиз Go 1.27 вышел 19 августа, а разбор механизма команда опубликовала только сейчас.

Ключевые выводы
  • Профиль goroutineleak входит в Go 1.27 и доступен через runtime/pprof и net/http/pprof (/debug/pprof/goroutineleak).
  • Ловит блокировки на отправке и приёме в канал, блокирующем select, Mutex, RWMutex, WaitGroup и Cond.
  • Не считает утечкой ожидание файлового и сетевого ввода-вывода, системных вызовов и самодельных спинлоков.
  • По словам команды Go, метод точный и почти не даёт ложных срабатываний; накладные расходы на память названы пренебрежимо малыми, худший случай для одного цикла GC оценён как O(n²).
  • В примере из блога профиль нашёл 116 зависших горутин на одной строке с отправкой в небуферизованный канал.

Что такое утечка горутины и почему её было трудно найти

Утечка горутины по определению из блога Go: горутина заблокирована на операции, условие для продолжения которой больше никогда не наступит. Классический пример: воркеры пишут результаты в небуферизованный канал, а читатель вышел из функции по первой ошибке. Каждый следующий воркер зависает на ch <- result навсегда, и чем дольше живёт процесс, тем больше таких горутин копится, растёт потребление памяти и нагрузка на сборщик мусора.

До Go 1.27 инструментов было три, и ни один не решал задачу для продакшена. goleak проверяет, не остались ли горутины после конкретного теста. Пакет synctest, появившийся в стандартной библиотеке Go 1.25, помогает тестировать конкурентный код с виртуальным временем. Обычный goroutine-профиль показывает заблокированные горутины, но не доказывает, что они не проснутся: всплеск нагрузки выглядит в нём так же, как утечка.

Как профиль отличает утечку от штатной блокировки

Идея, которую описывает автор поста Влад Сайок из команды Go, опирается на работу, которую сборщик мусора и так делает. GC вычисляет достижимость памяти от корней. Команда добавила к этому анализу горутины: горутина считается живой, если она не заблокирована на примитиве конкурентности или если примитив, на котором она ждёт, достижим из какой-нибудь живой горутины. Анализ стартует с незаблокированных горутин и распространяется по доступным им каналам и мьютексам. Всё, что осталось недостижимым после этого обхода, никто уже не разблокирует, значит, это утечка.

Go 1.27 находит утечки горутин в работающем сервисе через pprof_8
Схема изменённого алгоритма маркировки: горутины помечаются живыми вместе с достижимыми примитивами синхронизации. Источник: Go Blog

Команда Go утверждает, что механизм точный и «почти не даёт ложных срабатываний» (перевод редакции). Есть патологический случай: цепочка горутин, где каждая ждёт примитив, доступный только следующей. Для такой «гирлянды» проверка в худшем случае занимает O(n²) шагов за один цикл GC, поэтому в блоге советуют не включать сбор профиля постоянно, а снимать его периодически, например раз в четыре часа. Память на учёт горутин авторы называют пренебрежимо малой, а GC при этом продолжает работать параллельно с пользовательским кодом.

Разработка выросла из совместного исследования Орхусского университета, Университета Вашингтона в Сент-Луисе и Uber; академическую версию представили на конференции ASPLOS 2025.

Что видно в профиле на живом примере

В блоге разобран сервис, который раз в секунду запускает обработку десяти элементов и на пятом получает ошибку. Функция делает ранний return, оставшиеся воркеры зависают на отправке в канал. Программа подключает net/http/pprof и слушает localhost:6060; профиль снимают обычным способом:

			curl -o leak.prof http://localhost:6060/debug/pprof/goroutineleak
go tool pprof leak.prof
		

Через несколько минут работы pprof показывает Total: 116 заблокированных горутин, и все они стоят на одной операции: отправке в канал ch. Исправление для примера тривиальное: сделать канал буферизованным на число воркеров, make(chan result, len(ws)), тогда воркеры допишут результаты и завершатся, даже если читатель ушёл.

Что профиль не ловит

  • Ожидание файлового и сетевого ввода-вывода и системных вызовов утечкой не считается: горутина, зависшая на чтении из сокета, в профиль не попадёт.
  • Самодельные спинлоки и другие пользовательские механизмы блокировки не входят в гарантированный набор.
  • Горутина, которая по замыслу ждёт вечно на достижимом канале (например, фоновый слушатель), утечкой не считается, потому что канал достижим из живого кода.

Кому новый профиль ничего не даст: сервисам, где горутины блокируются в основном на сети, а не на каналах, и проектам, которые ещё не перешли на Go 1.27. Профиль работает только в рантайме этой версии; для старых сборок остаются goleak и ручной разбор стеков.

Как включить у себя

  1. Обновить тулчейн до Go 1.27 (релиз от 19 августа 2026 года, заметки к выпуску) и пересобрать сервис.
  2. Если net/http/pprof уже подключён, новый обработчик появится автоматически по пути /debug/pprof/goroutineleak; отдельной настройки не нужно.
  3. Снимать профиль периодически, а не постоянно: команда Go предлагает интервал вроде четырёх часов из-за квадратичного худшего случая.
  4. Смотреть в первую очередь на места с ранним return при ошибках, таймауты без select с контекстом и воркер-циклы, у которых читатель может уйти раньше писателей.

Схемы GC и полный пример кода лежат в посте команды Go. Из соседних новостей по инфраструктуре Go-сервисов на сайте есть разбор Kubernetes 1.37 и заметка о tailcat от Tailscale, написанном на Go.

Источники: Go Blog: Goroutine leak profiles, Go 1.27 Release Notes, Go Blog: Go 1.27 is released

Изображение на обложке: Go Authors, логотип Go