Estratégias de tracing distribuído com Jaeger e Zipkin

1. Fundamentos do Tracing Distribuído em Arquiteturas Modernas

1.1. Conceitos-chave: spans, traces, context propagation e baggage

O tracing distribuído é uma técnica essencial para monitorar requisições que atravessam múltiplos serviços em arquiteturas de microserviços. Os conceitos fundamentais incluem:

  • Span: unidade básica de trabalho que representa uma operação específica dentro de um serviço, contendo nome, tempo de início, duração, tags e logs.
  • Trace: conjunto hierárquico de spans que representa o caminho completo de uma requisição através do sistema.
  • Context Propagation: mecanismo que transporta identificadores de trace entre serviços, geralmente via headers HTTP ou metadados de mensageria.
  • Baggage: pares chave-valor propagados ao longo de todo o trace, úteis para transportar informações contextuais como ID de usuário ou versão de deploy.

1.2. Desafios de microserviços: latência, dependências e debugging distribuído

Em sistemas distribuídos, identificar a causa raiz de lentidão ou falhas torna-se complexo. Os principais desafios incluem:

  • Latência cumulativa: cada serviço adiciona overhead de rede e processamento.
  • Dependências ocultas: chamadas indiretas entre serviços podem criar gargalos inesperados.
  • Debugging distribuído: logs centralizados não mostram a relação temporal entre operações em diferentes serviços.

1.3. Por que Jaeger e Zipkin são referências open source no mercado

Ambas as ferramentas seguem o modelo de dados do OpenTracing (hoje OpenTelemetry) e oferecem:

  • Visualização de traces em formato de waterfall charts.
  • Análise de dependências entre serviços.
  • Suporte a múltiplos backends de armazenamento (Elasticsearch, Cassandra, etc.).
  • Integração nativa com Kubernetes, Prometheus e outras ferramentas de observabilidade.

2. Arquitetura e Componentes do Jaeger

2.1. Agente, coletor, consulta e armazenamento: fluxo de dados end-to-end

O Jaeger possui quatro componentes principais:

[Serviço Instrumentado] → [Jaeger Agent] → [Jaeger Collector] → [Storage Backend]
                                                                    ↓
                                                           [Jaeger Query] → [UI]
  • Jaeger Agent: sidecar que recebe spans via UDP e os envia para o coletor.
  • Jaeger Collector: valida, indexa e armazena spans.
  • Jaeger Query: API REST/GraphQL para consulta de traces.
  • Storage Backend: Elasticsearch, Cassandra ou Kafka (para streaming).

2.2. Estratégias de amostragem: probabilística, rate-limiting e amostragem remota

O Jaeger oferece três estratégias de amostragem configuráveis:

# Amostragem probabilística (ex: 10% dos traces)
sampler.type = probabilistic
sampler.param = 0.1

# Amostragem rate-limiting (ex: 100 spans por segundo)
sampler.type = ratelimiting
sampler.param = 100

# Amostragem remota (via servidor de amostragem)
sampler.type = remote
sampler.param = {
  "sampling_server_url": "http://jaeger-agent:5778/sampling"
}

2.3. Integração com OpenTelemetry e suporte a múltiplos backends de storage

O Jaeger é compatível com o OpenTelemetry SDK, permitindo instrumentação padronizada:

import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter;
import io.opentelemetry.sdk.trace.SdkTracerProvider;

JaegerGrpcSpanExporter exporter = JaegerGrpcSpanExporter.builder()
    .setEndpoint("http://localhost:14250")
    .build();

SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
    .addSpanProcessor(SimpleSpanProcessor.create(exporter))
    .build();

3. Arquitetura e Componentes do Zipkin

3.1. Coleta via HTTP, Kafka ou Scribe: agentes e transportes suportados

O Zipkin suporta múltiplos transportes para coleta de spans:

# Coleta via HTTP (padrão)
POST /api/v2/spans HTTP/1.1
Host: zipkin:9411
Content-Type: application/json

[{"id":"abc123","traceId":"trace123","name":"get-user","timestamp":123456789,"duration":15000}]

# Coleta via Kafka
zipkin.collector.kafka.bootstrap-servers=localhost:9092
zipkin.collector.kafka.topic=zipkin

3.2. Modelo de dados: span model, trace tree e dependências entre serviços

O modelo de dados do Zipkin é JSON-based:

{
  "traceId": "abcef1234567890",
  "id": "span123",
  "parentId": "span000",
  "name": "http-get",
  "timestamp": 1234567890000,
  "duration": 15000,
  "tags": {
    "http.method": "GET",
    "http.status_code": "200"
  }
}

3.3. Zipkin UI: busca por traces, análise de latência e gráfico de dependências

A interface do Zipkin permite:

  • Busca por traces usando filtros como serviço, span, tags e duração mínima.
  • Waterfall chart mostrando a sequência temporal de spans.
  • Gráfico de dependências gerado automaticamente a partir dos dados coletados.

