Рабочие заметки

Читаем 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 МБ и перестал расти.

Заметки на полях


← ко всем записям