Tail-based sampling для production
Tail-based sampling для production
Проблема head-based sampling
Вспомним урок 06:
Head-based sampling принимает решение в начале trace:
Request starts → sampled = true/false (10%)
↓
Если false — весь trace теряетсяПроблема: Мы не знаем будущее.
Сценарий:
100,000 requests/sec
├─ 99,000 успешных (latency < 100ms)
└─ 1,000 с ошибками (500 errors)
Head-based sampling (10%):
├─ 9,900 успешных traces сохранено
└─ 100 error traces сохранено ← потеряли 900 ошибок!Результат: Потеряли 90% критичных ошибок.
Решение: Tail-based Sampling
Принцип: Принимаем решение после завершения trace.
Request → Spans буферизуются в Collector
↓
Trace завершён
↓
Анализируем: есть ошибки? медленный? критичный endpoint?
↓
Решение: sample = true/false
↓
Отправляем в backend (или отбрасываем)Преимущества:
- ✅ 100% ошибок сохраняются
- ✅ 100% медленных запросов сохраняются
- ✅ Критичные endpoints — higher sampling rate
- ✅ Health checks — 0% sampling
Недостатки:
- ❌ Нужен OpenTelemetry Collector
- ❌ Буферизация spans → memory overhead
- ❌ Decision wait time (задержка перед принятием решения)
Архитектура Tail Sampling
┌─────────────────────────────────────────────────────┐
│ OpenTelemetry Collector │
│ │
│ ┌─────────┐ ┌──────────────┐ ┌──────────┐ │
│ │Receiver │──>│tail_sampling │──>│ Exporter │ │
│ │ OTLP │ │ processor │ │ Tempo │ │
│ └─────────┘ └──────────────┘ └──────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ In-memory │ │
│ │ buffer │ │
│ │ (traces) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────┘Как работает:
- Receiver принимает spans от приложений
- Spans буферизуются в памяти (по trace_id)
- Ждём
decision_waitвремя (например, 10s) - Когда trace завершён или time expired → применяем policies
- Если хотя бы одна policy = true → отправляем trace в backend
Tail Sampling Policies
Policy 1: Always Sample (критичные endpoints)
Зачем: Всегда сохранять важные endpoints (checkout, payment).
processors:
tail_sampling:
decision_wait: 10s
policies:
- name: critical-endpoints
type: string_attribute
string_attribute:
key: http.route
values:
- /api/checkout
- /api/payment
- /api/ordersРезультат: Все traces с этими endpoints сохраняются (100%).
Policy 2: Status Code (все ошибки)
Зачем: Сохранять все traces с ошибками.
policies:
- name: errors-4xx
type: status_code
status_code:
status_codes: [ERROR, UNSET] # OpenTelemetry status
- name: http-errors
type: numeric_attribute
numeric_attribute:
key: http.status_code
min_value: 400
max_value: 599Результат: 100% traces с http.status_code >= 400 сохраняются.
Policy 3: Latency (медленные запросы)
Зачем: Сохранять медленные traces (> P99 latency).
policies:
- name: slow-traces
type: latency
latency:
threshold_ms: 1000 # > 1 секундыРезультат: Все traces длительностью > 1s сохраняются.
Policy 4: Probabilistic (остальные)
Зачем: Для нормальных traces — низкий sampling rate.
policies:
- name: sample-normal-traffic
type: probabilistic
probabilistic:
sampling_percentage: 1 # 1% остальныхPolicy 5: Rate Limiting
Зачем: Ограничить количество traces/sec (защита от спайков).
policies:
- name: rate-limit
type: rate_limiting
rate_limiting:
spans_per_second: 1000 # Max 1000 traces/secПолная Production-конфигурация
Сценарий:
- 100% ошибок (4xx, 5xx)
- 100% медленных (> 1s)
- 100% критичных endpoints (/checkout, /payment)
- 1% остальных
- Max 5000 traces/sec
processors:
tail_sampling:
# Время ожидания завершения trace
decision_wait: 10s
# Количество traces в памяти
num_traces: 100000
# Ожидаемое количество новых traces/sec
expected_new_traces_per_sec: 10000
policies:
# 1. Всегда сохранять критичные endpoints
- name: critical-endpoints
type: string_attribute
string_attribute:
key: http.route
values:
- /api/checkout
- /api/payment
- /api/orders
# 2. Все ошибки (OpenTelemetry status)
- name: errors
type: status_code
status_code:
status_codes: [ERROR]
# 3. HTTP ошибки (4xx, 5xx)
- name: http-errors
type: numeric_attribute
numeric_attribute:
key: http.status_code
min_value: 400
max_value: 599
# 4. Медленные traces (> 1s)
- name: slow-requests
type: latency
latency:
threshold_ms: 1000
# 5. Очень медленные traces (> 5s)
- name: very-slow-requests
type: latency
latency:
threshold_ms: 5000
# 6. Rate limiting (защита от спайков)
- name: rate-limit
type: rate_limiting
rate_limiting:
spans_per_second: 5000
# 7. Остальные traces — 1%
- name: probabilistic-sample
type: probabilistic
probabilistic:
sampling_percentage: 1Composite Policies (И/ИЛИ логика)
Зачем: Сложные условия ("сохранить если endpoint = /checkout И latency > 500ms").
policies:
- name: slow-checkout
type: and # ← И логика
and:
and_sub_policy:
- name: checkout-endpoint
type: string_attribute
string_attribute:
key: http.route
values: [/api/checkout]
- name: slow
type: latency
latency:
threshold_ms: 500Результат: Сохраняем только медленные /api/checkout запросы.
OR логика:
policies:
- name: errors-or-slow
type: composite
composite:
max_total_spans_per_second: 1000
policy_order: [errors, slow]
composite_sub_policy:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow
type: latency
latency: { threshold_ms: 1000 }Multi-tier Sampling Strategy
Проблема: Tail-sampling на одном Collector не масштабируется (все spans одного trace должны попасть на один Collector).
Решение: Hybrid approach.
Tier 1: Head-sampling в приложении
// App: 100% sampling (отправляем все spans)
const sdk = new NodeSDK({
sampler: new AlwaysOnSampler(), // ← Всё отправляем
});Tier 2: Tail-sampling в Collector
┌──────┐ ┌──────┐ ┌──────┐
│ App1 │ │ App2 │ │ App3 │
└───┬──┘ └───┬──┘ └───┬──┘
│ (100%) │ (100%) │ (100%)
└─────────┴─────────┘
│
▼
┌────────────────────┐
│ OTel Collector │
│ (tail_sampling) │
│ • Errors: 100% │
│ • Slow: 100% │
│ • Normal: 1% │
└─────────┬──────────┘
│ (10-20%)
▼
┌────────┐
│ Tempo │
└────────┘Результат:
- Приложение: 0% overhead от sampling logic
- Collector: Централизованное умное сэмплирование
- Backend: Только важные traces
Load Balancing для Tail Sampling
Проблема: Spans одного trace должны попасть на один Collector (для принятия решения).
Решение: Load balancing по trace_id.
Архитектура
┌──────────────────────────────────────────┐
│ Applications │
└───────────────┬──────────────────────────┘
│
▼
┌───────────────────────┐
│ Load Balancer │
│ (hash by trace_id) │
└───────────┬───────────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│Collector│ │Collector│
│ #1 │ │ #2 │
│(tail) │ │(tail) │
└────┬────┘ └────┬────┘
│ │
└──────────┬──────────┘
▼
┌─────────┐
│ Tempo │
└─────────┘Kubernetes Service с session affinity:
apiVersion: v1
kind: Service
metadata:
name: otel-collector-lb
spec:
selector:
app: otel-collector
sessionAffinity: ClientIP # ← НЕ работает для trace_id
ports:
- port: 4317Проблема: Kubernetes Service не поддерживает hash по trace_id.
Решение: Используйте специальный load balancer (например, Envoy).
Мониторинг Tail Sampling
Ключевые метрики:
# Traces в буфере (memory usage)
otelcol_processor_tail_sampling_trace_id_in_memory
# Traces dropped (не поместились в буфер)
rate(otelcol_processor_tail_sampling_traces_dropped[5m])
# Decision latency
histogram_quantile(0.95,
otelcol_processor_tail_sampling_policy_decision_latency_bucket
)
# Sampling rate по policies
otelcol_processor_tail_sampling_policy_decision{policy="errors"} # = true/false countAlerts:
- alert: TailSamplingBufferFull
expr: otelcol_processor_tail_sampling_trace_id_in_memory > 90000
annotations:
summary: Tail sampling buffer almost full
- alert: TailSamplingDroppingTraces
expr: rate(otelcol_processor_tail_sampling_traces_dropped[5m]) > 100
annotations:
summary: Tail sampling dropping tracesTroubleshooting Tail Sampling
Проблема 1: Incomplete traces (не все spans дошли)
Симптомы: Decision принято на основе неполного trace.
Причина: decision_wait слишком короткий.
Решение: Увеличьте decision_wait:
tail_sampling:
decision_wait: 30s # Было 10sTrade-off: Больше memory usage (дольше держим spans в памяти).
Проблема 2: High memory usage
Причина: Слишком много traces в буфере.
Решение 1: Уменьшите num_traces:
tail_sampling:
num_traces: 50000 # Было 100000Решение 2: Уменьшите decision_wait:
tail_sampling:
decision_wait: 5s # Было 30sПроблема 3: Traces dropped (buffer overflow)
Причина: Incoming rate > processing rate.
Решение: Добавьте rate limiting policy:
policies:
- name: rate-limit
type: rate_limiting
rate_limiting:
spans_per_second: 3000 # Ограничить входящий потокCost Optimization: Real-world Case
До tail-sampling:
Traffic: 1M req/sec
Head-based sampling: 1% → 10k traces/sec
Storage: 864M traces/day × 100KB = 86TB/day
Cost: $2,580/monthПосле tail-sampling:
Traffic: 1M req/sec
Tail-sampling policies:
├─ Errors (0.1% of traffic): 1k traces/sec × 100% = 1k/sec
├─ Slow (0.5% of traffic): 5k traces/sec × 100% = 5k/sec
├─ Critical endpoints (1% of traffic): 10k traces/sec × 100% = 10k/sec
└─ Normal (98.4% of traffic): 984k traces/sec × 0.1% = 984/sec
Total: 17k traces/sec → 1.47B traces/day
Storage: 147TB/day
Cost: $4,410/monthНо! Теперь у нас:
- ✅ 100% ошибок
- ✅ 100% медленных запросов
- ✅ 100% критичных endpoints
Better observability за те же деньги!
Best Practices
1. Начните с консервативных policies
policies:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: probabilistic
type: probabilistic
probabilistic: { sampling_percentage: 5 } # Начните с 5%Постепенно уменьшайте probabilistic rate на основе costs.
2. Мониторьте buffer usage
# Alert если буфер > 80% заполнен
otelcol_processor_tail_sampling_trace_id_in_memory > 800003. Используйте composite policies для сложных правил
# "Медленные checkout ИЛИ любые ошибки"
- name: important-traces
type: composite
composite:
policy_order: [slow-checkout, errors]4. decision_wait должен быть > max trace duration
Если P99 trace duration = 5s:
decision_wait: 10s # 2× запас5. Используйте rate_limiting для защиты
- name: rate-limit
type: rate_limiting
rate_limiting:
spans_per_second: 5000 # Защита от спайковПрактическое задание
Задача: Настройте tail-sampling для e-commerce приложения.
Требования:
- 100% ошибок (status_code >= 400)
- 100% медленных запросов (> 2s)
- 100% критичных endpoints:
/checkout,/payment - 0% health checks (
/health,/metrics) - 1% остальных запросов
- Max 1000 traces/sec (rate limiting)
Ожидаемый результат:
- При спайке ошибок — все ошибки сохраняются
- Health checks не попадают в storage
- Normal traffic — только 1%
Следующий урок
В следующем уроке мы изучим Performance Tuning — как минимизировать overhead от трассировки на приложение (batching, async export, sampling optimization).
Теперь вы можете сохранять 100% критичных traces, платя только за 1-5% storage!