Как выявить утечку памяти в Go-микросервисе?

Ответ

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

1. Профилирование с помощью pprof (Основной метод)

pprof — это встроенный в Go пакет для профилирования CPU, памяти, горутин и многого другого. Это самый мощный инструмент для поиска утечек.

Шаг 1: Интеграция pprof в сервис Достаточно импортировать пакет net/http/pprof и запустить HTTP-сервер. Обычно это делается в отдельной горутине.

import (
    "log"
    "net/http"
    _ "net/http/pprof" // Важно: анонимный импорт для регистрации хендлеров
)

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

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

    // ... запуск основного сервера
}

Шаг 2: Анализ Heap Profile (профиля кучи) Утечка памяти — это рост используемой памяти с течением времени. Чтобы ее найти, нужно сравнить два снимка состояния памяти (heap profile), сделанные в разное время.

  1. Сделайте первый снимок, когда сервис поработал некоторое время:
    # Сохраняем профиль в файл base.heap
    go tool pprof -seconds 30 http://localhost:6060/debug/pprof/heap > /dev/null
    mv pprof_heap_*.pb.gz base.heap
  2. Подождите (например, 5-10 минут), пока сервис продолжает работать под нагрузкой.
  3. Сделайте второй снимок:
    # Сохраняем новый профиль в файл current.heap
    go tool pprof -seconds 30 http://localhost:6060/debug/pprof/heap > /dev/null
    mv pprof_heap_*.pb.gz current.heap
  4. Сравните два профиля, чтобы увидеть, какие объекты накопились в памяти:
    # Запускаем pprof в интерактивном режиме, сравнивая current.heap с base.heap
    go tool pprof -http=:8081 -base base.heap current.heap

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

2. Другие подходы и инструменты

  • Мониторинг через runtime.ReadMemStats Можно периодически считывать метрики памяти и отправлять их в систему мониторинга (например, Prometheus). Это не покажет где утечка, но покажет что она есть.

    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    // m.Alloc - байт выделено и не освобождено
    // m.HeapObjects - количество объектов в куче
    log.Printf("Alloc = %v MiB, HeapObjects = %v", m.Alloc / 1024 / 1024, m.HeapObjects)
  • Нагрузочное тестирование Используйте инструменты вроде k6 или vegeta, чтобы симулировать нагрузку на сервис, и одновременно наблюдайте за графиками потребления памяти. Резкий и неостанавливающийся рост — явный признак утечки.

Основные причины утечек памяти в Go:

  • Утечки горутин (Goroutine Leaks): Горутина блокируется на канале и никогда не завершается, а стек и связанные с ней переменные остаются в памяти.
  • Глобальные коллекции: Карты или слайсы, объявленные в глобальной области видимости, в которые данные только добавляются, но никогда не удаляются.
  • Незакрытые ресурсы: Забыли вызвать Close() у файловых дескрипторов, сетевых соединений или строк (rows) при работе с БД.
  • Срезы (slices) без копирования: Если вы делаете срез из очень большого массива (smallSlice := bigSlice[0:10]), smallSlice будет удерживать в памяти весь исходный bigSlice.