8. Sampling: qué trazas guardar

Por: Artiko
jaegersamplingperformancetail-sampling

8. Sampling: qué trazas guardar

A volumen bajo guardás todas las trazas y listo. A 50.000 requests por segundo, guardar todo significa terabytes por día y un storage que se cae. Sampling es decidir qué trazas se guardan y cuáles se descartan.


Head-based vs tail-based

flowchart LR
    subgraph head[Head-based sampling]
        H1[Decisión al<br/>iniciar el trace] --> H2[Aplica a todos<br/>los spans del trace]
    end
    subgraph tail[Tail-based sampling]
        T1[Trace completo<br/>en buffer] --> T2[Decisión cuando<br/>termina]
        T2 --> T3[Descarta o<br/>persiste]
    end

Head-based

La decisión de samplear o no se toma al inicio del trace, en el primer servicio. Esa decisión viaja en el header traceparent (bit 01 = sampled). Todos los servicios downstream respetan la decisión.

Tail-based

El trace completo se mantiene en buffer (en una capa intermedia, típicamente OpenTelemetry Collector). Cuando termina, se decide si se persiste según el resultado.

Jaeger v1 hace head-based. Para tail-based usás OpenTelemetry Collector con tail_sampling processor delante de Jaeger (lo cubrimos al final del capítulo).


Estrategias head-based en Jaeger

Probabilistic (la más común)

Cada nuevo trace tiene probabilidad P de ser sampleado.

samplingRate=0.1  → 10% de las trazas se guardan
samplingRate=0.01 → 1%
samplingRate=1.0  → todo (default en dev)

Pros: simple, predecible, distribución estadísticamente representativa.

Contras: si tenés tráfico bajo, podés terminar guardando muy poco. Si tenés 1000 endpoints distintos, los menos usados quedan invisibles.

Rate-limiting

Tope de N trazas por segundo, sin importar el volumen entrante.

maxTracesPerSecond=10

Pros: predecible para capacity planning. No te sorprende un pico de tráfico.

Contras: a tráfico alto, el % efectivo es minúsculo y la distribución se sesga (las primeras N de cada segundo).

Const

Decisión fija: siempre true (todo) o siempre false (nada). Útil para debug local o para apagar tracing en un servicio específico.

Adaptive (la más sofisticada)

Combina probabilistic con rate-limiting por servicio + operación:

Pros: mejor distribución entre endpoints, especialmente útil cuando tenés alta heterogeneidad.

Contras: requiere que el collector esté en modo “remote sampling provider” — más config.


Configurar sampling en el SDK

Probabilistic 10% (Node.js)

const { TraceIdRatioBasedSampler } = require('@opentelemetry/sdk-trace-base');

new NodeSDK({
  sampler: new TraceIdRatioBasedSampler(0.1),
  // ...
});

Por variable de entorno

export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1

Sampler basado en parent

Lo más común en producción:

export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1

Significa:

Esta es la configuración correcta para servicios downstream que reciben tráfico desde un edge.


Sampling remoto (configuración centralizada)

Lo problemático del sampling en el SDK: si querés cambiarlo, tenés que redeployar todos los servicios.

Sampling remoto delega la decisión a una configuración servida por Jaeger:

flowchart LR
    APP[App] -->|"GET /sampling?service=X"| C[Collector\n:5778]
    C -->|JSON con strategy| APP
    APP -->|"sampling rate aplicado"| T[Trazas]

sampling.json

Arrancás el collector con un archivo de estrategias:

{
  "service_strategies": [
    {
      "service": "checkout-service",
      "type": "probabilistic",
      "param": 0.5
    },
    {
      "service": "health-check",
      "type": "ratelimiting",
      "param": 1
    },
    {
      "service": "order-service",
      "type": "probabilistic",
      "param": 0.1,
      "operation_strategies": [
        {
          "operation": "GET /api/orders",
          "type": "probabilistic",
          "param": 0.05
        },
        {
          "operation": "POST /api/orders",
          "type": "probabilistic",
          "param": 1.0
        }
      ]
    }
  ],
  "default_strategy": {
    "type": "probabilistic",
    "param": 0.01
  }
}
docker run -d --name jaeger \
  -e SAMPLING_STRATEGIES_FILE=/etc/jaeger/sampling.json \
  -v $PWD/sampling.json:/etc/jaeger/sampling.json \
  -p 5778:5778 -p 16686:16686 -p 4317:4317 \
  jaegertracing/all-in-one:1.62

En el SDK

export OTEL_TRACES_SAMPLER=parentbased_jaeger_remote
export OTEL_TRACES_SAMPLER_ARG="endpoint=http://jaeger:5778,pollingIntervalMs=5000,initialSamplingRate=0.1"

El SDK pollea cada 5s y aplica la última config. Cambiás el sampling.json y los servicios se enteran sin redeploy.


Adaptive sampling

Pasos:

  1. Arrancá el collector con --sampling.strategies-file=/etc/jaeger/sampling.json.
  2. Habilitá adaptive sampling en el collector:
docker run \
  -e SAMPLING_CONFIG_TYPE=adaptive \
  -e SAMPLING_STORAGE_TYPE=memory \
  jaegertracing/all-in-one:1.62
  1. Configurá la target rate por operación.
  2. Los SDKs en modo remote van a recibir las strategies dinámicas.

Adaptive funciona mejor con storage compartido (Cassandra) entre múltiples collectors para que coordinen las decisiones.


Tail-based con OpenTelemetry Collector

Si necesitás “siempre samplear errores” o “siempre samplear traces > 1s”, insertá un OTel Collector con tail_sampling processor entre tus apps y Jaeger:

flowchart LR
    APP[Apps] -->|OTLP\n100% sampled| OC[OTel Collector\ntail_sampling]
    OC -->|filtrado| J[Jaeger]

otel-collector-config.yaml:

receivers:
  otlp:
    protocols:
      grpc:

processors:
  tail_sampling:
    decision_wait: 30s
    num_traces: 100000
    expected_new_traces_per_sec: 10
    policies:
      - name: errors
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow
        type: latency
        latency: { threshold_ms: 1000 }
      - name: probabilistic
        type: probabilistic
        probabilistic: { sampling_percentage: 1 }

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls: { insecure: true }

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

Tres políticas en este ejemplo:

Costo: el collector debe mantener todos los spans de un trace en buffer hasta que termine. Memoria proporcional a num_traces × spans_por_trace × tamaño_promedio.


¿Qué estrategia elegir?

SituaciónEstrategia
Desarrollo localconst=1 (todo)
Pre-producción / stagingProbabilistic 10-50%
Producción tráfico bajo (< 100 req/s)Probabilistic 10-100%
Producción tráfico medioProbabilistic 1-10%
Producción tráfico altoProbabilistic 0.1-1% + tail-sampling errores/lentas
Crítico de negocioAdaptive con remote sampling

Empezá simple: probabilistic head-based con sampling remoto. Si aparece la necesidad real de capturar todos los errores y todas las trazas lentas, agregás tail-sampling.


Métrica clave

jaeger_sampler_decisions_total{sampled="true|false"}

Si descartás el 99% pero te das cuenta meses después que hubo algún incidente y no podés debuggearlo porque sus trazas se perdieron, bajaste demasiado el sampling. Ajustá.


¿Qué viene?

Las trazas están llegando, sampleadas correctamente. Toca aprender a leerlas. En el próximo capítulo recorremos la UI: search, timeline, comparación, system architecture y monitor (SPM).