Стратегии сэмплирования: от простого к adaptive
Стратегии сэмплирования: от простого к adaptive
Проблема: 100 000 trace/sec
Представьте: ваш сервис обрабатывает 100 000 запросов в секунду. Если отправлять каждый trace в Jaeger:
- Объём данных: ~1 ТБ в день (при среднем размере trace 100 KB)
- Стоимость хранения: $30-50/ТБ/месяц = ~$1000+/месяц
- Latency overhead: каждый span отправляется по сети
- Backend перегружен: Jaeger/Tempo не справляются
Решение: Сэмплирование (Sampling) — отправлять только часть traces.
Но как выбрать, какие traces сохранить?
Что такое Sampling?
Sampling — это процесс принятия решения: отправлять trace в backend или нет.
100 requests
│
├─ trace_1 ✅ sampled (отправлен)
├─ trace_2 ❌ not sampled
├─ trace_3 ❌ not sampled
├─ trace_4 ✅ sampled
├─ trace_5 ❌ not sampled
...
└─ trace_100 ❌ not sampled
Результат: 2% traces отправлены в JaegerКлючевой вопрос: Как не потерять важные traces (ошибки, медленные запросы)?
Head-based vs Tail-based Sampling
Head-based Sampling (решение в начале trace)
Момент решения: При создании root span (первый span в trace).
Преимущества:
- Низкая latency — решение мгновенное
- Низкий overhead — не нужно хранить spans в памяти
- Простая реализация
Недостатки:
- Не знаем будущее — можем пропустить важные ошибки
- Сложно сэмплировать редкие события
Request начинается → Создаём trace → Решаем: sampled=true/false
↓
Если false — все spans игнорируютсяTail-based Sampling (решение в конце trace)
Момент решения: После завершения всех spans в trace.
Преимущества:
- Знаем всю картину — можем сэмплировать по ошибкам, latency
- Гарантируем сохранение важных traces
Недостатки:
- Высокий overhead — нужно буферизовать все spans
- Сложная архитектура — требуется OpenTelemetry Collector
Request → Spans буферизуются → Trace завершён → Анализируем
↓
Решаем: отправить или нетВ этом уроке: Фокус на head-based sampling (tail-based в уроке 09).
OpenTelemetry Samplers: Head-based
1. AlwaysOn Sampler (100% sampling)
Использование: Development, debugging, low-traffic сервисы.
// Node.js
const { AlwaysOnSampler } = require("@opentelemetry/sdk-trace-base");
const sdk = new NodeSDK({
sampler: new AlwaysOnSampler(),
// ...
});# Python
from opentelemetry.sdk.trace.sampling import AlwaysOnSampler
provider = TracerProvider(
sampler=AlwaysOnSampler()
)// Go
import "go.opentelemetry.io/otel/sdk/trace"
tp := trace.NewTracerProvider(
trace.WithSampler(trace.AlwaysSample()),
)Результат: Все traces отправляются в backend.
2. AlwaysOff Sampler (0% sampling)
Использование: Отключение трассировки без изменения кода.
const { AlwaysOffSampler } = require("@opentelemetry/sdk-trace-base");
const sdk = new NodeSDK({
sampler: new AlwaysOffSampler(),
});Результат: Никакие traces не отправляются.
Важно: Spans всё равно создаются (overhead ~100ns), просто не экспортируются.
3. TraceIdRatioBased Sampler (Probability Sampling)
Принцип: Сэмплируем X% traces на основе trace_id.
Как работает:
trace_id = "4bf92f3577b34da6a3ce929d0e0e4736"
1. Берём младшие 8 байт trace_id
2. Конвертируем в число от 0 до 1
3. Сравниваем с порогом (ratio)
Если число < ratio → sampled = trueПример: 10% sampling
const { TraceIdRatioBasedSampler } = require("@opentelemetry/sdk-trace-base");
const sdk = new NodeSDK({
sampler: new TraceIdRatioBasedSampler(0.1), // 10%
});from opentelemetry.sdk.trace.sampling import TraceIdRatioBasedSampler
provider = TracerProvider(
sampler=TraceIdRatioBasedSampler(0.1) # 10%
)tp := trace.NewTracerProvider(
trace.WithSampler(trace.TraceIDRatioBased(0.1)), // 10%
)Результат:
100 requests → ~10 traces в JaegerПреимущества:
- Детерминированность: один и тот же trace_id всегда даёт одно решение
- Консистентность: если root span sampled, все child spans тоже sampled
Недостатки:
- Можем пропустить редкие ошибки
- Высоконагруженные endpoints и low-traffic endpoints семплируются одинаково
4. ParentBased Sampler (наследование от родителя)
Принцип: Если parent span sampled, child span тоже sampled.
Зачем? Гарантия полноты trace — не потерять часть spans.
const {
ParentBasedSampler,
TraceIdRatioBasedSampler,
AlwaysOnSampler,
} = require("@opentelemetry/sdk-trace-base");
const sdk = new NodeSDK({
sampler: new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.1), // Root spans: 10%
remoteParentSampled: new AlwaysOnSampler(), // Если parent sampled → всегда sample
remoteParentNotSampled: new AlwaysOffSampler(), // Если parent not sampled → не sample
}),
});Сценарий:
API Gateway (root span):
- Решение: TraceIdRatio(0.1) → sampled = true
↓ HTTP request (propagates sampled=true)
Auth Service (child span):
- Решение: parent sampled → sampled = true
↓ HTTP request (propagates sampled=true)
User Service (child span):
- Решение: parent sampled → sampled = trueРезультат: Весь trace сохранён полностью.
5. Custom Samplers: Sampling по условиям
Сценарий: Хотим сэмплировать:
- 100% ошибок (status_code >= 400)
- 100% медленных запросов (>1s)
- 1% остальных
Проблема: Head-based sampling не знает будущее (ошибку/latency).
Решение: Комбинация head-based + parent-based + manual override.
Пример: Custom Sampler (Node.js)
const { Sampler, SamplingDecision } = require("@opentelemetry/sdk-trace-base");
class ErrorAwareSampler {
constructor(baseSampler) {
this.baseSampler = baseSampler;
}
shouldSample(context, traceId, spanName, spanKind, attributes, links) {
// 1. Проверяем атрибуты (если доступны на момент создания)
const httpStatus = attributes["http.status_code"];
if (httpStatus && httpStatus >= 400) {
// Ошибка → всегда sample
return {
decision: SamplingDecision.RECORD_AND_SAMPLED,
};
}
// 2. Для остальных — используем base sampler
return this.baseSampler.shouldSample(
context,
traceId,
spanName,
spanKind,
attributes,
links
);
}
}
const sdk = new NodeSDK({
sampler: new ErrorAwareSampler(new TraceIdRatioBasedSampler(0.01)), // 1% base
});Ограничение: Атрибуты доступны только если установлены до создания span.
Rate Limiting Sampler
Проблема TraceIdRatio: При спайке трафика (100k → 1M req/sec), количество traces тоже вырастает в 10 раз.
Решение: Rate Limiting — ограничить количество traces/sec.
Пример (Custom Sampler):
class RateLimitingSampler {
constructor(maxTracesPerSecond) {
this.maxTracesPerSecond = maxTracesPerSecond;
this.tracesThisSecond = 0;
this.currentSecond = Math.floor(Date.now() / 1000);
}
shouldSample(context, traceId, spanName, spanKind, attributes, links) {
const now = Math.floor(Date.now() / 1000);
// Новая секунда — сброс счётчика
if (now !== this.currentSecond) {
this.currentSecond = now;
this.tracesThisSecond = 0;
}
// Проверяем лимит
if (this.tracesThisSecond < this.maxTracesPerSecond) {
this.tracesThisSecond++;
return { decision: SamplingDecision.RECORD_AND_SAMPLED };
}
return { decision: SamplingDecision.NOT_RECORD };
}
}
const sdk = new NodeSDK({
sampler: new RateLimitingSampler(100), // Максимум 100 traces/sec
});Результат:
Traffic: 10k req/sec → 100 traces/sec в Jaeger
Traffic: 100k req/sec → 100 traces/sec в Jaeger (защита)Jaeger Adaptive Sampling
Проблема: Статическое сэмплирование (10%) не учитывает:
- Разную нагрузку на разные endpoints (
/healthvs/checkout) - Изменения трафика со временем
Решение: Jaeger Remote Sampling — backend динамически управляет sampling rates.
Как работает Adaptive Sampling
1. Jaeger Collector анализирует поступающие traces
2. Вычисляет статистику: req/sec, error rate по каждому endpoint
3. Отдаёт клиенту sampling strategies через HTTP API
Client периодически запрашивает:
GET http://jaeger-collector:5778/sampling?service=api-gateway
Response:
{
"strategyType": "probabilistic",
"probabilisticSampling": {
"samplingRate": 0.1
},
"operationSampling": {
"perOperationStrategies": [
{
"operation": "GET /health",
"probabilisticSampling": {"samplingRate": 0.001}
},
{
"operation": "POST /checkout",
"probabilisticSampling": {"samplingRate": 0.5}
}
]
}
}Результат:
/health— 0.1% (низкая важность)/checkout— 50% (критичный endpoint)
Настройка Jaeger Adaptive Sampling
1. Включите adaptive sampling в Jaeger Collector:
# jaeger-all-in-one docker-compose.yml
jaeger:
image: jaegertracing/all-in-one:1.53
environment:
- SAMPLING_STRATEGIES_FILE=/etc/jaeger/sampling.json
volumes:
- ./sampling.json:/etc/jaeger/sampling.json2. Создайте sampling.json:
{
"service_strategies": [
{
"service": "api-gateway",
"type": "probabilistic",
"param": 0.1,
"operation_strategies": [
{
"operation": "GET /health",
"type": "probabilistic",
"param": 0.001
},
{
"operation": "POST /checkout",
"type": "probabilistic",
"param": 0.5
}
]
}
],
"default_strategy": {
"type": "probabilistic",
"param": 0.05
}
}3. Настройте клиент для использования remote sampling (OpenTelemetry SDK):
// Node.js — пока нет встроенной поддержки Jaeger remote sampling в OTel
// Используйте Jaeger SDK или ждите OTel Remote Sampler
// Временное решение: периодически обновляйте sampler
const axios = require("axios");
async function updateSampler() {
const response = await axios.get(
"http://jaeger-collector:5778/sampling?service=api-gateway"
);
const strategy = response.data;
// Parse strategy и обновите sampler
// (требуется custom реализация)
}
setInterval(updateSampler, 60000); // Обновляем каждую минутуSampling Decision Propagation
Проблема: Если API Gateway решил sampled=false, но User Service независимо решил sampled=true — получаем incomplete trace.
Решение: Sampling decision передаётся в traceparent header.
traceparent format:
traceparent: 00-<trace-id>-<parent-id>-<flags>
^^
|
sampled bit (01 = sampled)Пример:
API Gateway:
trace_id: a1b2c3d4
sampled: false
traceparent: 00-a1b2c3d4-0001-00 ← flags=00 (not sampled)
↓ HTTP request
User Service:
Извлекает traceparent
Видит flags=00
ParentBasedSampler → sampled=false
Spans создаются, но не отправляютсяProduction Best Practices
1. Начните с консервативного sampling (1-5%)
const sdk = new NodeSDK({
sampler: new TraceIdRatioBasedSampler(0.01), // 1%
});Постепенно увеличивайте на основе costs и observability needs.
2. Используйте разные rates для разных сервисов
High-traffic сервисы:
// api-gateway: 0.1% (100k req/sec)
sampler: new TraceIdRatioBasedSampler(0.001);Low-traffic сервисы:
// background-worker: 100% (10 req/sec)
sampler: new AlwaysOnSampler();3. Всегда используйте ParentBased wrapper
const sdk = new NodeSDK({
sampler: new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.01),
}),
});Гарантия: Если trace начат — он будет полным.
4. Мониторьте sampling effectiveness
Metrics:
traces_sampled_count— количество sampled tracestraces_dropped_count— количество dropped tracessampling_rate— фактический % sampling
Пример (Prometheus):
const sampledCounter = new promClient.Counter({
name: "traces_sampled_total",
help: "Total sampled traces",
});
const droppedCounter = new promClient.Counter({
name: "traces_dropped_total",
help: "Total dropped traces",
});5. Debug mode override
Сценарий: Нужно force-sample конкретный запрос для debugging.
Решение: Custom header X-Debug-Trace: true.
app.use((req, res, next) => {
if (req.headers["x-debug-trace"] === "true") {
// Force sample этот trace
const span = trace.getActiveSpan();
span.setAttributes({ "debug.forced": true });
// Переопределите sampling decision
// (требуется custom sampler или OTel Collector)
}
next();
});Cost vs Observability Trade-offs
Сценарий: 1 миллион req/sec
100% sampling:
- Traces: 1M/sec × 86400s = 86.4B traces/day
- Storage: ~8.6 PB/day
- Cost: $200k+/month
- Observability: ✅ Идеально
1% sampling:
- Traces: 864M traces/day
- Storage: ~86 TB/day
- Cost: $2k/month
- Observability: ⚠️ Можем пропустить редкие ошибки
0.01% sampling + 100% errors:
- Normal traces: 8.6M/day
- Error traces: зависит от error rate (например, 0.1% = 86k/day)
- Storage: ~1 TB/day
- Cost: $30/month
- Observability: ✅ Ошибки видны, latency patterns видны
Рекомендация: Начните с 0.1-1% + tail-based sampling для ошибок (урок 09).
Практическое задание
Задача: Настройте sampling с разными rates для разных endpoints.
Требования:
/health— 0.1% sampling/api/checkout— 10% sampling- Все остальные — 1% sampling
- Используйте ParentBased wrapper
Подсказка: Создайте custom sampler, который проверяет http.route attribute.
Ожидаемый результат:
1000 requests to /health → ~1 trace в Jaeger
1000 requests to /api/checkout → ~100 traces
1000 requests to /api/users → ~10 tracesСледующий урок
В следующем уроке мы научимся корреляции traces с logs и metrics — как связать trace_id с логами, чтобы от ошибки в Jaeger перейти к стеку вызовов в логах.
Теперь вы можете оптимизировать costs, не теряя observability!