Distributed tracing с OpenTelemetry: W3C trace context propagation, head-based vs tail-based sampling, hot trace investigation, anti-patterns с async boundaries.
В монолите debug — это stacktrace. В микросервисах один запрос проходит через 5-50 сервисов, и у каждого свой stacktrace; склеить их руками невозможно. Distributed tracing склеивает их автоматически через trace_id, который протянут сквозь каждый hop. Без tracing вопрос «куда ушли 2.5 секунды?» решается часами корреляции дашбордов и догадок. С tracing — клик и waterfall, видно точно: «80% времени в Postgres call внутри svc-payment».
Trace — это граф спанов, где каждая span = «что-то делал N миллисекунд».
trace_idсвязывает их через всю цепочку. Главная магия — context propagation: каждый сервис должен передатьtrace_idчерез HTTP-header / message metadata, иначе trace обрывается. 90% поломок tracing — пропущенный propagation в async-коде.
Базовые сущности:
user.tier=premium, tenant.id=...).SERVER, CLIENT, PRODUCER, CONSUMER, INTERNAL.Стандартные заголовки (W3C Trace Context):
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tracestate: vendorname=value,othervendor=value
baggage: user.tier=premium,region=eu
Четыре группы: Edge (LB, API Gateway), Services (Auth, Order, Inventory, Payment — все инструментированы OTel), Data (Postgres, Redis, Kafka), Tracing pipeline (OTel Collector → Tempo на S3 → Grafana).
Edges делятся на два класса:
user → lb → gw → services → data, по нему течёт обычный трафик с traceparent в headers.services → otel → tempo, отдельный side-channel: spans уезжают в Collector асинхронно по OTLP (gRPC), не блокируя hot path.Это ключевая мысль: tracing-трафик идёт параллельно бизнес-трафику и не на критическом пути. Если Collector ляжет — приложение продолжит работать, потеряются только spans.
Edge gateway генерирует trace_id на root request, кладёт traceparent header. Каждый downstream service достаёт header, создаёт child span с тем же trace_id и новым span_id, ссылается на parent. Все spans уезжают в OTel Collector асинхронно — Grafana собирает их по trace_id в один waterfall. Семь спанов от одного запроса = одна полная картинка пути.
Head sampler на edge решил «не сэмплируем» (99% случаев — экономим storage). Но при ошибке tail-collector в OTel переопределяет решение: видит error=true в одном из spans → сохраняет весь trace, даже если head его дропнул. То же для slow traces (duration > p99). On-call открывает Grafana, видит полный waterfall, root cause за минуту вместо часа.
P99-алерт сработал: запрос занял 3.2 секунды. Инженер идёт в Grafana → Tempo, фильтрует duration > 3s, находит конкретный trace_id. Waterfall показывает: auth=12ms, redis=2ms, pg=8ms, inventory=45ms, payment=2980ms. Дрилл в payment span → внутри http.client → stripe.com = 2.85s. Root cause не в нашем коде — внешний API регрессировал. Тикет открыт Stripe-у за 2 минуты.
Антипаттерн: сервис публикует в Kafka, забывает инжектить traceparent в headers сообщения. Consumer на другой стороне начинает новый root span с новым trace_id. Каузальная связь «produce → consume» теряется — в Grafana два разных trace, не связанных. Самая частая дыра в tracing: всегда propagator.inject(ctx, headers) перед publish.
Главная архитектурная развилка — head- vs tail-based sampling (ADR-001 в группе Services):
| Подход | Плюсы | Минусы |
|---|---|---|
| Head-based (1%) | Дёшево, без памяти, решение на root | Пропускаешь редкие ошибки и slow traces — самое ценное |
| Tail-based | Видишь все errors, все p99 slow | Stateful collector с памятью на ~30s буфер; HA нужна |
| Гибрид (выбран) | Head 1% базовый + tail override на errors/slow | Сложнее эксплуатация, но <5% volume / >95% полезных traces |
На 50K req/s полный 100%-sampling = ~50 GB/час storage (1KB/span × 20 spans/trace). Tail-collector буферизирует ~30 секунд spans в RAM — это десятки гигабайт памяти на узел при пиковых QPS, но при этом ловит 95%+ инцидентов.
Второй важный trade-off — auto vs manual instrumentation:
opentelemetry-instrument для Python, --require для Node) покрывают 80% — все HTTP/gRPC/JDBC/Kafka hops бесплатно.for-итерацию = 100K spans в одном trace, UI ляжет.Третий — backend choice:
| Backend | Сильные стороны | Слабости |
|---|---|---|
| Jaeger | OSS де-факто (Uber, eBay), CNCF graduated | UI устарел, query slow при scale |
| Tempo | S3-backed, очень дёшево, LGTM-стек | Только trace, query через trace_id |
| Honeycomb | Лучший UX, BubbleUp, wide-events | $$$, vendor lock-in |
| Datadog APM | Корреляция с metrics/logs, лучший APM | $$$, vendor lock-in |
| AWS X-Ray | Native в AWS, дёшево | Слабый UI, AWS lock-in |
user_id на каждом span — это OK в отличие от metrics!) позволяют BubbleUp анализ: «среди slow trace что общего? — все из tier=enterprise + region=ap-south». Один запрос за минуту.user.tier, tenant.id в baggage на edge — потом фильтруй traces по этим полям.ctx context.Context первым аргументом; в Node: async hooks / OTel auto-instrumentation; в Kafka/RabbitMQ: propagator.inject(headers) перед publish.db.statement, auth token в http.url. Та же security-проблема, что и с logs. Используй OTel processor для маскинга на Collector.traceparent стандарт с 2020.trace_id в логах = linkability. Решение: separate trace stream без PII.