Створення кастомних Go-профілів з допомогою pprof. Запам'ятовуємо стеки


Кадр із серіалу «Коломбо»

Go-шний пакет pprof часто використовується для профілювання процесора, пам'яті, але не всі знають про можливості створювати власні кастомні профілі. Вони можуть бути корисні для пошуку витоків ресурсів або, наприклад, для спостереження за зловживанням якимись важкими викликами.

Профілі
документації по pprof:
Профіль – це набір стек-трейсов, що показує порядок викликів, які привели до якого-небудь події. Наприклад, виділення пам'яті. Пакети можуть створювати і підтримувати свої власні профілі. Найпоширеніша причина – спостереження за будь-якими ресурсами, які вимагають явного закриття: файлами, мережевими з'єднаннями.
Іншим потенційним застосуванням профілів може бути стеження не за чимось, що потрібно явно закрити, а за функцією або ресурсом, який може блокувати виконання при виклику, і вам потрібно зрозуміти, де такі виклики, скільки їх і так далі. Профілі особливо корисні там, де корисні стек-трейсы, і основною перевагою є проста інтеграція з go tool pprof.

Застереження при використанні профілів
Пакет profile панікує практично при будь-якому неправильному його використанні. Наприклад:
  • Створення профілю, який вже існує
  • Прив'язування профілю до одного і того ж об'єкту двічі
  • Видалення стека, який до цього не був доданий
  • Видалення стека, який вже був видалений
Постарайтеся уникати таких ситуацій.

Створення профілю
var libProfile *pprof.Profile
func init() {
profName := "my_experiment_thing"
libProfile = pprof.Lookup(profName)
if libProfile == nil {
libProfile = pprof.NewProfile(profName)
}
}

Оскільки ми не можемо створити два різних профілю з одним і тим же ім'ям, має сенс створювати їх в init(). Можливо, ви захочете створити профіль в один рядок.

// Warning: /vendor panic possibilities
var panicProfile = pprof.NewProfile("this_unique_name")

Але таке використання може призвести до того, що вибране вами ім'я вже може бути використано. Навіть якщо ви упевнені, що воно унікальне, якщо вашу бібліотеку вендорят кілька разів (що цілком можливо), додаток запанікує при старті. Т. к. профілі є thread safe, а init()-функція виконується один раз, підхід перевірити-і-створити – це правильний підхід.

Godoc згадує, що загальноприйнятий спосіб створювати унікальні імена – це використання префікса 'import/path.', але, якщо слідувати пораді, це призведе до того, що ви натрапите на відомий баг в cmd/pprof. Так що використовуйте шлях і ім'я вашого пакету, але з такими символами [a-zA-Z0–9_].

Використання профілю
type someResource struct {
*os.File
}

func MustResource() *someResource {
f, err := os.Create(...)
if err != nil {
panic(err)
}
r := &someResource{f}
libProfile.Add(r, 1)
return r
}

func (r *someResource) Close() error {
libProfile.Remove®
return r.File.Close()
}

Головними функціями пакета є Add і Remove. У цьому прикладі я буду стежити за всіма створеними ресурсами, які не закриті, так що я додам стек-трейс, в той момент, коли створюю ресурс, і видалити його, коли закриваю його. Функція «Add» вимагає унікальний об'єкт при кожному виклику, так що я можу використовувати сам ресурс в якості ключа. Іноді не існує хорошого ключа, в такому випадку ви можете створити фіктивний байт і використовувати його адресу.

func usesAResource() {
pprofKey := new(byte)
libProfile.Add(pprofKey, 1)
defer libProfile.Remove(pprofKey)
/ / ....
}

Експортування нового профілю pprof
Якщо ви підключите бібліотеки http pprof, Go зареєструє http-обробники для пакета profile. Зазвичай це роблять додаванням «порожнього» імпорту у файлі main.go.
import _ "net/http/pprof"

Добитися того ж самого можна за допомогою ручної реєстрації pprof-обробника.
httpMux.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))

Використання pprof
Я створив тестове додаток для демонстрації всього того, про що кажу.

package main

import (
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"os"
"runtime/pprof"
"sync/atomic"
"time"
)

var libProfile *pprof.Profile

func init() {
profName := "my_experiment_thing"
libProfile = pprof.Lookup(profName)
if libProfile == nil {
libProfile = pprof.NewProfile(profName)
}
}

type someResource struct {
*os.File
}

var fileIndex = int64(0)

func MustResource() *someResource {
f, err := os.Create(fmt.Sprintf("/tmp/%d.txt", atomic.AddInt64(&fileIndex, 1)))
if err != nil {
panic(err)
}
r := &someResource{f}
libProfile.Add(r, 1)
return r
}

