¿Cómo funciona en la práctica el sampling en distributed tracing — por qué no se puede trazar el 100% de los requests a gran escala?
A 50.000 requests por segundo, trazar cada uno multiplicaría varias veces tu factura de almacenamiento y red solo para guardar spans que nadie va a mirar jamás.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo funciona en la práctica el sampling en distributed tracing — por qué no se puede trazar el 100% de los requests a gran escala?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Por qué no trazar todo, directamente
Una sola traza de un request atravesando una arquitectura de microservicios moderna puede fácilmente contener docenas de spans — uno por cada salto entre servicios, uno por cada llamada a base de datos, uno por cada llamada HTTP saliente, uno por cada publicación en una cola. Cada span lleva un puñado de atributos clave-valor, timestamps, y una referencia a su padre. Eso no es gratis: cuesta CPU crearlo y serializarlo, ancho de banda de red exportarlo desde cada proceso hasta un collector, capacidad de ingesta en el collector mismo, y almacenamiento en el backend de tracing (Jaeger, Tempo, Honeycomb, X-Ray) — y a diferencia de una métrica, que se mantiene de tamaño fijo sin importar cuánto tráfico tengas, los datos de tracing crecen linealmente con el volumen de requests y con la profundidad de tu grafo de llamadas.
A una escala de producción significativa —decenas de miles de requests por segundo entre docenas de servicios— trazar cada request individual implicaría exportar y almacenar un volumen enorme y mayormente redundante de datos: la gran mayoría de los requests son rápidos y exitosos, y una traza de un request rápido y exitoso se ve casi idéntica a las últimas diez mil trazas rápidas y exitosas. Estás pagando el precio completo por guardar datos que casi no agregan información nueva. El sampling existe para gastar ese presupuesto en las trazas que realmente son informativas, en lugar de en todas por igual.
#Head-based sampling: decidir primero, no preguntar después
El head-based sampling (sampling basado en el origen) toma la decisión de guardar o descartar al inicio mismo de una traza —típicamente en el servicio de borde o en el SDK cliente que crea el span raíz— y propaga esa decisión a cada servicio downstream a través del contexto de la traza (el flag sampled dentro del header traceparent de W3C, o su equivalente en el formato de propagación que uses). Cada servicio downstream simplemente respeta el flag que recibió; no toma su propia decisión independiente.
La política más simple es probabilística: samplear X% de las trazas, elegidas mediante un hash determinístico del trace ID (no un sorteo aleatorio nuevo por servicio — eso haría que distintos servicios de la misma traza no coincidan sobre si está sampleada o no). Por eso escala tan bien: no requiere coordinación entre servicios, no hay buffering, y la decisión se toma una sola vez, de forma barata, antes de que ocurra ningún trabajo real.
from opentelemetry.sdk.trace import TracerProviderfrom opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased# Samplea el 5% de las trazas nuevas (raíz); cualquier traza que ya# llegó con un flag sampled=true desde un caller upstream se conserva# de todos modos, gracias al comportamiento por defecto de ParentBased# de respetar la decisión del padre.sampler = ParentBased(root=TraceIdRatioBased(0.05))provider = TracerProvider(sampler=sampler)
El problema del head-based sampling es que la decisión se toma antes de que nadie sepa si la traza va a ser interesante. Un sample plano del 5% captura abrumadoramente los mismos requests aburridos, rápidos y exitosos que componen más del 95% de tu tráfico, y tiene muy pocas probabilidades estadísticas de capturar el error raro o el único request que tardó 8 segundos en vez de 80 milisegundos — exactamente las trazas que realmente querrías tener cuando estás depurando un incidente.
#Tail-based sampling: decidir después de ver toda la historia
El tail-based sampling (sampling basado en el final) posterga la decisión hasta que la traza completa terminó —después de que llegaron todos los spans de todos los servicios—. Una capa dedicada, típicamente el processor tail_sampling del OpenTelemetry Collector, guarda en buffer todos los spans que pertenecen a un mismo trace ID durante una ventana de espera configurable, y recién ahí evalúa políticas contra la traza completa: siempre conservarla si contiene un error, siempre conservarla si la latencia total superó algún umbral, y para el resto aburrido, caer a una tasa probabilística baja.
receivers:otlp:protocols:grpc:http:processors:tail_sampling:decision_wait: 10snum_traces: 100000expected_new_traces_per_sec: 2000policies:- name: siempre-samplear-errorestype: status_codestatus_code:status_codes: [ERROR]- name: siempre-samplear-lentostype: latencylatency:threshold_ms: 500- name: base-probabilisticatype: probabilisticprobabilistic:sampling_percentage: 5exporters:otlp:endpoint: tempo:4317service:pipelines:traces:receivers: [otlp]processors: [tail_sampling]exporters: [otlp]
Esto te da trazas de fidelidad mucho más alta exactamente donde las necesitás —los errores y los requests lentos prácticamente siempre se conservan, en vez de sobrevivir solo por azar— a un costo de infraestructura real: cada span, de cada servicio, primero tiene que llegar al collector y quedarse en memoria durante toda la ventana de decision_wait antes de que se tome una decisión, lo cual significa que la capa de collectors necesita memoria suficiente para bufferear el volumen completo de trazas durante esa ventana, y si un pool de servicios balancea trazas entre múltiples instancias de collector, tiene que hacerlo de forma consistente por trace ID (no round-robin por span), o los spans de una misma traza terminan repartidos entre collectors que cada uno ve solo una parte de la historia.
#Cómo se ve una traza sampleada al lado de una descartada
Notá que en head-based sampling, los spans de una traza descartada igual se crean localmente —la instrumentación no sabe que la traza no está sampleada hasta que consulta el flag propagado— simplemente nunca se exportan, que es donde realmente ocurre el ahorro (red y almacenamiento, no el costo marginal de CPU de construir el objeto span en sí).
#Eligiendo entre ambos en la práctica
| Head-based | Tail-based | |
|---|---|---|
| Punto de decisión | Inicio de la traza (span raíz) | Final de la traza, después de bufferear todos los spans |
| Coordinación necesaria | Ninguna — determinístico por trace ID | Requiere ruteo consistente por trace ID hacia los collectors |
| Captura errores/trazas lentas de forma confiable | No — solo por azar | Sí — política explícita |
| Costo de infraestructura | Bajo (decisión barata, sin buffering) | Mayor (buffer en memoria, latencia agregada en el pipeline) |
| Uso típico | Base de alto volumen, sensible al costo | Se aplica encima, para priorizar trazas "interesantes" |
En sistemas de producción reales estos enfoques no son mutuamente excluyentes: un patrón común es usar head-based sampling en el SDK para limitar de forma barata cuántos datos siquiera llegan a la flota de collectors (para que el collector mismo no se sature), aplicando políticas tail-based sobre lo que sí llega, de modo que los errores y los requests lentos se capturen de forma casi exhaustiva mientras el caso normal se reduce.