Зачем distributed tracing
Монолит: stack trace показывает весь путь запроса. Распределённая система: запрос идёт через 5-15 микросервисов. Stack trace одного сервиса не помогает, где именно время потерялось?
Distributed tracing склеивает все участки в один граф:
POST /checkout (4.2s) [edge]
├─ cart-service.fetch (12ms)
├─ payment-service.charge (4.1s) ◄── вот где тормозит
│ ├─ stripe-api.call (4.0s timeout) ◄── а это первоисточник
│ └─ db.update (8ms)
└─ inventory-service.reserve (45ms)
Это и есть trace, дерево spans с одним общим trace_id.
Терминология
- Span, единица работы: HTTP-handler, DB-query, RPC-call. Имеет
start_time,end_time,name,attributes,status. - Trace, все spans с одним
trace_id, организованные в дерево черезparent_span_id. - Trace context,
(trace_id, span_id, flags)который передаётся через сетевые границы. - Sampling decision, keep или drop эту трассу. Принимается на edge (head) или в Collector (tail).
Span structure (OTLP):
{trace_id: "4bf92f3577b34da6a3ce929d0e0e4736", // 16 bytes hex
span_id: "00f067aa0ba902b7", // 8 bytes hex
parent_span_id: "0ef3...", // null если root
name: "HTTP POST /checkout",
start_time_unix_nano: 1683456789123456789,
end_time_unix_nano: 1683456793321456789,
attributes: {"http.method": "POST",
"http.status_code": 500,
"http.route": "/checkout",
"service.name": "edge"
},
events: [
{time: ..., name: "exception", attributes: {"exception.type": "TimeoutError"}}],
status: {code: ERROR, message: "stripe timeout"}}
W3C Trace Context, context propagation
Без context propagation каждый сервис генерил бы свой trace_id невозможно склеить.
W3C Trace Context (стандарт 2020), два HTTP-header'а:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
^^ ^trace_id (32 hex) ^span_id (16) ^^
version flags
tracestate: vendor1=value1,vendor2=value2
flags=01= sampled (downstream должен tracing'овать)flags=00= not sampled (можно skip)
Edge сервис генерит traceparent. Каждый downstream сервис:
- Парсит входящий
traceparent - Создаёт child span под
parent_span_id=span_idиз header'а - Генерит новый
span_idдля своего span'а - Передаёт обновлённый
traceparent(свой span_id) дальше
Auto-instrumentation [[opentelemetry|OpenTelemetry SDK]] делает это автоматически для HTTP/[[grpc-basics|gRPC]]/Kafka. Через [[http2-internals|HTTP/2]] header'ы передаются в HPACK-сжатии.
B3 (Zipkin) headers, legacy
До W3C был B3 Propagation от Zipkin:
X-B3-TraceId: 4bf92f3577b34da6a3ce929d0e0e4736
X-B3-SpanId: 00f067aa0ba902b7
X-B3-ParentSpanId: 05e3...
X-B3-Sampled: 1
В 2025 поддерживай оба для совместимости со старыми клиентами:
OTEL_PROPAGATORS=tracecontext,b3
Sampling, head vs tail
100% traces неподъёмны: 10K req/s × 5 spans × 5KB = 250 MB/s. Sampling обязателен.
Head-based sampling
Решение в самом начале трассы (на edge):
if random() < 0.01: # 1% sampling
flags = 0x01 # sampled, всё дерево соберём
else:
flags = 0x00 # not sampled
- Плюс: предсказуемо, дёшево, downstream не нужны лишние данные
- Минус: рандомно теряем error traces (99% потеряли)
Tail-based sampling
Все spans собираем в [[opentelemetry|OTel Collector]], держим в памяти 5-30s, ждём всех children. Потом решаем по полной картине:
processors:
tail_sampling:
decision_wait: 30s
num_traces: 100000
policies:
- name: errors
type: status_code
status_code: {status_codes: [ERROR]}- name: slow
type: latency
latency: {threshold_ms: 1000}- name: random
type: probabilistic
probabilistic: {sampling_percentage: 1}Сохраняем 100% errors, 100% slow, 1% rest. Точно нужное.
- Плюс: видим все ошибки и аномалии
- Минус: Collector держит full traces в RAM 30s, нужно ~3 GB на 10K rps. Sharding по trace_id обязателен в кластере.
Adaptive sampling
Динамически меняет rate в зависимости от volume. Реализован в Datadog, Honeycomb. В open-source, guardrail в Tempo (выкидывает если backend перегружен).
Backend storage
| Backend | Storage | Query | Когда |
|---|---|---|---|
| Jaeger | Cassandra/ES | UI + API | classic, in-memory dev |
| Tempo (Grafana) | object (S3) | TraceQL, Grafana UI | дёшево, scale |
| Zipkin | MySQL/Cassandra | UI + API | старый, простой |
| Honeycomb | proprietary | BubbleUp | hosted, ML-driven |
| Datadog APM | proprietary | UI | hosted, integrated |
Tempo, рекомендованный для self-hosted: S3-backed (как Loki), не нужно поддерживать Cassandra/ES, дешёвое retention (~$0.05/GB).
Correlation: traces + logs + metrics
Сила трассировки, в связке:
- Лог пишем
{"trace_id": "abc", "level": "error", ...}, клик из Jaeger/Tempo прыгает в [[loki-grafana-logging|Loki]] с фильтром{trace_id="abc"} - Метрика histogram с exemplar
(p99=2.3s, trace_id="abc")клик из Grafana прыгает в Tempo на конкретную медленную трассу - Span'у
linkссылается на другую trace (для batch jobs)
Всё держится на двух идентификаторах: trace_id и span_id. Они
должны быть во всех логах edge-сервиса.
TraceQL, query language Tempo
Tempo 2.0+ ввёл TraceQL, SQL-like для трасс:
{ resource.service.name = "checkout" && duration > 1s &&span.http.status_code = 500 }
Возвращает trace_id'ы. Можно сложнее:
{ service.name = "edge" } >> { service.name = "payment" &&status = error }
>> означает "ancestor of", все трассы где edge вызвал payment, и
payment упал. Полезно для root-cause анализа.
Когда что-то пошло не так
- Trace разорван, child span'ов нет, context не передан.
Проверь
traceparentприходит на downstream (curl -v). Часто причина: HTTP client без instrumentation, или middleware стрипает headers. - trace_id есть, но в Tempo не видно, head sampling 0.01 уронил. Включи tail sampling с keep-on-error policy.
- Span'ы из Kafka не связаны, message-based propagation отдельно.
Передавай
traceparentкак Kafka header в producer, парси в consumer. - Ridiculously deep traces (1000+ spans), instrumented каждая DB-query внутри loop'а. Sample inside loop или upper-bound.
- Clock skew между сервисами → spans с end_time < start_time Jaeger/Tempo визуализирует как кривая. Поправь NTP (chrony-and-ntp).
- Trace context прыгает через async boundary,
setTimeout,Promise.then, goroutine. Используй context-passing (Go:ctx, Node:AsyncLocalStorage, Python:contextvars). - Browser → backend trace не связан, frontend не отправляет
traceparent. Подключи@opentelemetry/instrumentation-fetch.
Anti-patterns
- Tracing на каждый mysql.query в hot path, overhead. Sample выше, в HTTP-handler.
- Span на каждую внутреннюю функцию, это работа [[pyroscope-continuous-profiling|profiler'а]], не tracer'а. Tracing для cross-service границ.
- Огромные attributes (whole JSON body), exporter затыкается, storage растёт. Атрибуты должны быть короткие.