Rafael

Engenheiro de Software

Olá, Eu sou Rafael. Estou construindo coisas para a web e mobile.

Um desenvolvedor dedicado a criar experiências digitais de alto desempenho, acessíveis e limpas. Eu me especializo em transformar problemas complexos em soluções de engenharia elegantes.

Detectando goroutine leaks (vazamento de goroutines) no golang 1.27

No golang 1.27 podemos verificar goroutine leaks de uma forma nativa sem precisar de flags experimentais.

O que é uma goroutine leaks?

As *goroutines* sincronizam-se ou trocam informações por meio de primitivas de concorrência compartilhadas, como canais (*channels*), *locks* e *wait groups*. Ao se comunicarem, as *goroutines* frequentemente ficam bloqueadas nessas primitivas — ou seja, aguardam até que uma determinada condição seja atendida; exemplos comuns incluem esperar para adquirir um *mutex* que já está sendo utilizado ou aguardar o recebimento de uma mensagem em um canal. As *goroutines* também podem ficar bloqueadas em operações do sistema operacional, como a leitura de um *socket* de rede ou de um arquivo.

Podemos considerar que uma goroutine sofreu um vazamento se ela estiver bloqueada, mas as condições necessárias para desbloqueá-la nunca puderem ser atendidas. Com o tempo, o acúmulo de goroutines vazadas degrada o desempenho devido ao uso excessivo de memória (seja pelas próprias goroutines vazadas ou pela memória que elas referenciam), bem como ao consumo de CPU pelo coletor de lixo, especialmente se o `GOMEMLIMIT` estiver sendo utilizado.

O Go 1.27 introduz o *profiler* de vazamento de goroutines, um mecanismo flexível e leve para identificar vazamentos de goroutines em programas Go em execução, inclusive em sistemas de produção. Ao contrário de abordagens anteriores, que exigem análise humana, este mecanismo é preciso e gera poucos ou nenhum falso positivo. A contrapartida é que ele se limita a um subconjunto de vazamentos de goroutines: aquelas permanentemente bloqueadas em canais ou primitivas do pacote `sync`. Felizmente, isso já abrange uma parcela muito significativa dos vazamentos de goroutines, como veremos em nossos exemplos.

Vamos ver um exemplo prático

GO
package main

import (
	"fmt"
	"os"
	"runtime/pprof"
	"time"
)

func main() {
	buscarComVazamento()
	buscarSemVazamento()
	consumidorComVazamento()
	consumidorSemVazamento()

	fmt.Println("=== goroutineleak ===")
	pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
}

func buscarComVazamento() int {
	// canal SEM buffer
	ch := make(chan int)
	go func() {
		time.Sleep(100 * time.Millisecond)
		// VAZA: o timeout (10ms) já fez a função retornar, então ninguém
		// mais recebe de ch. Sem buffer, o envio bloqueia para sempre.
		ch <- 1
	}()

	// espera a goroutine chegar ao envio e ficar bloqueada, para o perfil
	// conseguir enxergá-la (só para demonstração)
	defer time.Sleep(150 * time.Millisecond)

	select {
	case v := <-ch:
		return v
	case <-time.After(10 * time.Millisecond):
		return -1
	}
}

func buscarSemVazamento() int {
	// canal COM buffer de 1: o envio completa mesmo sem receptor
	ch := make(chan int, 1)
	go func() {
		time.Sleep(100 * time.Millisecond)
		// NÃO VAZA: mesmo com o timeout já tendo ocorrido, o valor é
		// guardado no buffer, o envio retorna na hora e a goroutine termina.
		ch <- 1
	}()

	select {
	case v := <-ch:
		return v
	case <-time.After(10 * time.Millisecond):
		return -1
	}
}

func consumidorComVazamento() {
	ch := make(chan int)

	go func() {
		// VAZA: após receber 1 e 2, o range volta a esperar um novo valor.
		// Como ch nunca é fechado e ninguém mais o referencia, a goroutine
		// fica bloqueada aqui para sempre.
		for v := range ch {
			fmt.Println("vazando recebeu", v)
		}
		fmt.Println("vazando terminou") // nunca será impresso
	}()

	ch <- 1
	ch <- 2
	// falta close(ch): é isso que impede o range de terminar
}

func consumidorSemVazamento() {
	ch := make(chan int)

	go func() {
		// NÃO VAZA: o close(ch) abaixo faz o range terminar depois de
		// consumir todos os valores, e a goroutine retorna normalmente.
		for v := range ch {
			fmt.Println("ok recebeu", v)
		}
		fmt.Println("ok terminou")
	}()

	ch <- 1
	ch <- 2
	close(ch) //sinaliza "não há mais valores" e encerra o range
}

Lembramos que temos que estar usando a versão 1.27 do Go. Veja o código acima bem simples importamos o runtime do pprof e damos a saída no final do código pprof.Lookup(“goroutineleak”).WriteTo(os.Stdout, 1). Este exemplo vai printar no terminal:

vazando recebeu 1
vazando recebeu 2
ok recebeu 1
=== goroutineleak ===
ok recebeu 2
ok terminou
goroutineleak profile: total 2
1 @ 0x102805248 0x10279e224 0x10279de38 0x10285909c 0x10280b454
#       0x10285909b     main.buscarComVazamento.func1+0x3b      /Users/rafaelfernandosouzaribeiro/go/teste/goroutine-leak-profiles/cmd/exec-terminal/main.go:27

1 @ 0x102805248 0x10279f0e0 0x10279ec54 0x1028591a8 0x10280b454
#       0x1028591a7     main.consumidorComVazamento.func1+0x87  /Users/rafaelfernandosouzaribeiro/go/teste/goroutine-leak-profiles/cmd/exec-terminal/main.go:67

O log mais importante é a saída do goroutineleak profile: total 2 que mostra as duas funções com vazamento.

Agora vamos ver um exemplo em http

GO
package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof" // Registra os endpoints de debug automaticamente
)

// Endpoint com vazamento proposital
func leakyHandler(w http.ResponseWriter, r *http.Request) {
	ch := make(chan int)

	// Esta goroutine ficará travada para sempre tentando enviar para um canal sem leitor
	go func() {
		ch <- 42
	}()

	w.Write([]byte("Requisição processada (com vazamento!)"))

}

func main() {
	http.HandleFunc("/leaky", leakyHandler)

	fmt.Println("Servidor rodando em http://localhost:8080")
	// O pprof estará disponível em http://localhost:8080/debug/pprof/
	http.ListenAndServe(":8080", nil)
}

Neste exemplo temos só handler com vazamento, para conferir o leak você deve executar o endpoint: http://localhost:8080/leaky e depois verificar a análise: http://localhost:8080/debug/pprof/goroutineleak?debug=1 ou http://localhost:8080/debug/pprof/?debug=1.

Lembrando que importamos o net/http/pprof. O pacote expõe endpoints HTTP que permitem inspecionar o estado interno do processo Go em tempo de execução (profiling).

0 comentários

Deixe seu comentário