# Distributed tracing: span, context propagation, sampling _Observability и мониторинг · LinuxLab Knowledge Base_ **TL;DR:** Tracing - граф spans (parent-child) одного логического запроса через сервисы. Context передаётся HTTP-header traceparent (W3C). Sampling: head (на edge, дёшево) или tail (в Collector, точнее). Backend: Jaeger, Tempo, Zipkin. ## Зачем 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 сервис: 1. Парсит входящий `traceparent` 2. Создаёт **child span** под `parent_span_id` = `span_id` из header'а 3. Генерит **новый** `span_id` для своего span'а 4. Передаёт **обновлённый** `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): ```python 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. Потом решаем по полной картине: ```yaml 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](/kb/chrony-and-ntp.md)). - **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 растёт. Атрибуты должны быть короткие. ## Команды ```bash curl -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' http://api/v1/test ``` Ручной trigger trace - downstream должен подцепить trace_id и span_id из header'а ```bash curl -s tempo:3200/api/traces/4bf92f3577b34da6a3ce929d0e0e4736 | jq '.batches[].scopeSpans[].spans[] | {name, durationNanos}' ``` Tempo trace by ID - все spans с длительностью ```bash curl -s 'tempo:3200/api/search?tags=service.name%3Dcheckout&minDuration=1s' ``` Tempo search API - найти медленные трассы checkout-сервиса ```bash OTEL_TRACES_SAMPLER=parentbased_traceidratio OTEL_TRACES_SAMPLER_ARG=0.05 ./app ``` Head sampling 5% с inheritance от parent (consistent через все сервисы) ```bash otelcol --config=tail-sampling.yaml ``` Запуск Collector с tail sampling - keep errors+slow+1pct random ```bash jaeger-query --query.base-path=/jaeger ``` Jaeger UI на :16686 - поиск traces по service/operation/tags ## См. также - [OpenTelemetry: signals, OTLP, Collector pipeline](/kb/opentelemetry.md) - [HTTP/2 internals - binary framing, HPACK, stream multiplexing](/kb/http2-internals.md) - [gRPC - HTTP/2 + Protobuf RPC framework](/kb/grpc-basics.md) - [Loki: label-based логи, LogQL, Promtail/Vector pipeline](/kb/loki-grafana-logging.md) - [Continuous profiling: Pyroscope, eBPF, flame graphs в проде](/kb/pyroscope-continuous-profiling.md)