golang

А где память? Утечка goroutine и как ее пофиксить

  • пятница, 14 февраля 2025 г. в 00:00:07
https://habr.com/ru/articles/881978/

Введение

В мире современной разработки на языке Go (Golang) горутины являются одной из ключевых особенностей, обеспечивающих высокую производительность и эффективность многозадачности. Они представляют собой легковесные потоки, которые позволяют выполнять задачи параллельно, что делает Go идеальным выбором для создания высоконагруженных приложений. Однако, как и любая мощная технология, горутины требуют внимательного подхода к управлению ресурсами. Одной из распространённых проблем, с которой сталкиваются разработчики, является утечка горутин — ситуация, когда горутины продолжают существовать и потреблять ресурсы, даже если они больше не нужны.

Утечка горутин может привести к серьёзным последствиям: от увеличения потребления памяти до полной остановки приложения из-за исчерпания системных ресурсов. В этой статье мы рассмотрим, что такое утечка горутин, как её обнаружить, какие инструменты и методики помогут предотвратить эту проблему, а также разберём практические примеры и лучшие практики для написания безопасного и эффективного кода на Go.

А что собственно происходит?

Утечка горутин в Go происходит, когда горутина продолжает существовать и потреблять ресурсы, даже если она больше не выполняет полезной работы или не может завершиться. Это может произойти по разным причинам.
Мы рассмотрим 3 примера из которых: 2 будут на каналах, 1 с использованием mutex.

Пример №1 - запись канал

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()
	}
}

Пример №2 - чтение из канала

У нас есть следующий код. Легенда: некоторый сервис обрабатывает набор данных и отдает их в канал для дальнейшей работы.

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)
}

Пример №3 - блокировка блокированного Мьютекса

Вот же лексический повтор.
Вкратце мы имеем следующий код.

func work() {
	m := &sync.Mutex{}
	m.Lock()

	go func() { // try to lock locked mutex
		m.Lock()
	}()
}

Заметим, что код максимально простой , тут мы всё что делаем это блокируем уже заблокированный Мьютекс внутри другой горутины.
Что имеем? Анонимная функция не будет закончена, пока ей не удастся залочить mutex.
Результат: у нас утекла горутина (

Как отладить?

Легко - никак
Проблема на самом деле острая и прийдется покопаться в грязи.
Для отладки есть несколько способов.

1) Линтер и проверка кода глазками.
В описанных выше случаях линтер не помогает, он не находит проблему, всё что мне остается - это искать глазками

2)Метрики и скрины стэк трейса.
Этот служит сужение круга подозреваемых, для дальнейшей отладки ручками. В основном все использует передачу метрик например в Grafana и debug с помощью скринов сервиса. Уже предустановленным пакетом pprof.

Советы - их будет 3

1) Следите за code style программы. Бейте большие участки кода на маленькие, что позволит сузить круг подозреваемых объектов

2) Убедитесь, что использованный объекты вы обработали правильно. Будь, то ctx и отменной Deadline или закрытие Канала

3) Набирайтесь опыта: благодаря опыту вы будете не допускать ошибок, просто потому-что вы помните как отлаживали подобное ... и не имеете желания делать это вновь.

PS

Моя первая техническая статья , так -что без буллинга, а то налетят Яндексишкины и накидают -rep, поймите карма не бесконечная. Я попытался максимально кратко обозначить проблему и дать мини ответы для пути её решения. Расписать всё подробно = статья на 30-60 минут, ее тогда никто не будет читать. Главное, что я хочу донести: боец осведомлён, что бывает утечка горутин, а как ее решать подробно, боец и без меня поймет. Иначе, не стать ему генералом.

Всех обнял.

Ссылка на исходники, там более подробно описано решение с помощью вывода стек-трейсов в рантайме.

- гитхаб с исходниками

- канальчик для вкатунов ВЕК
- личный ютуб бложек про ваше IT