awesome-everything EN
↑ Обратно к восхождению

Наблюдаемость

Стратегии сэмплирования: head, tail и parent-based

Суть Head-сэмплирование дёшево и предсказуемо; tail-сэмплирование дорого, но даёт контроль над отбором. ParentBased(TraceIdRatioBased(0.2)) на SDK плюс tail-сэмплирование на шлюзе — 100% ошибок и медленных хвостов, 1% базового трафика — это учебная production-комбинация.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 13 min

При 10к запросов в секунду запись 100% трассировок заполняет бэкенд за часы и генерирует пятизначный ежемесячный счёт. Но если сбросить 99% трассировок, именно ошибки и медленные запросы, нужные для отладки, будут потеряны. Сэмплирование — это компромисс между стоимостью и точностью.

Head-сэмплирование: решение быстро и дёшево

Sampler SDK запускается на самом первом спане трассировки (корневом спане) и решает, записывать ли трассировку. Решение распространяется на нижестоящие сервисы через флаг sampled в заголовке W3C traceparent.

AlwaysOn — записывает каждую трассировку. Полная точность, максимальная стоимость.

AlwaysOff — ничего не записывает. Полезен для канарейки или сервиса под нагрузочным тестом.

TraceIdRatioBased(p) — записывает случайную долю p на основе хэша trace_id. Детерминировано между сервисами: если два сервиса оба используют TraceIdRatioBased(0.1), они независимо принимают одинаковое решение для одной трассировки, потому что хэш берётся от trace_id, который разделяется по всему запросу. При p=0.1 записывается 10% трассировок.

ParentBased — следует решению о сэмплировании родительского спана (через traceparent.sampled). Когда вышестоящий сервис решает сэмплировать, все нижестоящие следуют за ним. Когда вышестоящий не сэмплирует, нижестоящие обычно тоже нет, если локальный сэмплер не переопределяет. Предотвращает несогласованные частичные трассировки.

Production-комбинация: ParentBased(root=TraceIdRatioBased(0.2)). Корневой спан сэмплирует 20% трассировок. Все нижестоящие спаны наследуют решение. Результат: либо вся трассировка записана, либо ничего — никаких частичных трассировок с осиротевшими дочерними спанами.

Tail-сэмплирование: решение поздно, дорого

Tail-сэмплирование выполняется в Collector после того, как все спаны трассировки прибыли и трассировка завершена (или близка к этому). Оно может анализировать полный контекст трассировки: финальный статус-код, общую задержку, наличие спанов с ошибками где-либо в дереве, бизнес-атрибуты на любом спане.

Типичные политики tail-сэмплирования:

  • status_code=ERROR — сохранять 100% трассировок с любым спаном ERROR
  • latency > 1000ms — сохранять 100% медленных трассировок
  • probabilistic 1% — сохранять 1% всего остального как базовый трафик

Комбинация покрывает весь интересный трафик (ошибки, медлительность) и обеспечивает ограниченный по стоимости базовый уровень для нормального трафика.

ПодходМомент решенияВидит ошибки?СтоимостьС состоянием?
Head (TraceIdRatioBased)При старте корневого спанаНет — решает до возникновения ошибкиНизкая (SDK, <1 мкс)Нет
Tail (процессор Collector)После завершения трассировкиДа — видит полный контекстВысокая (буферизует спаны в RAM)Да — должен видеть все спаны
ParentBased headПри старте каждого спанаНет — то же ограничение, что у headНизкаяНет

Ограничение sticky-маршрутизации

Tail-сэмплирование требует, чтобы все спаны трассировки прибывали на один и тот же экземпляр Collector-шлюза. Если спаны одной трассировки попадают на два разных экземпляра, ни один из них не может оценить полный контекст трассировки, и решение о сэмплировании будет неверным.

Экспортёр loadbalancing OTel Collector на агентном уровне решает эту проблему: он хэширует по trace_id и маршрутизирует детерминировано на один и тот же pod шлюза для всех спанов трассировки.

