Исследуем утечки памяти в 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 с последующим вызовом
// Функция lookup() берёт профиль namepprof.Lookup("heap").WriteTo(some_file, 0)
Здесь
Другой способ — пустить его через 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
В примере выше добавление
Используем pprof на практике
Существуют 2 главные стратегии анализа памяти посредством pprof. Первая — это inuse. Она подразумевает рассмотрение текущих выделений памяти (байтов либо числа объектов). Вторую называют alloc. Она предполагает просматривание всех выделенных байтов либо количества объектов в процессе выполнения программы.
Выборкой выделения памяти является профиль heap. При этом «за кулисами» pprof применяет функцию
Итак, как только файл профиля будет собран, его можно загружать в интерактивную консоль 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»