← Voltar para o blog

Desvendando Gargalos com o Gravador de Voo do Go: Diagnóstico Preciso de Problemas de Performance

Resumo em português do artigo oficial do Go Blog: Desvendando Gargalos com o Gravador de Voo do Go: Diagnóstico Preciso de Problemas de Performance

O artigo do blog oficial do Go introduz o “gravador de voo” (flight recorder), uma nova ferramenta de diagnóstico disponível a partir do Go 1.25, que permite capturar traces de execução (execution traces) de forma seletiva, focando em momentos críticos do programa. Ele complementa os execution traces já existentes, oferecendo uma abordagem mais direcionada para identificar e resolver problemas de performance em aplicações Go de longa duração, como servidores web.

Execution Traces: Uma Revisão

Os execution traces do Go registram eventos que ocorrem durante a execução de uma aplicação, detalhando a interação entre goroutines e o sistema operacional. Essa informação é valiosa para depurar problemas de latência, pois revela quando as goroutines estão em execução e, crucialmente, quando estão bloqueadas ou aguardando. A API runtime/trace permite iniciar e parar a coleta de traces através das funções runtime/trace.Start e runtime/trace.Stop.

No entanto, essa abordagem tem limitações para aplicações de longa duração. Coletar um trace completo de uma aplicação que roda por dias ou semanas geraria uma quantidade enorme de dados, dificultando a identificação do problema específico. A amostragem aleatória de traces (random sampling) é uma alternativa, mas exige infraestrutura complexa para armazenamento, triagem e processamento dos dados, muitos dos quais podem não ser relevantes.

O Gravador de Voo: Precisão Cirúrgica

O gravador de voo surge como uma solução mais eficiente para cenários onde o programa consegue detectar que algo deu errado, mas a causa raiz pode ter ocorrido em um momento anterior. Ele coleta o execution trace continuamente, mas armazena apenas os últimos segundos em um buffer na memória. Quando o programa detecta um problema, ele pode solicitar o conteúdo do buffer, capturando um snapshot (instantâneo) do período crítico que levou ao evento.

A analogia com um gravador de voo de um avião é apropriada: ele registra dados importantes para diagnosticar a causa de um incidente. No contexto do Go, o gravador de voo permite isolar o problema com precisão, evitando a análise de grandes volumes de dados irrelevantes.

Exemplo Prático: Diagnóstico de um Servidor HTTP Lento

O artigo ilustra o uso do gravador de voo com um exemplo de um servidor HTTP que implementa um jogo de “adivinhe o número”. O servidor possui um endpoint /guess-number que recebe um inteiro como palpite e responde ao cliente se ele acertou o número. Uma goroutine envia, a cada minuto, um relatório dos números adivinhados para outro serviço via HTTP.

O código original do servidor:

// bucket é um contador simples protegido por mutex.
type bucket struct {
	mu      sync.Mutex
	guesses int
}

func main() {
	// Cria um bucket para cada número válido que um cliente pode adivinhar.
	// O handler HTTP irá procurar o número adivinhado nos buckets
	// usando o número como um índice no slice.
	buckets := make([]bucket, 100)

	// A cada minuto, enviamos um relatório de quantas vezes cada número foi adivinhado.
	go func() {
		for range time.Tick(1 * time.Minute) {
			sendReport(buckets)
		}
	}()

	// Escolhe o número a ser adivinhado.
	answer := rand.Intn(len(buckets))

	http.HandleFunc("/guess-number", func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		// Busca o número da variável de query "guess" da URL e o converte
		// para um inteiro. Então, o valida.
		guess, err := strconv.Atoi(r.URL.Query().Get("guess"))
		if err != nil || !(0 <= guess && guess < len(buckets)) {
			http.Error(w, "invalid 'guess' value", http.StatusBadRequest)
			return
		}

		// Seleciona o bucket apropriado e incrementa seu valor com segurança.
		b := &buckets[guess]
		b.mu.Lock()
		b.guesses++
		b.mu.Unlock()

		// Responde ao cliente com o palpite e se ele estava correto.
		fmt.Fprintf(w, "guess: %d, correct: %t", guess, guess == answer)

		log.Printf("HTTP request: endpoint=/guess-number guess=%d duration=%s", guess, time.Since(start))
	})
	log.Fatal(http.ListenAndServe(":8090", nil))
}

