Tail-based решение принимается после завершения трейса: «записать все ошибочные и медленные, из остальных — 1%». Это компромисс между полнотой и стоимостью, но у него есть цена — память и сложность.
Как это работает
Процессор tail_sampling в OTel Collector: собирает спаны трейса, ждёт
(decision_wait), затем применяет политики в порядке приоритета и выносит вердикт
для трейса целиком:
processors:
tail_sampling:
decision_wait: 10s # ждать завершения трейса
num_traces: 50000 # ёмкость кэша решателя
expected_new_traces_per_sec: 2000
policies:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow
type: latency
latency: { threshold_ms: 1000 }
- name: rare-service
type: string_attribute
string_attribute:
key: service.name
values: [legacy-billing]
- name: base
type: probabilistic
probabilistic: { sampling_percentage: 2 }
Политики — ИЛИ: трейс сохраняется, если сработала любая. Схема применения: ошибки и медленные — 100%, всё прочее — 2%.
Доступные политики
| Политика | Логика |
|---|---|
status_code | спаны с ERROR → записать трейс |
latency | трейсы дольше порога → записать |
probabilistic | базовый процент для остального |
rate_limiting | жёсткий потолок трейсов/сек |
string_attribute | по значению атрибута: сервис, tenant, флаг |
numeric_attribute | по числовому атрибуту (например, размер ответа) |
and / composite | комбинации и вложенные политики |
Цена и ограничения
- Память: до решения все спаны живут в RAM Collector'а. Расчёт:
num_traces × средний размер трейса; 50 000 × 20 КБ = 1 ГБ — уже ощутимо; - decision_wait: слишком мал — разрежем «медленные» трейсы (решение примется до их завершения); слишком велик — память и задержка появления в UI;
- Один решатель на трейс: при нескольких репликах Collector'а спаны одного трейса могут попасть на разные реплики и трейс «порежется». Решение — балансировка по trace ID (load-balancing exporter: агент → LB-коллектор → tail-sampling пул);
- Отказ Collector'а = потеря ожидаемых трейсов: tail-сэмплер держит их только в памяти.
💡 Практический рецепт: head-based 100% на агенте (не терять ничего по пути) → gateway-пул с tail_sampling (отсекать по правилам) → хранилище. Приложения ничего не знают о политике.
Head vs Tail — сводка
| Head-based | Tail-based | |
|---|---|---|
| Где решение | в SDK, до запроса | в Collector, после трейса |
| Гарантии | — | ошибки/медленные 100% |
| Цена | нулевая | память, LB по trace ID, сложность |
| Когда | по умолчанию | когда бюджет позволяет и нужна полнота ошибок |
Практика
- Поставьте tail_sampling на свой Collector: errors 100%, latency > 500мс 100%, base 5%. Нагрузите сервис и убедитесь: ошибочные и медленные трейсы есть все, быстрых — каждый двадцатый.
- Подкрутите decision_wait при «медленном» endpoint'е (3с) и посмотрите на «обрубленные» трейсы; верните корректное значение.
- Разверните два реплики Collector за LB-экспортёром по trace ID и убедитесь в целостности трейсов.
- Замерьте память Collector'а при росте num_traces — составьте свою формулу бюджета.
Мини-квиз
1. Почему спаны одного трейса должны попадать на одну реплику tail-сэмплера? — Решение принимается по всему трейсу; разрезанный между репликами трейс решается неверно.
2. Что случится при слишком коротком decision_wait? — Часть спанов опоздает: трейсы обрублены, медленные не распознаны.
3. Почему при tail-схеме на SDK ставят always_on? — Чтобы в Collector поступали все трейсы — решать будет политика, а не случайность в SDK.