Skip to content

rafaelmfried/tracing-go

Repository files navigation

Tracing na prática: queries síncronas vs goroutines

Material de aula sobre distributed tracing com OpenTelemetry, Tempo e Grafana — usando uma API Go que executa as mesmas três queries de duas formas diferentes para tornar o ganho de performance da concorrência visível no trace.

Autor: Rafael Friederick — Unnamed-Lab


1. Objetivo da aula

Mostrar, com algo que o aluno possa rodar, abrir no Grafana e tocar, três coisas:

  1. O que é um trace — uma árvore de spans que descreve uma requisição ponta a ponta.
  2. Como o tracing torna gargalos óbvios — quando você vê os spans no tempo, sequencial vs paralelo deixa de ser teoria.
  3. Quando vale paralelizar I/O com goroutines em Go — e quando não vale.

Não é uma aula de Go puro nem de SQL. É uma aula sobre observabilidade aplicada: o mesmo trabalho, executado de duas formas, e o trace contando a história.


2. A demo

A API expõe duas rotas que fazem exatamente o mesmo trabalho: três queries lentas no Postgres (cada uma é um pg_sleep parametrizado, simulando um I/O remoto).

Rota Como executa Duração esperada
GET /sync Sequencial — uma query depois da outra ≈ N × QUERY_SLEEP_SECONDS
GET /parallel Concorrente — errgroup + goroutines ≈ 1 × QUERY_SLEEP_SECONDS

Com QUERY_SLEEP_SECONDS=1 e 3 queries: /sync ≈ 3s, /parallel ≈ 1s.

No trace, o contraste é literal:

/sync       [ q1 ][ q2 ][ q3 ]      ← spans em série
/parallel   [ q1 ]
            [ q2 ]                  ← spans empilhados, sobrepostos no tempo
            [ q3 ]

3. Stack

  • Go 1.26 — Gin + pgx (sem ORM)
  • PostgreSQL 18
  • OpenTelemetry SDK (OTLP/HTTP) → TempoGrafana
  • Prometheus (compose já provisionado para extensão futura)

Instrumentação:

  • HTTP: otelgin (span por request)
  • DB: otelpgx (span por query, com db.statement)
  • App: otel.Tracer manual em RunSequential / RunParallel para agrupar os filhos

Documentação:

  • OpenAPI/Swagger gerados via swaggo/swag a partir de annotations nos handlers — make swagger regenera docs/

4. Subindo o ambiente

A infra (Postgres, Tempo, Grafana, Prometheus) vive no monorepo paihttps://github.com/rafaelmfried/tracing. Este submódulo contém apenas a aplicação Go.

Para rodar a stack completa (esta API + Postgres + Tempo + Grafana):

# Do monorepo (um nível acima):
cd ..
make up go            # sobe Postgres + Tempo + esta API
# OU:
make up all           # sobe também Node, Grafana, Prometheus

Para rodar o binário Go localmente (precisa de um Postgres acessível):

# Do monorepo, suba a infra:
cd .. && make up infra && make up obs && cd go
# Depois, rode a API local:
make build-go
DATABASE_URL=postgres://tracing:tracing@localhost:5433/tracing?sslmode=disable \
  OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
  ./build/api

Endereços (defaults):


5. Roteiro da aula

5.1. Bater nas duas rotas

curl -s localhost:8080/sync     | jq
curl -s localhost:8080/parallel | jq

A resposta JSON inclui total_ms — o aluno já vê a diferença antes de abrir o Grafana. Use isso para introduzir a pergunta:

"Por que /parallel é ~3× mais rápido se executa as mesmas três queries?"

5.2. Abrir o trace no Grafana

  1. Grafana → Explore → datasource Tempo → aba Search.
  2. Service: tracing-api. Filtrar por name=GET /sync e name=GET /parallel.
  3. Abrir um trace de cada lado, lado a lado.

O que mostrar:

  • O waterfall dos spans. Em /sync, cada query.* começa onde o anterior terminou. Em /parallel, todos começam praticamente juntos.
  • O span do pgx (SELECT ...) com db.statement — tracing dá observabilidade do SQL real, não só do código Go.
  • O span pai queries.sequential / queries.parallel com a duração total.

5.3. Discussão (trade-offs)

Goroutines não são gratuitas. Pontos para puxar com a turma:

  • Pool de conexões/parallel só ganha se houver conexões livres. Se o pool tem MaxConns=1, o paralelismo desaparece (vire MaxConns em internal/infra/db/postgres.go e mostre o trace de novo).
  • Propagação de contextoerrgroup.WithContext cancela as goroutines irmãs quando uma falha. Mostre derrubando o Postgres no meio.
  • Ordem dos resultadosRunParallel preserva a ordem por índice, não por tempo de chegada. Importante quando a saída é lida por outro sistema.
  • Quando NÃO paralelizar — quando há dependência entre as queries (q2 precisa do resultado de q1), goroutines não ajudam. Tracing torna essa dependência visível se ela existir.
  • Limites do paralelismoerrgroup.SetLimit(N) para evitar saturar o pool/DB downstream.

6. Exercícios sugeridos

  1. Mexer no QUERY_SLEEP_SECONDS (no .env) para 0.2s e 5s. Comparar total_ms e o waterfall.
  2. Adicionar uma quarta query que dependa do resultado da primeira — observar que /parallel perde a vantagem nessa parte específica.
  3. Reduzir MaxConns do pool para 1 e observar que /parallel fica equivalente a /sync no trace.
  4. Adicionar um span manual dentro de runQuery antes do pool.QueryRow para isolar "tempo de espera por conexão" vs "tempo de query no servidor".
  5. Fazer 100 chamadas concorrentes em cada rota com hey ou k6 e comparar p50/p95 — onde o paralelismo dentro do request começa a competir com o paralelismo entre requests?

7. Estrutura do projeto

.
├── cmd/api/main.go                       # entrypoint, wiring, graceful shutdown
├── internal/
│   ├── app/queries.go                    # RunSequential, RunParallel, runQuery
│   └── infra/
│       ├── db/postgres.go                # pgxpool + otelpgx tracer
│       ├── http/handlers.go              # Gin router, /sync e /parallel
│       └── observability/tracing.go      # OTLP/HTTP exporter para Tempo
├── docs/                                 # OpenAPI/Swagger gerado por `make swagger`
├── docker/
│   └── Dockerfile                        # build da app Go (multi-stage)
└── Makefile                              # make test / build / swagger

8. Referências


© Rafael Friederick — Unnamed-Lab. Material livre para uso educacional, com atribuição.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors