OpenTelemetry OpenTelemetry

Controla costos con muestreo: configuración del SDK y del Collector

Decide cuándo muestrear en el SDK y cuándo usar tail sampling en un Collector de dos niveles.

15 min Avanzado

Conservar cada traza se vuelve caro rápidamente, y la mayoría son peticiones rutinarias que nadie abrirá nunca. El muestreo conserva un subconjunto representativo o interesante. OpenTelemetry ofrece dos lugares donde decidir: en el SDK cuando empieza una traza (head sampling) y en el Collector cuando la traza está completa (tail sampling). Ambos son pura configuración. Esta guía muestra cada uno y cómo elegir.

Head sampling en el SDK

La decisión se toma cuando se crea el span raíz, así que es barata y no necesita infraestructura adicional. No puede saber si la petición fallará o será lenta, porque todavía no ha ocurrido.

export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1

Los nombres de sampler definidos por la especificación incluyen always_on, always_off, traceidratio, parentbased_always_on (el predeterminado), parentbased_always_off y parentbased_traceidratio. Usa las variantes parentbased_: siguen la decisión de muestreo del padre de la petición entrante, de modo que una traza se conserva o se descarta como un todo entre servicios. Un traceidratio simple en cada servicio tomaría decisiones independientes y produciría trazas rotas.

En el archivo de configuración declarativa, la misma idea se ve así:

tracer_provider:
  sampler:
    parent_based:
      root:
        trace_id_ratio_based:
          ratio: 0.1

Consulta la documentación de tu SDK para los nombres de sampler que soporta en el formato de archivo.

Tail sampling en el Collector

El tail sampling espera hasta que una traza está completa y luego aplica políticas. Te permite conservar cada error y cada traza lenta más una pequeña parte del resto, algo que el head sampling no puede hacer. El costo es más infraestructura y más memoria.

processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 50000
    expected_new_traces_per_sec: 100
    policies:
      - name: keep-errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: keep-slow
        type: latency
        latency:
          threshold_ms: 2000
      - name: sample-the-rest
        type: probabilistic
        probabilistic:
          sampling_percentage: 5

Cómo funciona:

  • decision_wait es cuánto espera el processor tras ver el primer span de una traza antes de decidir. Debe ser mayor que la duración de tu traza más lenta, o decidirás sobre trazas parciales.
  • num_traces es cuántas trazas se mantienen en memoria mientras se espera. Dimensiónalo aproximadamente como tu tasa de llegada de trazas por decision_wait, con margen.
  • policies se evalúan para cada traza. Una traza se conserva si cualquier política dice que se conserve, así que las tres de arriba significan “conserva los errores, conserva las lentas y además conserva el 5 por ciento de todo”.
  • Otros tipos de política incluyen string_attribute (coincidir con un valor como el nivel de un cliente), numeric_attribute, always_sample, and (combinar políticas) y composite (asignar tasas entre políticas).

tail_sampling está en la distribución contrib, y los nombres de sus opciones evolucionan, así que consulta su README para tu versión.

La trampa: una traza debe llegar a un solo Collector

El tail sampling solo funciona si todos los spans de una traza llegan a la misma instancia del Collector. Con varias réplicas detrás de un balanceador de carga simple, los spans de una traza se dispersan y las decisiones se toman sobre fragmentos. La solución estándar son dos niveles:

  1. Nivel 1 (capa de balanceo de carga) usa el exporter loadbalancing para enrutar los spans por trace ID a una instancia consistente del nivel 2.
  2. Nivel 2 (capa de muestreo) ejecuta tail_sampling y exporta las trazas conservadas.

Nivel 1:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

exporters:
  loadbalancing:
    routing_key: traceID
    protocol:
      otlp:
        tls:
          insecure: true
    resolver:
      dns:
        hostname: otel-sampler-headless.observability.svc.cluster.local
        port: 4317

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [loadbalancing]

Nivel 2:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 20
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: keep-errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: sample-the-rest
        type: probabilistic
        probabilistic:
          sampling_percentage: 10
  batch:

exporters:
  otlp/backend:
    endpoint: tempo:4317

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlp/backend]

Usa un Service headless para que el DNS liste los pods del nivel 2, y ten en cuenta que cuando los pods escalan hacia arriba o hacia abajo el enrutamiento cambia, y unas pocas trazas en curso pueden decidirse con datos incompletos. Mantener las capas separadas también aísla los fallos: un problema en el muestreo no bloquea la ingesta.

Combinar head y tail

Una configuración común y sensata:

  • Un head sampler modesto en el SDK (por ejemplo 50 por ciento) para reducir el volumen en el origen en servicios de tráfico muy alto.
  • Tail sampling en el Collector para garantizar que los errores y las peticiones lentas sobrevivan.

Ten cuidado: lo que se descarta en el head no puede recuperarse en el tail. Si debes conservar todos los errores, no apliques head sampling agresivo a esos servicios.

Las métricas sobreviven al muestreo si las derivas primero

Descartar trazas también descarta los números que podrías calcular a partir de ellas, como las tasas de peticiones y los percentiles de latencia. Genera métricas antes del paso de muestreo con el connector spanmetrics, para que los dashboards sigan siendo precisos:

connectors:
  spanmetrics:

service:
  pipelines:
    traces/all:
      receivers: [otlp]
      exporters: [spanmetrics, loadbalancing]
    metrics/derived:
      receivers: [spanmetrics]
      processors: [batch]
      exporters: [otlphttp/prometheus]

Aquí cada span alimenta el connector, mientras que solo el conjunto muestreado continúa hacia el almacenamiento.

Cómo elegir

ObjetivoEnfoque
Reducir costos de forma simple, sin infraestructura nuevaHead sampling con parentbased_traceidratio
Conservar todos los errores y peticiones lentasTail sampling en un Collector de dos niveles
Mantener tasas y percentiles precisosspanmetrics antes del muestreo
Pocos servicios, bajo volumenSin muestreo; revísalo cuando importe el costo

Verifica

  • En el nivel 2, compara los recuentos de spans de entrada y salida con las propias métricas del Collector (puerto 8888) para confirmar que la proporción coincide con tus políticas.
  • Busca una petición fallida conocida y asegúrate de que su traza completa está presente.
  • Vigila la memoria del nivel 2, porque mantiene trazas durante decision_wait. Si crece, reduce num_traces o añade réplicas, y mantén memory_limiter delante.

Guías populares