Читаем heap-профиль в Go без паники
RSS сервиса рос от 200 МБ до полутора гигабайт за сутки и падал только с рестартом. Классическая «утечка», которая утечкой не оказалась.
Снять профиль
В сервисе уже был подключён net/http/pprof на отдельном порту, наружу не смотрящем:
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("127.0.0.1:6060", nil))
}()
Дальше два снимка с интервалом в час — разница важнее абсолютных чисел:
curl -s localhost:6060/debug/pprof/heap > heap1.pb.gz
sleep 3600
curl -s localhost:6060/debug/pprof/heap > heap2.pb.gz
go tool pprof -base heap1.pb.gz heap2.pb.gz
Четыре разных вопроса
В heap-профиле четыре среза, и путать их — главный источник ложных выводов:
| Срез | Отвечает на вопрос |
|---|---|
inuse_space | что занимает память прямо сейчас |
inuse_objects | сколько живых объектов сейчас |
alloc_space | сколько аллоцировано за всё время |
alloc_objects | кто создаёт давление на GC |
Для поиска утечки нужен inuse_space. Для поиска лишнего мусора — alloc_space. Я по привычке смотрел второе и полчаса изучал совершенно здоровый json-декодер.
go tool pprof -sample_index=inuse_space heap2.pb.gz
(pprof) top10 -cum
(pprof) list bufferPool.*Get
Чем кончилось
В inuse_space девяносто процентов занимал sync.Pool с буферами под ответы. Утечки не было: код клал в пул буферы, выросшие до нескольких мегабайт после редких больших ответов, и пул их честно держал.
Починка — не возвращать в пул переросшие буферы:
const maxPooled = 64 << 10
func put(b *bytes.Buffer) {
if b.Cap() > maxPooled {
return // пусть соберёт GC
}
b.Reset()
pool.Put(b)
}
RSS встал на 260 МБ и перестал расти.
Заметки на полях
- Рост RSS сам по себе ещё не утечка: Go отдаёт память ОС лениво. Сравнивать надо
inuse_space, а неtopв консоли. GODEBUG=madvdontneed=1заставляет рантайм отдавать страницы агрессивнее — помогает отличить «держим» от «не вернули».-baseу pprof бесценен: он сразу показывает, что именно выросло между снимками.- Профиль heap сэмплируется раз в 512 КБ по умолчанию. Для мелких объектов имеет смысл
runtime.MemProfileRateопустить на время расследования.