Как обнаружить утечку горутин (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().