Дамп кучи в Go с использованием pprof

Несколько недель назад я столкнулся с утечкой памяти в одном из наших приложений Go. Это приложение представляет собой внешнее средство масштабирования, которое интегрирует и предоставляет метрики KEDA для масштабирования других приложений внутри нашего кластера K8s.

Через некоторое время приложение просто отключилось. Для работы не требуется много памяти, она нормально работала с менее чем 100 МБ. Он начал требовать намного больше памяти, достигнув предела более 1 ГБ, и приложение было закрыто.

Я очень привык анализировать инструменты для Java, такие как MAT, JConsole, VisualVM, GCEasy, некоторые APM и другие. Но мне это никогда не было нужно для Go, до этой проблемы.

В поисках инструмента для решения этой проблемы я нашел pprof. Он предоставляет данные профилирования через HTTP-сервер в вашем приложении. Насколько мне известно, в настоящее время это лучший вариант для получения данных профилирования в Go. Сообщите мне, у вас есть другие варианты.

Как использовать pprof в вашем приложении

Если ваше приложение представляет собой веб-приложение, в котором уже запущен HTTP-сервер, вы можете просто импортировать пакет pprof:

import _ "net/http/pprof"

Если приложение, которое вам нужно проанализировать, не имеет HTTP-сервера (в моем случае), вам необходимо импортировать HTTP-пакет и pprof:

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

Если у вас не запущен HTTP-сервер, вам также необходимо запустить его, чтобы вы могли добавить это в свой код:

go func() {
	log.Println(http.ListenAndServe("localhost:8081", nil))
}()

Это автоматически делает pprof доступным через ваш HTTP-сервер.

Если вы не используете DefaultServeMux, нужно сделать еще один шаг. Вам необходимо зарегистрировать маршрут для pprof или зарегистрировать его для обработки http.DefaultServeMux. В приведенном ниже коде показано, как справиться с этим с помощью мультиплексора по умолчанию:

myRouter.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux)

При желании вы можете зарегистрироваться для каждого маршрута вручную:

myRouter.HandleFunc("/debug/pprof/", pprof.Index)
myRouter.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
myRouter.HandleFunc("/debug/pprof/profile", pprof.Profile)
myRouter.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
myRouter.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
myRouter.Handle("/debug/pprof/heap", pprof.Handler("heap"))
myRouter.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
myRouter.Handle("/debug/pprof/block", pprof.Handler("block"))

Теперь вы можете извлекать дампы для своего приложения.

Извлечение дампа

Теперь у вас есть разные конечные точки, которые дадут вам разные профили. Чтобы увидеть доступные профили, вы можете выполнить GET на конечную точку http://localhost:8081/debug/pprof

Вы можете скачать результаты профилей для последующего анализа. Теперь займемся кучей.

curl http://localhost:8081/debug/pprof/heap > heap.out

Результат будет сохранен в файле с именем heap.out. Вы также можете получить другие профили. Например, давайте получим все трассировки стека всех текущих горутин:

curl http://localhost:8081/debug/pprof/goroutine> goroutine.out

Анализируем результат

Есть несколько способов проанализировать дамп. Лучшее, что я нашел, - это использовать инструмент pprof в качестве веб-сервера, после чего вы можете перемещаться по различным типам представлений, образцов и фильтровать данные.

Чтобы запустить этот сервер, используйте следующую команду:

go tool pprof -http=:8082 heap.out

Теперь можно получить доступ к этому инструменту из вашего браузера. Вы можете просто выбрать порт и передать загруженный файл. Вы можете использовать его и с другими профилями, просто изменив имя файла:

go tool pprof -http=:8082 goroutine.out

Теперь вы можете увидеть, как используется ваша куча. Легче определить, есть ли в вашем приложении утечка памяти и где ее найти.

Извлечение дампа из K8S

Здесь я покажу вам, как я обычно извлекаю эти файлы из нашего кластера K8s. Во-первых, если у вас есть API, у вас уже должен быть сервис, предоставляющий его, тогда проще просто запросить данные из этого API.

В этом случае мой модуль не открывал службу HTTP, поэтому у меня было два варианта.

  1. Откройте службу HTTP, чтобы получить файлы pprof
  2. Подключитесь к модулю, чтобы извлечь файлы

Поскольку мне не нужно открывать HTTP-сервис по какой-либо другой причине, я выбрал второй вариант. Для этого сначала я запускаю команду sh в модуле:

kubectl exec -it POD_NAME sh

Затем я делаю запрос на локальный хост для извлечения нужного мне файла:

curl http://localhost:8081/debug/pprof/heap > heap.out

После этого вы можете просто скопировать файл на свой компьютер:

kubectl cp POD_NAME:/app/heap.out .\

Теперь вы можете проанализировать файл с помощью pprof, используя тот же процесс, что и раньше:

go tool pprof -http=:8081 heap.out

Решение

Как я уже упоминал ранее, у меня были проблемы с утечкой памяти в одном из наших приложений. Это приложение, которое обслуживает сервер gRPC. Глубоко проанализировав кучу, я обнаружил, что более 90% памяти использовалось для обработки соединений.

Проблема заключалась в том, что приложение, использующее эту службу, не управляло соединениями должным образом, что приводило к открытию большего количества соединений, чем необходимо. Через какое-то время подключений оказалось больше, чем наш сервис может обработать. Мы не справились с этой проблемой на нашем сервере (мы должны), потому что потребители известны и надежны.

Решение состояло в том, чтобы заставить потребителей правильно открывать и повторно использовать эти открытые соединения, избегая затем сохранения большого количества открытых соединений.

Код, использованный в демонстрации, можно найти здесь.