Для обнаружения аномально высокой длительности выполнения отдельных функций (а также избыточного выделения или утечек памяти) используются инструменты профилирования над виртуальной машиной (например, JProfiler или Visual VM для JVM) или интегрированные в выполняемый код, например встроенный механизм при компиляции Go-приложений. Альтернативой может стать использование универсальных механизмов профилирования, которые интегрируются со средой выполнения и отправляют результаты профилирования на сервер, который может анализировать аномальное поведение и визуализировать выделение памяти и время выполнения отдельных функций (и построить flame graph по результатам анализа приложения во время выполнения). В этой статье мы рассмотрим использование Pyroscope совместно с Go для обнаружения утечек памяти.
Прежде всего, нужно отметить что Pyroscope может интегрироваться с разными средами выполнения и поддерживает JVM, PHP, Ruby, CLR (.Net), Python и Go. Архитектурно Pyroscope состоит из агента (который одновременно является launcher'ом для запуска исследуемого приложения) и сервера, который накапливает данные и позволяет их анализировать после сбора. Сейчас Pyroscope стал частью экосистемы Grafana и позволяет интегрироваться в другие компоненты Observability (включая визуализацию метрик, распределенной трассировки на Grafana Tempo и др.).
Для начала создадим простое консольное приложение, в котором будем симулировать утечку памяти:
package main
import (
"io"
"math"
"net/http"
"runtime/debug"
"sync"
)
var globalSlice = make([][]byte, 0, 0)
func leak() {
h, _ := http.Get("https://www.google.com")
body, _ := io.ReadAll(h.Body)
globalSlice = append(globalSlice, body)
go leak()
}
func main() {
// disable GC
debug.SetGCPercent(-1)
debug.SetMemoryLimit(math.MaxInt64)
var wg sync.WaitGroup
wg.Add(1)
go leak()
wg.Wait()
}
Для установки агента Pyroscope в Go установим его как часть нашего приложения, это поможет правильно сконфигурировать сбор данных. Добавим модуль:
go get github.com/pyroscope-io/client/pyroscope
И выполним инициализацию для захвата профилирования (например, в функции main):
package main
import (
"io"
"math"
"net/http"
"runtime/debug"
"github.com/pyroscope-io/client/pyroscope"
"sync"
)
func main() {
pyroscope.Start(pyroscope.Config{
ApplicationName: "simple.golang.app",
ServerAddress: "http://localhost:4040",
Logger: pyroscope.StandardLogger,
// список поддерживаемых профилировщиков
ProfileTypes: []pyroscope.ProfileType{
pyroscope.ProfileCPU,
pyroscope.ProfileAllocObjects,
pyroscope.ProfileAllocSpace,
pyroscope.ProfileInuseObjects,
pyroscope.ProfileInuseSpace,
},
})
// disable GC
debug.SetGCPercent(-1)
debug.SetMemoryLimit(math.MaxInt64)
var wg sync.WaitGroup
wg.Add(1)
go leak()
wg.Wait()
}
Для сбора данных Pyroscope использует API, которое публикуется на тот же порт, что и веб-интерфейс:
docker run -d -p 4040:4040 pyroscope/pyroscope - server
Pyroscope может получать данные как непосредственно от агента (как в нашем случае, данные профилирования будут загружаться периодически на указанный адрес сервера, по умолчанию один раз в 10 секунд), так и извлекаться со стороны сервера (через механизм поллинга). В случае поллинга поддерживается любая библиотека, которая может отправлять данные в http-ответе в формате pprof (например, в Go это может быть net/http/pprof
). Конфигурация поллинга определяется в файле server.yml (при запуске в docker - /etc/pyroscope/server.yml
):
scrape-configs:
- job-name: pyroscope
scrape-interval: 10s
enabled-profiles: [cpu, mem, goroutines, mutex, block]
static-configs:
- application: example-app
spy-name: gospy
targets:
- app:8080
labels:
env: dev
В веб-интерфейсе Pyroscope выберем наше приложение и метрику inuse_space:
Для анализа конкретной функции мы также можем использовать тэги:
pyroscope.TagWrapper(context.Background(), pyroscope.Labels("leak"), func(c context.Context) {
go leak()
})
Также тэги можно задавать при запуске агента pyroscope (например, для агрегации информации в микросервисной архитектуре). На графике изменения метрики можно также добавить аннотации (например, пометить события запуска GC). Исследовать метрику можно как с помощью диаграммы Flamegraph (где показана иерархия использования метрики во вложенных вызовах), так и визуализацию на графе (через graphviz):
Также можно сохранить результаты замеров как baseline и сравнить его в дальнейшем с новыми замерами. Pyroscope также предоставляет возможность анализа ранее сохраненного файла в формате pprof (Adhoc Profiling).
Анализируя изменение в течении времени используемой памяти с привязкой к иерархии функций можно обнаружить проблему с выделением ресурсов и исследовать первопричину ее возникновения. Pyroscope позволяет визуализировать как метрики использования памяти (alloc_space
, inuse_space
), так и количества выделенных ресурсов (alloc_objects
, inuse_objects
) и использование процессора (cpu
), что позволяет выявлять длительные операции и выполнять оптимизацию кода по замерам в реальных условиях.
Статья подготовлена в преддверии старта курса Golang Developer. Professional.
KrasPvP
Конечно, у pyroscope есть и пример https://github.com/grafana/pyroscope/tree/main/examples/golang-push, и документация https://pyroscope.io/docs/golang/, которые раскрывают такую же суть статьи, но всё равно спасибо, скопипастил, запустил код, потыкал 5 минут. Новый опыт :)