Estratégias de sampling em tracing distribuído para reduzir custo

1. Fundamentos do Tracing Distribuído e o Problema do Custo

O tracing distribuído é a espinha dorsal da observabilidade em arquiteturas de microserviços. Cada requisição de usuário gera uma árvore de spans — unidades de trabalho que representam operações individuais — que, juntas, formam um trace completo. Em sistemas com dezenas ou centenas de serviços, uma única requisição pode produzir centenas de spans.

O problema surge quando cada requisição é rastreada integralmente. Em sistemas de alto throughput (milhares de requisições por segundo), o volume de dados de tracing pode facilmente atingir terabytes por dia. Armazenar, processar e consultar esses dados em ferramentas como Jaeger, Zipkin ou Grafana Tempo gera custos significativos de infraestrutura.

A relação é direta: quanto mais traces, maior o custo. A solução não é desligar o tracing, mas sim implementar estratégias inteligentes de sampling que reduzam o volume sem sacrificar a capacidade de diagnosticar problemas.

2. Conceitos Essenciais de Sampling

Head-based sampling decide no início da requisição se o trace será amostrado. É simples, mas pode perder eventos importantes que ocorrem depois da decisão.

Tail-based sampling adia a decisão até o final da requisição, permitindo analisar o trace completo antes de decidir. É mais preciso, mas consome mais recursos de buffer.

A taxa de amostragem fixa (ex.: 10% de todos os traces) é a abordagem mais simples, mas desperdiça recursos ao capturar traces normais enquanto perde outliers importantes.

A amostragem probabilística usa um hash do trace ID para determinar consistência — o mesmo trace é sempre amostrado ou não, independentemente de qual serviço o processa.

3. Estratégias Head-Based para Redução de Custo

Sampling por probabilidade uniforme

Implementação direta: cada serviço amostra uma fração fixa dos traces.

// Configuração de sampler head-based com taxa de 10%
// Em OpenTelemetry SDK (exemplo conceitual)
Sampler sampler = new TraceIdRatioBasedSampler(0.1);
TracerProvider tracerProvider = SdkTracerProvider.builder()
    .setSampler(sampler)
    .build();

Limitação: captura 10% de tudo, incluindo 90% de requisições normais que poderiam ser descartadas.

Sampling baseado em endpoints

Prioriza endpoints críticos (checkout, login) e reduz amostragem de endpoints de baixa relevância (health checks, consultas de catálogo).

// Lógica de sampler head-based por endpoint
function shouldSample(request):
    if request.path starts with "/api/pagamento":
        return true  // 100% para pagamentos
    if request.path starts with "/api/consulta":
        return random() < 0.01  // 1% para consultas
    return random() < 0.05  // 5% padrão

Sampling adaptativo por latência ou erro

Amostragem dinâmica baseada em métricas em tempo real: aumenta a taxa quando a latência ou taxa de erro sobe.

// Sampler adaptativo head-based
function shouldSample(request):
    errorRate = getCurrentErrorRate()
    if errorRate > 0.05:  // 5% de erro
        return random() < 0.5  // 50% de amostragem
    latencyP99 = getCurrentLatencyP99()
    if latencyP99 > 2000:  // 2 segundos
        return random() < 0.3  // 30% de amostragem
    return random() < 0.05  // 5% normal

4. Estratégias Tail-Based para Precisão e Economia

O sampling tail-based usa um buffer para armazenar spans temporariamente e decide após o trace ser concluído quais serão persistidos.

Amostragem por erro e exceção

Garante que 100% dos traces com erro sejam armazenados, mesmo que a taxa geral de amostragem seja baixa.

// Regra tail-based: reter todos os traces com erro
if trace.hasErrors():
    storeCompleteTrace()
else:
    if random() < 0.05:
        storeCompleteTrace()
    else:
        discardTrace()

Sampling por duração de requisição

Foco em outliers de performance — traces que excedem thresholds de latência.

// Regra tail-based por duração
if trace.duration > 5000ms:  // 5 segundos
    storeCompleteTrace()  // 100% dos lentos
elif trace.duration > 1000ms:
    storeWithProbability(0.5)  // 50% dos moderados
else:
    storeWithProbability(0.01)  // 1% dos normais

