А где память? Утечка goroutine и как ее пофиксить
- пятница, 14 февраля 2025 г. в 00:00:07
В мире современной разработки на языке Go (Golang) горутины являются одной из ключевых особенностей, обеспечивающих высокую производительность и эффективность многозадачности. Они представляют собой легковесные потоки, которые позволяют выполнять задачи параллельно, что делает Go идеальным выбором для создания высоконагруженных приложений. Однако, как и любая мощная технология, горутины требуют внимательного подхода к управлению ресурсами. Одной из распространённых проблем, с которой сталкиваются разработчики, является утечка горутин — ситуация, когда горутины продолжают существовать и потреблять ресурсы, даже если они больше не нужны.
Утечка горутин может привести к серьёзным последствиям: от увеличения потребления памяти до полной остановки приложения из-за исчерпания системных ресурсов. В этой статье мы рассмотрим, что такое утечка горутин, как её обнаружить, какие инструменты и методики помогут предотвратить эту проблему, а также разберём практические примеры и лучшие практики для написания безопасного и эффективного кода на Go.
Утечка горутин в Go происходит, когда горутина продолжает существовать и потреблять ресурсы, даже если она больше не выполняет полезной работы или не может завершиться. Это может произойти по разным причинам.
Мы рассмотрим 3 примера из которых: 2 будут на каналах, 1 с использованием mutex.
func func1() int64 {
time.Sleep(2 * time.Second)
return 1
}
Обычная задача , мы хотим обернуть функцию func1 в декоратор и использовать context для возврата ошибки по истечению времени. Совсем просто сказал я и создал этот код.
func func2WithContext(ctx context.Context) (int64, error) {
ch := make(chan int64)
go func() { // goroutine try to write result in channel
ch <- func1()
}()
select {
case value := <-ch:
return value, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
Казалось бы, всё работает - мы прокидываем контест и ожидаем либо возврата от func1, либо истечения времени ctx.
Но представим, что ctx завершился раньше. Тогда вслед за ctx.Done() мы выйдем из func2WithContext, а func1 всё еще не закончит своё выполнение.
Спустя некоторое время func1 смогла вернуть результат обозначим его result , тогда анонимная горутина попытается записать result в наш канал, тут нам и хана.
Произойдет блокировка, так как канал не буферизированный и его уже давно никто не читает, анонимная горутинка зависнет.
Решение этого довольно легкое. Давайте просто сделаем буферизированный канал, тогда анонимная горутина сможет записать в него значение, а дальше сам канал удалится благодаря сборщику мусора.
func func2WithContext(ctx context.Context) (int64, error) {
ch := make(chan int64, 1)
go func() { // goroutine try to write result in channel
ch <- func1()
}()
select {
case value := <-ch:
return value, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
У нас есть следующий код. Легенда: некоторый сервис обрабатывает набор данных и отдает их в канал для дальнейшей работы.
func printData(dataCh chan string) { //goroutine wait info from dataCh
for info := range dataCh {
fmt.Println(info)
}
}
func processData(data []string) {
dataCh := make(chan string)
go printData(dataCh)
for _, info := range data {
dataCh <- info
}
}
Тут мы совершили страшный проступок ошибка и ты ошибся.
Если в каналы пишем конечный набор данных , а не используем их для бесконечной отдачи данных другой горутины, то канал должен закрываться
Заметим, что функция printData завершается только в том случае, если канал закрылся.
Функция processData не закрывает канал в который пишет, что в данном случае и приводит к утечке горутины и утечке объекта dataCh.
Решение довольно простое: нужно закрыть канал
func processData(data []string) {
dataCh := make(chan string)
go printData(dataCh)
for _, info := range data {
dataCh <- info
}
close(dataCh)
}
Вот же лексический повтор.
Вкратце мы имеем следующий код.
func work() {
m := &sync.Mutex{}
m.Lock()
go func() { // try to lock locked mutex
m.Lock()
}()
}
Заметим, что код максимально простой , тут мы всё что делаем это блокируем уже заблокированный Мьютекс внутри другой горутины.
Что имеем? Анонимная функция не будет закончена, пока ей не удастся залочить mutex.
Результат: у нас утекла горутина (
Легко - никак
Проблема на самом деле острая и прийдется покопаться в грязи.
Для отладки есть несколько способов.
1) Линтер и проверка кода глазками.
В описанных выше случаях линтер не помогает, он не находит проблему, всё что мне остается - это искать глазками
2)Метрики и скрины стэк трейса.
Этот служит сужение круга подозреваемых, для дальнейшей отладки ручками. В основном все использует передачу метрик например в Grafana и debug с помощью скринов сервиса. Уже предустановленным пакетом pprof.
1) Следите за code style программы. Бейте большие участки кода на маленькие, что позволит сузить круг подозреваемых объектов
2) Убедитесь, что использованный объекты вы обработали правильно. Будь, то ctx и отменной Deadline или закрытие Канала
3) Набирайтесь опыта: благодаря опыту вы будете не допускать ошибок, просто потому-что вы помните как отлаживали подобное ... и не имеете желания делать это вновь.
Моя первая техническая статья , так -что без буллинга, а то налетят Яндексишкины и накидают -rep, поймите карма не бесконечная. Я попытался максимально кратко обозначить проблему и дать мини ответы для пути её решения. Расписать всё подробно = статья на 30-60 минут, ее тогда никто не будет читать. Главное, что я хочу донести: боец осведомлён, что бывает утечка горутин, а как ее решать подробно, боец и без меня поймет. Иначе, не стать ему генералом.
Всех обнял.
Ссылка на исходники, там более подробно описано решение с помощью вывода стек-трейсов в рантайме.
- гитхаб с исходниками
- канальчик для вкатунов ВЕК
- личный ютуб бложек про ваше IT