Перейти к содержимому
К программе курса
Распределенная трассировка: от основ до production
14 / 1878%

Tail-based sampling для production

60 минут

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)    │                    │
│                 └─────────────┘                    │
└─────────────────────────────────────────────────────┘

Как работает:

  1. Receiver принимает spans от приложений
  2. Spans буферизуются в памяти (по trace_id)
  3. Ждём decision_wait время (например, 10s)
  4. Когда trace завершён или time expired → применяем policies
  5. Если хотя бы одна 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: 1

Composite 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 count

Alerts:

- 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 traces

Troubleshooting Tail Sampling

Проблема 1: Incomplete traces (не все spans дошли)

Симптомы: Decision принято на основе неполного trace.

Причина: decision_wait слишком короткий.

Решение: Увеличьте decision_wait:

tail_sampling:
  decision_wait: 30s # Было 10s

Trade-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 > 80000

3. Используйте 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 приложения.

Требования:

  1. 100% ошибок (status_code >= 400)
  2. 100% медленных запросов (> 2s)
  3. 100% критичных endpoints: /checkout, /payment
  4. 0% health checks (/health, /metrics)
  5. 1% остальных запросов
  6. 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!

Tail-based sampling для production — Распределенная трассировка: от основ до production — Potapov.me