func (r *someResource) Close() error {
libProfile.Remove®
return r.File.Close()
}

func trackAFunction() {
tracked := new(byte)
libProfile.Add(tracked, 1)
defer libProfile.Remove(tracked)
time.Sleep(time.Second)
}

func usesAResource() {
res := MustResource()
defer res.Close()
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
}
}

func main() {
http.HandleFunc("/nonblock", func(rw http.ResponseWriter, req *http.Request) {
go usesAResource()
})
http.HandleFunc("/functiontrack", func(rw http.ResponseWriter, req *http.Request) {
trackAFunction()
})
http.HandleFunc("/block", func(rw http.ResponseWriter, req *http.Request) {
usesAResource()
})
log.Println("Running!")
log.Println(http.ListenAndServe("localhost:6060", nil))
}

Запустивши цю програму, ви можете зайти на http://localhost:6060/debug/pprof/ і побачити всі доступні профілі.



Подайте трохи трафіку на /nonblock та /block, потім натисніть на посилання my_example_thing, щоб побачити профіль.

my_experiment_thing profile: total 6
4 @ 0x2245 0x5d961
# 0x2244 main.usesAResource+0x64 /Users/.../pproftest.go:64

2 @ 0x2245 0x2574 0x9c184 0x9d56f 0x9df7d 0x9aa07 0x5d961
# 0x2244 main.usesAResource+0x64 /Users/.../pproftest.go:64
# 0x2573 main.main.func3+0x13 /Users/.../pproftest.go:79
# 0x9c183 net/http.HandlerFunc.ServeHTTP+0x43 /usr/local/Cellar/go/1.7.1/libexec/src/net/http/server.go:1726
# 0x9d56e net/http.(*ServeMux).ServeHTTP+0x7e /usr/local/Cellar/go/1.7.1/libexec/src/net/http/server.go:2022
# 0x9df7c net/http.serverHandler.ServeHTTP+0x7c /usr/local/Cellar/go/1.7.1/libexec/src/net/http/server.go:2202
# 0x9aa06 net/http.(*conn).serve+0x4b6 /usr/local/Cellar/go/1.7.1/libexec/src/net/http/server.go:1579

Граф викликів
Я використовував brew, щоб встановити Graphviz на мій Mac: він потрібен, щоб pprof міг створити png картинки.

brew install Graphviz

Після установки graphviz я можу використовувати pprof, щоб згенерувати png-картинку з графом викликів.
go tool pprof -png /tmp/mybinary 'localhost:6060/debug/pprof/my_experiment_thing?debug=1' > /tmp/exp.png

Я використовував PNG для зручності вставки в цю статтю, але зазвичай SVG зручніше для перегляду в браузері. Згенеруйте svg замість png, додавши -svg замість -png при виклику команди pprof.
Готова картинка – нижче.


Ця картинка показує мені стек-трейсы створення тих ресурсів, які не були закриті. Коли я генерував цю картинку, я послав в два рази більше неблокирующих запитів, і це видно по трейсу. Всі стек-трейсы закінчуються в MustResource. Якщо вам це не подобається, ви можете передати ціле число при виклику Profile.Add.
Ще ви можете використовувати інтерактивну консоль, яка доступна при запуску pprof з терміналу. Нижче я запустив pprof і використовую команду top, щоб побачити, які виклики зустрічаються частіше серед всіх моїх стек-трейсов.

> go tool pprof 'localhost:6060/debug/pprof/my_experiment_thing?debug=1'

Fetching profile from http://localhost:6060/debug/pprof/my_experiment_thing?debug=1
Saved profile in /Users/.../pprof/pprof.localhost:6060.my_experiment_thing.007.pb.gz
Entering interactive mode (type "help" for commands)

(pprof) top30

6 of 6 total ( 100%)

flat flat% sum% cum cum%
6 100% 100% 6 100% main.usesAResource
0 0% 100% 2 33.33% main.main.func3
0 0% 100% 2 33.33% net/http.(*ServeMux).ServeHTTP
0 0% 100% 2 33.33% net/http.(*conn).serve
0 0% 100% 2 33.33% net/http.HandlerFunc.ServeHTTP
0 0% 100% 2 33.33% net/http.serverHandler.ServeHTTP
0 0% 100% 6 100% runtime.goexit

(pprof)

Висновок
Не всі можливості, які використовуються при профілюванні процесора, пам'яті, доступні через API pprof, але все одно ми отримуємо дуже класну візуалізацію, враховуючи, як мало коду необхідно. Наступного разу, коли ви будете писати бібліотеку, подивіться, можливо, стек-трейсы вам допоможуть отдебажить конкретно вашу проблему.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.