5. Sampling Híbrido: Combinando Head e Tail

A abordagem mais eficiente combina os dois estágios:

  1. Head-based reduz volume inicial (ex.: 20% de amostragem uniforme)
  2. Tail-based refina a seleção, priorizando traces valiosos
// Arquitetura de dois estágios
// Estágio 1: Head-based reduz para 20%
Sampler headSampler = new TraceIdRatioBasedSampler(0.2);

// Estágio 2: Tail-based no coletor
// Regras no OpenTelemetry Collector
tail_sampling:
  policies:
    - name: error-policy
      type: status_code
      status_codes: [ERROR]
      sampling_percentage: 100
    - name: latency-policy
      type: latency
      threshold_ms: 3000
      sampling_percentage: 100
    - name: probabilistic-policy
      type: probabilistic
      sampling_percentage: 5

Exemplo prático: Em um sistema de e-commerce, traces de pagamento são amostrados a 100% no head, enquanto consultas de produtos são amostradas a 1%. No tail, traces com erro ou latência alta são promovidos para armazenamento completo.

6. Implementação Prática com OpenTelemetry

Configuração de sampler customizado no SDK OpenTelemetry:

// Sampler customizado head-based com lógica de negócio
class BusinessSampler implements Sampler {
    @Override
    public SamplingResult shouldSample(
            Context parentContext, 
            TraceId traceId, 
            String name, 
            SpanKind spanKind, 
            Attributes attributes, 
            List<Link> parentLinks) {

        String path = attributes.get("http.target");

        // Prioridade para endpoints críticos
        if (path != null && path.startsWith("/api/payment")) {
            return SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE);
        }

        // Amostragem reduzida para health checks
        if (path != null && path.equals("/health")) {
            return SamplingResult.create(SamplingDecision.DROP);
        }

        // Amostragem probabilística padrão
        if (Math.random() < 0.05) {
            return SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE);
        }

        return SamplingResult.create(SamplingDecision.DROP);
    }
}

Integração com coletores para sampling distribuído:

// Configuração de pipeline no OpenTelemetry Collector
receivers:
  otlp:
    protocols:
      grpc:

processors:
  tail_sampling:
    decision_wait: 30s
    num_traces: 100000
    expected_new_traces_per_sec: 1000
    policies:
      - name: error-policy
        type: status_code
        status_codes: [ERROR]
        sampling_percentage: 100
      - name: slow-policy
        type: latency
        threshold_ms: 2000
        sampling_percentage: 100
      - name: probabilistic-policy
        type: probabilistic
        sampling_percentage: 5

exporters:
  jaeger:
    endpoint: jaeger:14250

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [tail_sampling]
      exporters: [jaeger]

7. Monitoramento e Ajuste Contínuo da Estratégia

Para avaliar a eficácia do sampling, monitore:

  • Taxa de cobertura: porcentagem de serviços cobertos por traces amostrados
  • Falsos negativos: incidentes que não foram capturados pelo sampling
  • Custo de armazenamento: redução real em GB/dia

Ferramentas como o Jaeger Query permitem analisar a distribuição de traces amostrados versus descartados.

Ciclo de feedback: após um incidente, verifique se o trace foi capturado. Se não, ajuste a taxa de amostragem para o endpoint ou serviço relevante.

8. Casos de Uso e Boas Práticas para Redução de Custo

Cenário 1: Sistema de alta throughput (10.000 req/s)

  • Head-based: 2% de amostragem uniforme
  • Tail-based: 100% para erros e latência > 1s
  • Resultado: redução de 98% no volume, mantendo visibilidade de problemas

Cenário 2: Sistema crítico (pagamentos, 500 req/s)

  • Head-based: 100% para endpoints de pagamento
  • Tail-based: 100% para erros, 50% para latência > 500ms
  • Resultado: custo alto mas justificado pela criticidade

Checklist para ajuste de taxa

Situação Ação
Muitos falsos negativos em incidentes Aumentar taxa de amostragem para endpoints afetados
Custo de armazenamento muito alto Reduzir taxa uniforme e aumentar dependência de tail-based
Serviço com baixa taxa de erro Reduzir amostragem head-based para 1-2%
Lançamento de nova funcionalidade Aumentar temporariamente para 50-100%

Referências