Наблюдаемость
Согласованность sampling и tier tail-sampling Collector
Tail-sampling Collector OOM’ит каждые несколько часов в пиковый трафик. Команда поднимает memory limit — и снова OOM. Никто не спросил: почему растёт память, что её контролирует и что её ограничивает?
Согласованность sampling между сервисами
«Consistent sampling» означает: все span’ы одного трейса либо все сэмплированы, либо все дропнуты. Никаких частичных трейсов — либо backend имеет полный трейс, либо ничего.
Механизм: вероятностные head-sampler’ы используют детерминированный хэш trace-id по модулю 100 для решения, какие трейсы хранить. Потому что хэш детерминированный и trace-id одинаков во всех сервисах (пропагируется traceparent), каждый сервис независимо приходит к тому же решению keep/drop. Координации не нужно.
W3C Trace Context Level 2 (2024) формализует это через флаг random-trace-id (бит 1, значение 02): когда установлен, trace-id гарантированно равномерно случаен, делая hash-based consistent sampling надёжным. Consistent hash-based sampler’ы читают этот флаг как предусловие; при его отсутствии могут откатиться к более простому подходу.
Почему частичные трейсы хуже отсутствия трейсов:
- Трейс без span’а сервиса B выглядит как прямой вызов из A в C. Атрибуция latency неверна.
- Расчёты перцентилей по частичным трейсам дают систематическое смещение — короткие span’ы перепредставлены.
- On-call-инженеры диагностируют по тому, что видят; частичные трейсы ведут к уверенным неверным диагнозам.
Модель памяти tail-sampler’а
Tail-sampling Collector держит все span’ы каждого активного трейса в памяти до закрытия decision-окна. Использование памяти:
RAM = active_traces × avg_spans_per_trace × bytes_per_span
= (in_flight_request_rate × decision_window_seconds) × avg_spans × span_sizeПри 10k req/s, 30с окно, 10 span’ов/трейс, 1 KB/span:
10 000 × 30 × 10 × 1 024 ≈ 3 GB на Collector instance
Каждый фактор — независимый рычаг OOM:
| Фактор | Что раздувает | Смягчение |
|---|---|---|
| active_traces | Traffic-всплеск, слишком длинное окно | Масштабировать реплики, сократить окно |
| spans_per_trace | Long-running batch-job’ы, рекурсивная инструментация | Ломать длинные трейсы через span-link’и |
| bytes_per_span | Большие значения атрибутов, избыточные метаданные | Лимиты размера значений атрибутов |
| decision_window | Консервативно слишком длинное | Настраивать per-service SLA; 30с — типично |
Требование load-balancing exporter
Несколько реплик Collector’а — стандарт в production (5–20 реплик типично). Без роутинга span’ы одного трейса случайно разбрасываются по репликам — ни одна не видит полный трейс и ни одна не может принять корректное policy-решение.
OTel Collector поставляется с exporter’ом loadbalancing, хэширующим по trace-id и направляющим на фиксированную реплику. Механизмы discovery: DNS round-robin, статический список или Kubernetes endpoint API.
Операционные подвохи:
- Scale-события: когда Collector-реплика добавляется или удаляется, hash ring перебалансируется. Span’ы в полёте для трейсов, хэшированных на перебалансированный shard, могут прийти на новую реплику до более ранних span’ов — tail sampler видит частичный трейс и может принять решение преждевременно. Смягчение: короткие decision-окна (30с), graceful drain при scale-down.
- Потеря реплики: если Collector-реплика умирает в середине decision-окна, её in-flight трейсы теряются. Tail sampling по умолчанию не персистирует на диск. Это приемлемо: потеря трейсов при сбое Collector’а — известный операционный trade-off.
Защита от OOM tail-sampler’а
Tail-sampler без cap’а будет буферизовать неограниченно до OOM процесса. Процессор tail_sampling предоставляет num_traces как жёсткий cap на число активных трейсов. При достижении cap’а sampler начинает вытеснять самые старые in-progress трейсы (незавершённые трейсы отбрасываются).
Инцидент Slack 2023: tail-sampling Collector’ы уронили tracing-pipeline во время крупного инцидента, потому что num_traces был без cap’а. Postmortem добавил cap, отдельный high-priority always-keep tier для критических трейсов и alert на otelcol_processor_dropped_spans_rate.
Полный паттерн конфигурации:
processors:
tail_sampling:
decision_wait: 30s
num_traces: 100000 # жёсткий cap — вытеснять старейшие при превышении
policies:
- name: errors
type: status_code
status_code:
status_codes: [ERROR]
- name: slow-traces
type: latency
latency:
threshold_ms: 2000
- name: probabilistic
type: probabilistic
probabilistic:
sampling_percentage: 1| Alert-метрика | Что означает | Порог |
|---|---|---|
| otelcol_processor_tail_sampling_count_traces_on_memory | Активные in-flight трейсы | Alert при 80% num_traces cap |
| otelcol_processor_dropped_spans | Span’ы дропнуты из-за cap’а | Alert при любой ненулевой частоте |
| container_memory_working_set_bytes (Collector pod) | Реально используемая RAM | Alert при 85% memory limit |
Tail-sampling Collector OOM'ит каждые несколько часов. Проследи root cause.
- W3C Trace Context Level 1
- Recommendation 2020-02
- W3C Trace Context Level 2 (флаг random-trace-id)
- Recommendation 2024
- Типичное tail decision-окно
- 30с
- RAM Collector при 50k трейсов × 100 span × 1 KB
- ~5 GB
- Типичных реплик Collector в production
- 5–20
- Ключ роутинга load-balancing exporter
- trace-id (детерминированный хэш)
Сервис эмитирует 5% orphan span'ов для внутренних (не entry-point) сервисов. Команда добавляет tail sampling. Orphan rate остаётся. Что не так понято?
Реплика Collector'а удаляется при scale-down. Что происходит с трейсами, чьё decision-окно ещё не закрылось на этой реплике?
- 01Как детерминированное хэширование trace-id достигает consistent sampling без координации между сервисами?
- 02Объясни failure mode OOM tail-sampler'а и три независимых смягчения.
- 03Почему load-balancing exporter обязан использовать хэширование по trace-id, а не round-robin или least-connections?
Consistent sampling использует детерминированный hash(trace-id) mod 100, чтобы все сервисы независимо договорились о keep/drop — частичные трейсы никогда не возникают. Флаг random-trace-id W3C Level 2 делает это безопасным, утверждая равномерное распределение trace-id. RAM tail-sampling Collector’а — active_traces × span'ы/трейс × байт/span × decision_window; каждый фактор — независимый OOM-рычаг. Жёсткий cap num_traces предотвращает неограниченный рост; alert’ы на otelcol_processor_dropped_spans ловят срабатывание cap’а. Load-balancing exporter обязан использовать хэширование по trace-id — round-robin разбрасывает span’ы трейса по репликам, делая policy-решения неверными. Long-running трейсы (batch-job’ы, накапливающие тысячи span’ов) нужно ломать на под-трейсы через span-link’и, чтобы вмещаться в decision-окно.