Исследуем утечки памяти в Go с помощью pprof | OTUS

Исследуем утечки памяти в Go с помощью pprof

Утечки памяти в Go способны принимать разные формы. Как правило, мы считаем, что это баги, однако истинная причина проблем может быть заложена ещё на стадии проектирования. Рассмотрим распространённые примеры появления проблем с памятью: • неверное представление данных; • интенсивное применение рефлексии либо строк; • применение глобальных переменных; • бесконечные горутины.

Помочь найти проблемы с памятью в Golang может инструмент под названием pprof. Также он позволяет находить проблемы в работе процессора.

Инструмент pprof создаёт файл дампа, куда кладёт сэмпл кучи. Данный файл вы сможете в итоге проанализировать/визуализировать, чтобы позволит получить карту: • текущего выделения памяти; • накопительного (общего) выделения памяти.

Кроме того, у pprof есть возможность сравнивать снимки, которые сделаны в разное время. Это бывает полезно при определении проблемных областей кода при стрессовых сценариях.

Профили pprof

Инструмент pprof функционирует с помощью профилей. Под профилем понимается набор трассировок стека, которые показывают последовательности вызовов, ставших причиной появления определённого события, к примеру, выделения памяти. Подробная информация о реализации профилей содержится в файле runtime/pprof/pprof.go.

Язык программирования Go имеет целый перечень встроенных профилей, которые вы можете применять в стандартных ситуациях: • goroutine — следы всех текущих горутин; • allocs — выборка всех предыдущих выделений памяти; • heap — выборка выделений памяти живых объектов; • threadcreate — следы стека, ставшие причиной создания новых потоков в ОС; • mutex — следы стека держателей конфликтующих мьютексов; • block — следы стека, ставшие причиной блокировки примитивов синхронизации.

Heap

Heap (куча) представляет собой абстрактное представление места, в котором ОС хранит объекты, использующие код. В дальнейшем эта память очищается сборщиком мусора либо освобождается вручную.

Однако куча не является единственным местом, где осуществляется выделение памяти — часть памяти выделяется и на стеке. В языке программирования Go стек обычно используют для присвоений, которые происходят в рамках работы функции. Также в Go используется стек в случае, когда компилятор «знает», сколько конкретно памяти надо зарезервировать перед выполнением (к примеру, для массивов фиксированного размера).

При этом данные кучи должны быть освобождены с применением сборки мусора, а вот данные стека — нет. Именно поэтому гораздо эффективнее применять стек там, где это представляется возможным.

Получаем данные кучи посредством pprof

Существуют 2 основных способа получить данные. Первый, как правило, применяют в качестве части теста — он включает импорт runtime/pprof с последующим вызовом pprof.WriteHeapProfile(some_file) в целях записи информации в кучу.

// Функция lookup() берёт профиль
namepprof.Lookup("heap").WriteTo(some_file, 0)

Здесь WriteHeapProfile() существует для обратной совместимости. Однако прочие профили таких возможностей не имеют, поэтому для получения данных профилей вам следует применять функцию Lookup().

Другой способ — пустить его через HTTP (по web-адресу). Этот способ позволяет извлекать определённые данные из запущенного контейнера в тестовой среде либо e2e-среде или даже из production. При этом всю документацию пакета pprof вы можете и не читать, но как его включить, вы знать должны.

import (
  "net/http"
  _ "net/http/pprof"
)

...

func main() {
  ...
  http.ListenAndServe("localhost:8080", nil)
}

«Побочный эффект» импорта net/http/pprof — регистрация конечных адресов на web-сервере в корневом каталоге /debug/pprof. Применяя curl, мы сможем получить файлы с нужной информацией для анализа.

$ curl -sK -v http://localhost:8080/debug/pprof/heap > heap.out

В примере выше добавление http.ListenAndServe() потребуется лишь в том случае, когда программа раньше не имела прослушивателя HTTP-сервера. Также есть способы настроить его посредством ServeMux.HandleFunc().

Используем pprof на практике

Существуют 2 главные стратегии анализа памяти посредством pprof. Первая — это inuse. Она подразумевает рассмотрение текущих выделений памяти (байтов либо числа объектов). Вторую называют alloc. Она предполагает просматривание всех выделенных байтов либо количества объектов в процессе выполнения программы.

Выборкой выделения памяти является профиль heap. При этом «за кулисами» pprof применяет функцию runtime.MemProfile(), которая собирает информацию о предоставлении памяти на каждые 512 КБ выделенных байтов, делая это по умолчанию. Мы можем изменить MemProfile() в целях сбора информации о всех объектах, но такой шаг, вероятнее всего, замедлит работу нашего приложения.

Итак, как только файл профиля будет собран, его можно загружать в интерактивную консоль pprof.

$ go tool pprof heap.out

Давайте посмотрим на отображаемую информацию:

Type: inuse_space
Time: Jan 22, 2019 at 1:08pm (IST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

Обратите внимание на Type: inuse_space. Дело в том, что мы смотрим на данные выделения памяти в конкретный момент, то есть когда захватили профиль. Тип — это значение конфигурации sample_index, а возможными значениями бывают: • inuse_space — объём выделенной и пока не освобождённой памяти; • inuse_object s — число выделенных и пока не освобождённых объектов; • alloc_space — общий объём выделенной памяти (вне зависимости от освобождённой); • alloc_objects — общее число выделенных объектов (вне зависимости от освобождённых).

Если вы теперь введёте в интерактивную консоль top, в выводе отобразятся главные потребители памяти.

Например, можно заметить строку, показывающую сброшенные узлы (Dropped Nodes). Узел представляет собой выделение объекта либо «узел» в дереве. Удалять узлы в целях уменьшения количества мусора — вроде бы неплохая идея, однако порой это может скрывать первопричину утечек памяти.

Если пожелаете включить все данные профиля, следует при запуске pprof добавить опцию -nodefraction=0 либо ввести в интерактивной консоли nodefraction=0.

В выводимом списке вы сможете увидеть 2 значения — flat и cum. Что они означают: • flat — память выделена функцией и ей же удерживается; • cum — память выделена функцией либо функцией, вызванной стеком.

В целом, этой информации обычно хватает, чтобы понять, существует ли проблема. К примеру, функция отвечает за выделение большого объёма памяти, но при этом она не удерживает её. Такое положение вещей будет значить, что какой-либо иной объект указывает на данную память и удерживает её — следовательно, возможно возникновение утечки памяти либо бага.

Отдельно стоит сказать про команду top в интерактивной консоли. По умолчанию она выведет первые десять позиций потребителей памяти. Однако данная команда поддерживает формат topN, причём N здесь — это число записей, которые вы желаете увидеть. К примеру, при наборе top70 выведутся все узлы.

По материалам статьи «How I investigated memory leaks in Go using pprof on a large codebase»

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто