Как обнаружить утечку горутин (goroutine leak) и связанные с ней утечки памяти?

Ответ

Утечка горутин — это ситуация, когда горутина остается заблокированной навсегда и никогда не завершается. Это приводит к утечке её стека и других ресурсов. Для обнаружения таких проблем используется встроенный в Go инструмент для профилирования — pprof.

Шаг 1: Инструментирование кода

Добавьте в ваше приложение HTTP-сервер с эндпоинтами pprof. Обычно это делается в main.go.

import (
    _ "net/http/pprof" // Анонимный импорт для регистрации обработчиков
    "net/http"
    "log"
)

func main() {
    // ... ваш основной код ...

    // Запускаем pprof сервер в отдельной горутине
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    // ... ваш основной код ...
}

Шаг 2: Анализ утечки горутин

  1. Запустите приложение.
  2. Когда вы подозреваете, что произошла утечка, выполните в терминале команду:
    go tool pprof http://localhost:6060/debug/pprof/goroutine
  3. В интерактивной консоли pprof введите команду top. Она покажет группы горутин с одинаковым стеком вызовов. Если вы видите большое и постоянно растущее число горутин, заблокированных в одном и том же месте, — это главный кандидат на утечку.
  4. Команда traces покажет полные стеки вызовов для всех горутин, что поможет точно определить место блокировки.

Шаг 3: Анализ утечки памяти

Утекающие горутины часто удерживают память. Для анализа памяти используется профиль heap.

go tool pprof http://localhost:6060/debug/pprof/heap

С помощью команд top, list <имя_функции> и web можно найти объекты, которые занимают больше всего памяти, и код, который их создает.

Типичные причины утечек горутин:

  • Незакрытые каналы: Горутина ждет данных из канала, в который больше никто никогда не запишет.
  • Заблокированные каналы: Отправка в канал, из которого никто не читает (если канал небуферизованный или переполнен).
  • Бесконечные циклы: Циклы без условия выхода или ожидания отмены.
  • Отсутствие обработки отмены в select: Горутина блокируется на канале, игнорируя при этом ctx.Done().

Ответ 18+ 🔞

А, ну это же классика, ёпта! Ситуация, когда твои горутины разбегаются, как тараканы от света, и никак не хотят заканчиваться. Утечка горутин, блядь! Они остаются висеть, заблокированные навечно, и жрут память, как не в себя. К счастью, в Go есть встроенный стукач — pprof, который всё про них расскажет.

Шаг 1: Вставляем прослушку в код

Надо в твой main.go воткнуть небольшой HTTP-сервер для шпионажа. Делается это просто, как два пальца.

import (
    _ "net/http/pprof" // Подключаем стукача анонимно, чтобы он сам зарегистрировался
    "net/http"
    "log"
)

func main() {
    // ... тут твой основной код, который, возможно, течёт ...

    // Запускаем сервер для стукачей в отдельной горутинке, чтобы не мешал
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    // ... и тут твой код продолжается ...
}

Шаг 2: Ловим сбежавших горутин

  1. Запускаешь своё приложение.
  2. Когда чувствуешь, что что-то пошло не так и горутин стало овердохуища, открываешь терминал и пишешь:
    go tool pprof http://localhost:6060/debug/pprof/goroutine
  3. Попадёшь в консоль pprof. Там команда top покажет тебе кучки горутин, которые застряли в одинаковых местах. Если видишь, что одна и та же пачка растёт, как на дрожжах — вот он, корень зла, блядь!
  4. Команда traces вывалит тебе полные стеки вызовов всех этих несчастных, и ты точно увидишь, на каком хуёвисе они повисли.

Шаг 3: А память-то тоже течёт!

Сбежавшие горутины редко ходят налегке — они тащат за собой память. Чтобы это проверить, дергаем профиль кучи.

go tool pprof http://localhost:6060/debug/pprof/heap

Тут команды top, list <имя_функции> и даже web (если графику любишь) помогут найти, кто же там такие здоровые объекты в памяти оставляет и какой код их порождает.

А из-за чего они вообще сбегают, эти твари?

  • Каналы, которые никто не закрыл: Представь, горутина сидит и ждёт у моря погоды, то есть данных из канала, а ты уже давно про неё забыл и никогда туда ничего не напишешь. Вечное ожидание, пиздец.
  • Каналы, которые забили наглухо: Ты пытаешься запихнуть данные в канал, а оттуда никто не берёт (если он небуферизованный или буфер забит под завязку). Встал в очередь и стоишь до скончания времён.
  • Циклы, которым похуй на всё: Бесконечный цикл без возможности выйти или хотя бы получить сигнал отмены. Крутится, как белка в колесе, и конца-края не видно.
  • select, который игнорирует отмену: Горутина тупо заблокировалась на чтении из канала, а на сигнал ctx.Done() ей насрать. Ну, тупая, что с неё взять.