8. Sampling: qué trazas guardar
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.
- Fácil de implementar.
- Bajo overhead.
- Problema: no podés samplear “todas las trazas con error” si la decisión se tomó antes de saber que iba a haber error.
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.
- Permite “siempre quedate con las trazas con error” o “con latencia > 1s”.
- Problema: requiere infra extra (collector con buffer big), latencia adicional, costo más alto.
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:
- Cada operación (servicio + endpoint) tiene una target rate.
- Jaeger ajusta dinámicamente el sampling rate de cada operación para acercarse a esa target.
- Operaciones de bajo volumen quedan totalmente sampleadas.
- Operaciones de alto volumen quedan capeadas.
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:
- Si la request llega con
traceparentindicandosampled=1→ se respeta y se sampela. - Si llega con
sampled=0→ se respeta el descarte. - Si no hay
traceparent→ se aplica probabilistic 10%.
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:
- Arrancá el collector con
--sampling.strategies-file=/etc/jaeger/sampling.json. - Habilitá adaptive sampling en el collector:
docker run \
-e SAMPLING_CONFIG_TYPE=adaptive \
-e SAMPLING_STORAGE_TYPE=memory \
jaegertracing/all-in-one:1.62
- Configurá la target rate por operación.
- Los SDKs en modo
remotevan 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:
- Toda traza con error → guardada.
- Toda traza con latencia > 1s → guardada.
- 1% del resto → guardado.
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ón | Estrategia |
|---|---|
| Desarrollo local | const=1 (todo) |
| Pre-producción / staging | Probabilistic 10-50% |
| Producción tráfico bajo (< 100 req/s) | Probabilistic 10-100% |
| Producción tráfico medio | Probabilistic 1-10% |
| Producción tráfico alto | Probabilistic 0.1-1% + tail-sampling errores/lentas |
| Crítico de negocio | Adaptive 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).