Горутины и потоки
Как найти и починить утечки, которые валят API
Игнорирование контекста — это путь боли
Как найти и починить утечки, которые валят API
API падает каждый день? Память растёт, горутины множатся, потом крах. Это не магия — это утечки ресурсов. Вот как их найти и убить.
Шаг 1: Собрать доказательства перед тем как гадать
Не начинайте с предположений. Сначала соберите логи. Ищите паттерны: растут ли таймауты, появляются ошибки «context canceled», скачет ли error rate?
Heap и goroutine профили. Это главное. Добавьте pprof endpoint, если его нет:
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe(":6060", nil))
}()
На следующем инциденте вытащите профили:
curl localhost:6060/debug/pprof/heap > heap.pb
curl localhost:6060/debug/pprof/goroutine?debug=2 > goroutines.txt
Проверьте лимиты: /proc/<pid>/limits, количество открытых файловых дескрипторов, состояние сокетов.
Шаг 2: Прочитать профили и понять, что протекает
Откройте goroutine профиль текстом — там все ответы:
go tool pprof -http=:8080 heap.pb
Что нужно найти:
• Горутины в CLOSE_WAIT. Это сокеты, которые никто не закрыл нормально.
• Горутины, зависшие на channel send/receive. Если тысячи горутин ждут отправить или получить из канала — канал где-то забился или заблокирован.
• Горутины в syscall. Много горутин ждут на сетевых операциях — это может быть утечка соединений или зависание на стороне сервера.
Шаг 3: Найти корни утечек в коде
Три главных виновника:
• Горутины без контекста. Запустили горутину для async операции, но не гарантировали её завершение:
// Плохо: горутина может зависнуть
go func() {
resp, _ := http.Get(url)
// ...
}()
// Хорошо: контекст отменяет работу
go func(ctx context.Context) {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, _ := httpClient.Do(req)
}(r.Context())
• HTTP клиенты без переиспользования. Создаёте нового клиента на каждый запрос:
// Плохо: новый Transport на каждый вызов
func handleRequest(w http.ResponseWriter, r *http.Request) {
client := &http.Client{}
resp, _ := client.Get("https://api.example.com/data")
}
// Хорошо: переиспользуем один клиент
var httpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
}
• Неограниченные очереди. На пике нагрузки канал растёт без предела:
// Плохо: неограниченный канал
jobs := make(chan Job)
// Хорошо: ограничиваем размер
jobs := make(chan Job, 1000)
select {
case jobs <- job:
// принято
default:
http.Error(w, "перегружены", http.StatusServiceUnavailable)
}
Чтобы не падать в проде можно попробовать имитировать нагрузку на дев стенде:
# Генерируем нагрузку
ab -n 100000 -c 100 http://localhost:8080/endpoint
# В другом терминале снимаем профиль каждую минуту
for i in {1..60}; do
curl localhost:6060/debug/pprof/goroutine?debug=1 > goroutine_$i.txt
sleep 60
done
Растёт ли количество горутин? Если да — утечка. Если нет — ложная тревога.
Не полагайтесь только на профили. Мониторьте в реальном времени:
// Метрика: количество горутин
runtime.NumGoroutine()
// Метрика: память
var m runtime.MemStats
runtime.ReadMemStats(&m)
m.Alloc // текущее использование
// Метрика: открытые файловые дескрипторы (Linux)
files, _ := ioutil.ReadDir("/proc/self/fd")
len(files) // количество открытых fd
Выставляйте эти метрики в Prometheus или что у вас есть. Алерты на рост горутин выше нормы — это первый звоночек.
Не решайте задачу вслепую. Профилируйте, смотрите данные, ищите паттерны. Утечки это не магия — это просто ресурсы, которые вы забыли закрыть.
Игнорирование контекста — это путь боли
Новичок пишет код примерно так:
res, err := http.Get(url)
Выглядит невинно, но если вызов зависнет, то зависнет и горутина. Навсегда. Нет таймаута, нет способа его отменить. Сервис просто накапливает горутины, пока не упадёт.
Профессионалы всегда передают контекст через все слои приложения:
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
res, err := http.DefaultClient.Do(req)
Они распространяют его на весь путь — от HTTP хендлера через бизнес-логику до репозиториев:
func (r *UserRepo) Get(ctx context.Context, id int) (*User, error) {
// ctx здесь и управляет отменой, и ловит таймауты
}
Это не просто хорошая практика — это основа надёжности.
No comments to display
No comments to display