4. Estratégias de Instrumentação para Aplicações

4.1. Instrumentação automática vs manual: quando usar cada abordagem

  • Automática: ideal para frameworks web (Spring Boot, Express.js, Django) e bibliotecas HTTP/gRPC. Usa agentes ou middlewares que interceptam chamadas automaticamente.
  • Manual: necessária para operações de negócio específicas, como processamento de filas ou tarefas agendadas.

4.2. Propagação de contexto em chamadas síncronas (HTTP/gRPC) e assíncronas (mensageria)

Para propagação de contexto em chamadas HTTP:

// Extrair contexto do header
String traceId = request.getHeader("uber-trace-id");
SpanContext parentContext = JaegerSpanContext.fromString(traceId);

// Criar span filho
Span span = tracer.buildSpan("process-request")
    .asChildOf(parentContext)
    .start();

Para mensageria com Kafka:

// Producer: injetar contexto no header da mensagem
Headers headers = record.headers();
TextMapInjector injector = new TextMapInjectorAdapter(headers);
tracer.inject(span.context(), Format.Builtin.TEXT_MAP, injector);

// Consumer: extrair contexto
TextMapExtractor extractor = new TextMapExtractorAdapter(headers);
SpanContext parentContext = tracer.extract(Format.Builtin.TEXT_MAP, extractor);

4.3. Melhores práticas para nomear spans e adicionar tags relevantes

// Boas práticas de nomenclatura
span.setOperationName("POST /api/users");  // Método + caminho
span.setTag("user.id", userId);            // Identificador de negócio
span.setTag("error", true);                // Indicador de erro
span.setTag("http.status_code", 500);      // Código HTTP

5. Comparação Jaeger vs Zipkin: Casos de Uso e Trade-offs

5.1. Performance e escalabilidade: overhead de coleta e armazenamento

Característica Jaeger Zipkin
Overhead de coleta ~5-10μs por span ~3-8μs por span
Armazenamento padrão Elasticsearch Cassandra
Escalabilidade horizontal Suporte nativo via Kafka Suporte via Kafka

5.2. Recursos de análise: busca avançada, agregação e filtros

  • Jaeger: busca por tags, duração e serviço; agregação de métricas (latência p95, p99).
  • Zipkin: busca básica por serviço e span; filtros por tags e anotações.

5.3. Ecossistema e integrações: Kubernetes, Prometheus e ferramentas de observabilidade

Ambos integram-se com:

  • Kubernetes: sidecar injection para coleta automática.
  • Prometheus: exposição de métricas de coleta.
  • Grafana: dashboards de observabilidade.

6. Boas Práticas de Implantação e Operação

6.1. Configuração de amostragem para balancear custo e visibilidade

Para ambientes de produção:

# Amostragem adaptativa
sampler.type = remote
sampler.param = {
  "sampling_server_url": "http://jaeger-agent:5778/sampling",
  "max_traces_per_second": 50
}

# Amostragem baseada em prioridade
sampler.type = probabilistic
sampler.param = 0.01  # 1% para requisições normais
# 100% para requisições com header "X-Debug: true"

6.2. Estratégias de retenção de dados e tuning de storage

# Elasticsearch ILM para retenção
PUT _ilm/policy/tracing_policy
{
  "policy": {
    "phases": {
      "hot": {"min_age": "0ms", "actions": {"rollover": {"max_size": "50GB"}}},
      "delete": {"min_age": "30d", "actions": {"delete": {}}}
    }
  }
}

6.3. Monitoramento da própria infraestrutura de tracing

# Métricas essenciais para monitorar
- jaeger_collector_spans_received_total
- jaeger_collector_spans_dropped_total
- zipkin_collector_spans_received
- zipkin_collector_spans_dropped

7. Exemplos Práticos de Análise de Traces

7.1. Identificando gargalos de latência em uma cadeia de chamadas

Trace ID: abc123
  Span A (API Gateway): 500ms
    Span B (Auth Service): 200ms
      Span C (Database Query): 150ms  ← Gargalo identificado
    Span D (User Service): 300ms
      Span E (Cache Check): 250ms     ← Outro gargalo

7.2. Rastreamento de erros e exceções distribuídas com baggage

// Adicionar baggage para rastreamento de erros
tracer.activeSpan().setBaggageItem("error.source", "payment-service");
tracer.activeSpan().setBaggageItem("error.code", "ECONNREFUSED");

// Recuperar em serviços downstream
String errorSource = tracer.activeSpan().getBaggageItem("error.source");

7.3. Uso de trace analytics para otimizar performance de serviços críticos

// Query Jaeger para identificar traces lentos
curl -X GET "http://jaeger-query:16686/api/traces?service=payment-service&minDuration=1000ms&limit=100"

// Resultado: análise de latência p95
{
  "data": [
    {
      "traceID": "abc123",
      "spans": [
        {"operationName": "process-payment", "duration": 2500},
        {"operationName": "charge-card", "duration": 2000}
      ]
    }
  ]
}

Referências