// sendReport envia o estado atual dos buckets para um serviço remoto.
func sendReport(buckets []bucket) {
	counts := make([]int, len(buckets))

	for index := range buckets {
		b := &buckets[index]
		b.mu.Lock()
		defer b.mu.Unlock()

		counts[index] = b.guesses
	}

	// Serializa os dados do relatório para um payload JSON.
	b, err := json.Marshal(counts)
	if err != nil {
		log.Printf("failed to marshal report data: error=%s", err)
		return
	}
	url := "http://localhost:8091/guess-number-report"
	if _, err := http.Post(url, "application/json", bytes.NewReader(b)); err != nil {
		log.Printf("failed to send report: %s", err)
	}
}

Suponha que, após a implantação em produção, os usuários relatam que algumas chamadas ao endpoint /guess-number estão demorando mais do que o esperado. Os logs mostram que alguns tempos de resposta excedem 100 milissegundos, enquanto a maioria das chamadas leva apenas microssegundos.

Para diagnosticar o problema, o gravador de voo é integrado ao servidor. Primeiro, ele é configurado e iniciado na função main:

// Configura o gravador de voo
fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{
	MinAge:   200 * time.Millisecond,
	MaxBytes: 1 << 20, // 1 MiB
})
fr.Start()

MinAge define o tempo mínimo que os dados do trace são mantidos de forma confiável. O artigo recomenda configurá-lo para aproximadamente o dobro da janela de tempo do evento que está sendo depurado. MaxBytes limita o tamanho do buffer do trace para evitar consumo excessivo de memória. O artigo sugere que, em média, espera-se alguns MB de dados de trace por segundo de execução, ou 10 MB/s para um serviço com alta carga.

Em seguida, uma função auxiliar é adicionada para capturar o snapshot e gravá-lo em um arquivo:

var once sync.Once

// captureSnapshot captura um snapshot do gravador de voo.
func captureSnapshot(fr *trace.FlightRecorder) {
	// once.Do garante que a função fornecida seja executada apenas uma vez.
	once.Do(func() {
		f, err := os.Create("snapshot.trace")
		if err != nil {
			log.Printf("opening snapshot file %s failed: %s", f.Name(), err)
			return
		}
		defer f.Close() // ignora o erro

		// WriteTo grava os dados do gravador de voo no io.Writer fornecido.
		_, err = fr.WriteTo(f)
		if err != nil {
			log.Printf("writing snapshot to file %s failed: %s", f.Name(), err)
			return
		}

		// Para o gravador de voo após o snapshot ter sido tirado.
		fr.Stop()
		log.Printf("captured a flight recorder snapshot to %s", f.Name())
	})
}

Finalmente, antes de registrar um request completo, o snapshot é acionado se o request demorar mais de 100 milissegundos:

http.HandleFunc("/guess-number", func(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    // ... (código omitido) ...
    log.Printf("HTTP request: endpoint=/guess-number guess=%d duration=%s", guess, time.Since(start))
    if time.Since(start) > 100*time.Millisecond {
        captureSnapshot(fr)
    }
})

Analisando o trace capturado, é possível identificar que o problema de performance está relacionado à goroutine que envia o relatório dos números adivinhados. A mutex b.mu.Lock() no método sendReport está sendo mantida por um tempo excessivo, bloqueando o acesso aos buckets e causando lentidão nas chamadas ao endpoint /guess-number. A solução seria otimizar a lógica dentro da seção crítica ou utilizar uma estrutura de dados mais eficiente para o rastreamento dos palpites.

Implicações Práticas e Conclusão

O gravador de voo do Go oferece uma abordagem poderosa e direcionada para diagnosticar problemas de performance em aplicações Go. Ao permitir a captura seletiva de traces de execução, ele simplifica a identificação da causa raiz de problemas, economizando tempo e recursos. Ele é especialmente útil em cenários onde a aplicação consegue detectar a ocorrência de um problema, mas a causa pode estar em um ponto anterior da execução.

A ferramenta complementa as técnicas de tracing existentes e representa um avanço significativo nas capacidades de observabilidade do Go. Ao usar o gravador de voo, os desenvolvedores podem identificar e corrigir gargalos de performance com mais rapidez e precisão, resultando em aplicações mais eficientes e confiáveis. A configuração do MinAge e MaxBytes deve ser feita com cuidado para garantir que os dados relevantes sejam capturados sem sobrecarregar o sistema.


Artigo Original

Este e um resumo em português do artigo original publicado no blog oficial do Go.

Titulo original: Flight Recorder in Go 1.25

Leia o artigo completo em ingles no Go Blog

Autor original: Carlos Amedee and Michael Knyszek