← Назад к вопросам

Как узнать, какая горутина дает утечку памяти?

2.0 Middle🔥 241 комментариев
#Конкурентность и горутины#Основы Go

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Общие принципы диагностики утечек памяти в горутинах

Утечка памяти в горутинах (goroutines) обычно означает, что горутины создаются, но не завершаются, оставаясь в памяти вместе со своими связанными данными. Это приводит к росту потребления памяти и потенциальному краху программы. Диагностика такой проблемы требует системного подхода, сочетающего анализ логики программы, инструменты профилирования и мониторинг состояния горутин.

Основные причины утечек через горутины

  1. Незавершенные горутины в бесконечных циклах или блокирующих операциях: Горутина запускается, но никогда не достигает точки завершения (например, из-за отсутствия условия выхода из цикла или бесконечного ожидания канала).
  2. Некорректное использование каналов (channels): Горутина блокируется на отправке (send) или получении (receive) из канала, который никогда не будет готов, или забывает закрыть канал, что может заблокировать другие горутины.
  3. Утечка контекста (context): Создание горутин, зависящих от context.Context, который никогда не завершается (например, context.WithCancel() без вызова cancel()).
  4. "Зомби"-горутины: Горутины, которые технически работают, но не выполняют полезной работы и не освобождают ресурсы.

Методы и инструменты для диагностики

Для поиска утечек памяти, связанных с горутинами, используются следующие инструменты и подходы:

1. Профилирование памяти и горутин с помощью pprof

Go предоставляет мощный пакет net/http/pprof для профилирования. Добавьте его в HTTP-сервер для сбора данных.

import _ "net/http/pprof"

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

После запуска можно получить различные профили:

  • Профиль горутин (goroutine): http://localhost:6060/debug/pprof/goroutine?debug=1 показывает текущее количество и состояние всех горутин с их stack traces. Анализ стеков помогает найти "застрявшие" горутины.
  • Профиль памяти (heap): http://localhost:6060/debug/pprof/heap?debug=1 показывает распределение памяти. Можно сравнить snapshots в разные моменты времени, чтобы увидеть рост.

Для графического анализа используйте команду go tool pprof:

# Сбор профиля горутин
go tool pprof http://localhost:6060/debug/pprof/goroutine

# Сбор профиля памяти (heap)
go tool pprof http://localhost:6060/debug/pprof/heap

# Сравнение двух snapshots памяти для обнаружения роста
go tool pprof -base old.pprof -top new.pprof http://localhost:6060/debug/pprof/heap

2. Мониторинг количества горутин в реальном времени

Можно отслеживать метрику runtime.NumGoroutine() в логах или через системы мониторинга (Prometheus, Grafana). Постоянный рост или необычно высокое значение — явный сигнал проблемы.

import (
    "runtime"
    "time"
)

func monitorGoroutines() {
    ticker := time.NewTicker(30 * time.Second)
    for {
        select {
        case <-ticker.C:
            log.Printf("Current goroutines: %d\n", runtime.NumGoroutine())
        }
    }
}

3. Анализ логики кода и использование trace

Инструмент go tool trace позволяет анализировать поведение горутин во времени (создание, блокировки, выполнение).

import (
    "os"
    "runtime/trace"
)

func main() {
    f, err := os.Create("trace.out")
    if err != nil {
        panic(err)
    }
    defer f.Close()
    
    trace.Start(f)
    // ... ваш код ...
    trace.Stop()
}

Затем анализируйте trace:

go tool trace trace.out

В веб-интерфейсе можно увидеть:

  • График количества горутин (Goroutines).
  • Информацию о каждой горутине (кто ее создал, на чем она блокирована).
  • Это особенно полезно для обнаружения блокировок (например, на каналах или мьютексах), которые приводят к "застреванию" горутин.

4. Практические шаги для поиска конкретной "виновной" горутины

Шаг 1: Сбор и анализ профиля горутин

Получите текстовый профиль (?debug=1) и исследуйте stack traces. Ищите:

  • Горутины, которые находятся в одном и том же месте (например, в select, channel operation, sync.Wait) — это кандидаты на блокировку.
  • Горутины, созданные в функциях, которые вызываются многократно (например, в обработчике HTTP запроса), но не завершаются.

Шаг 2: Изучение связанного кода

По stack trace определите функцию и участок кода, где "застревает" горутина. Проверьте:

  • Каналы: Убедитесь, что есть соответствующая отправка/получение, и канал может быть закрыт.
  • Циклы: Есть ли условие завершения для for цикла в горутине?
  • Контексты: Если используется context, вызывается ли функция отмены (cancel)?
  • WaitGroup: Все ли вызовы Done() для sync.WaitGroup выполняются?

Шаг 3: Воспроизведение и локализация

Создайте минимальный тест или сценарий, который воспроизводит рост горутин. Используйте дифференциальный (diff) анализ профилей памяти между начальным и пиковым состояниями, чтобы точно определить, какие объекты (и связанные с ними горутины) растут.

Пример диагностики утечки из-за незакрытого канала

Рассмотрим проблемный код:

func leakyGoroutine(ch chan int) {
    for val := range ch { // Эта горутина никогда завершится, если ch не будет закрыт
        process(val)
    }
}

func main() {
    ch := make(chan int)
    go leakyGoroutine(ch)
    // ... но ch никогда не закрывается и не отправляет все данные ...
}

Профиль горутин (pprof) покажет множество горутин (или одну "застрявшую") в состоянии chan receive, с stack trace, ведущим к leakyGoroutine. Анализ кода обнаружит отсутствие close(ch) или недостаточное количество отправок в канал.

Профилактика и лучшие практики

  1. Контролируемое завершение: Используйте context.Context для управления жизненным циклом горутин и обеспечьте механизмы остановки (каналы остановки, таймауты).
  2. Паттерны с пулами горутин: Для задач, создающих множество горутин, используйте пулы (worker pools) для ограничения их числа.
  3. Закрывайте каналы, когда все данные отправлены, особенно в горутинах-потребителях с range.
  4. Мониторинг в production: Внедрите постоянный мониторинг runtime.NumGoroutine() и аллокаций памяти в ваше приложение.

Поиск горутин, вызывающих утечку памяти, — это процесс анализа состояния горутин, распределения памяти и логики кода с помощью инструментов pprof и trace. Ключевой шаг — сравнение snapshots и изучение stack traces для идентификации "застрявших" или бесконечно создаваемых горутин, а затем исправление соответствующей логики управления их жизненным циклом.

Как узнать, какая горутина дает утечку памяти? | PrepBro