Компромисс: шлюз становится с состоянием. Перезапуск pod или событие масштабирования перетасовывает кольцо хэшей и теряет трассировки в полёте. Production-меры защиты:

  • Предварительный прогрев новых podов перед включением в кольцо балансировки нагрузки
  • Консервативные политики масштабирования (избегать частых масштабирований во время пиков трафика)
  • Размер буфера num_traces шлюза = пиковая_скорость × decision_wait × защитный_коэффициент (~2x)

Числа

  • Буфер tail-сэмплирования: ~1-2 ГБ на 50к активных трассировок (100 спанов по ~1 КБ/спан)
  • decision_wait: 30-60 секунд (должен превышать p99 длительности трассировки)
  • num_traces: размер = пиковая_скорость × decision_wait × 2 — при 2000 трассировок/с и окне 30с: ~120к
  • Head-сэмплирование в production: 10-20%
  • Tail-сэмплирование сохраняет: 100% ошибок + 100% медленных (порог 1с) + 1-5% базового трафика
  • Итого удержано: ~3-5% от общего объёма, 100% интересного трафика
Почему это работает

Почему не делать tail-сэмплирование 100% без head-сэмплирования? Потому что tail-сэмплирование буферизует каждый спан в RAM до закрытия окна decision_wait. При 10к RPS × 50 спанов на запрос × 30с — это 15 миллионов спанов в памяти — несколько ГБ. Head-сэмплирование пропорционально снижает буфер в полёте. При 20% head и tail-выборке все ошибки и медленные хвосты сохраняются, потому что tail-сэмплер всегда переопределяет в сторону сохранения. Комбинация даёт контроль стоимости (head) и точность (tail) с ограниченным и предсказуемым потреблением памяти.

Викторина

Почему ParentBased(TraceIdRatioBased(0.2)) — стандартная комбинация head-сэмплера для распределённых систем?

Викторина

Tail-сэмплирование на шлюзе требует, чтобы все спаны трассировки попадали на один экземпляр Collector. Какой механизм обеспечивает это в паттерне agent-to-gateway?

Расставь шаги по порядку

Упорядочите события tail-сэмплирования от создания спана до решения сохранить/сбросить:

  1. 1 Приложение отправляет спаны агентскому Collector
  2. 2 loadbalancing exporter агента хэширует trace_id, маршрутизирует на детерминированную реплику шлюза
  3. 3 Шлюз буферизует спаны в процессоре tail_sampling (буфер num_traces, в RAM)
  4. 4 После decision_wait (30-60с) оценивается полный контекст трассировки
  5. 5 Проверка политики: статус ERROR? Задержка свыше 1с? Вероятностный базовый трафик?
  6. 6 Сохранить или сбросить — сохранённые трассировки пересылаются экспортёру; сброшенные удаляются
Вспомните перед уходом
  1. 01
    В чём ключевое различие между head-сэмплированием и tail-сэмплированием, и когда каждое из них даёт сбой?
  2. 02
    Почему tail-сэмплирование должно использовать sticky-маршрутизацию по trace_id, и что ломается, если спаны одной трассировки попадают на две реплики шлюза?
  3. 03
    Как определить размер буфера tail-сэмплирования (num_traces) и что происходит при его недостаточном размере?
Итог

Сэмплирование — это компромисс между стоимостью и точностью. Head-сэмплирование (TraceIdRatioBased, AlwaysOn, ParentBased) решает при старте корневого спана — дёшево (<1 мкс), без состояния, но слепо к ошибкам и задержке. ParentBased гарантирует, что все спаны трассировки следуют решению корня, предотвращая частичные трассировки. Tail-сэмплирование в Collector решает после прибытия полной трассировки — может сохранить 100% ERROR-трассировок и все трассировки с задержкой свыше 1с плюс 1% базового трафика. Требует буфера в памяти с состоянием (размер = пиковая_скорость × 30с × 2, обычно 1-2 ГБ на 50к активных трассировок) и sticky trace_id-маршрутизации через loadbalancing exporter. Production-паттерн: ParentBased(TraceIdRatioBased(0.2)) на SDK снижает объём буфера на 80%, пока tail-сэмплер сохраняет весь интересный трафик. Итог: ~3-5% от общего объёма удержано, 100% интересных трассировок захвачено.

Связанные уроки
встречается в167
Продолжить восхождение ↑Vendor-нейтральность, eBPF-инструментирование, Operator и OTel в браузере и serverless
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.