Real-Time Backend

Distributed Tracing

Сообщение проходит через 7 сервисов за 340мс. Где потеряно 200мс из них? Без distributed tracing - это несколько часов поиска по логам. С ним - 30 секунд.

  • Slack в 2021 нашёл источник деградации за 8 минут благодаря трейсу через WebSocket -> Kafka -> PostgreSQL -> Push Service - без трейса аналогичный инцидент занял бы часы
  • Uber сократил MTTR production инцидентов с 45 до 12 минут после внедрения correlation ID через все 1000+ микросервисов
  • Discord при масштабировании до 26 млн concurrent WebSocket-соединений использует per-message spans чтобы изолировать медленные операции без анализа агрегированных метрик

Трейсинг WebSocket-соединений

**Февраль 2021, Slack.** В 14:23 пришли первые жалобы - сообщения доходят с задержкой 8-12 секунд. Инженеры смотрят в метрики: p99 latency в норме, CPU спокоен, Redis отвечает. Четыре часа поиска вслепую. Потом кто-то нашёл WebSocket-соединение, которое застряло на конкретном backend-узле с версией на 2 недели старее остальных. Без трейса - четыре часа тумана.

HTTP-трейсинг работает просто: каждый запрос отдельный, трейс идёт вместе с ним в заголовках. WebSocket ломает эту модель. Одно соединение живёт часами, по нему летят тысячи сообщений. Как прицепить трейс к конкретному сообщению, не к соединению в целом?

Ответ: **per-message span**. Каждое сообщение - отдельный span внутри долгоживущего root-трейса соединения. Root span открывается при handshake и закрывается при disconnect. Каждый message-span ссылается на него как на parent.

Discord обрабатывает 26 млн concurrent WebSocket-соединений. Без per-message span отлаживать конкретный message drop в этом масштабе нереально - метрики показывают агрегаты, трейс показывает конкретный путь.

Какой подход правильный для трейсинга WebSocket-сессии с тысячами сообщений?

Correlation IDs через async-границы

Сообщение пришло по WebSocket, обработалось, ушло в Kafka, там подхватил consumer, записал в PostgreSQL, отправил push-уведомление через FCM. Семь сервисов. Три разных технологии транспорта. Как склеить эти операции в один трейс?

Correlation ID - строка, которая путешествует вместе с данными через все границы. Это может быть trace ID из OpenTelemetry, или самодельный UUID, или оба вместе. Главное правило: **он никогда не теряется**. Каждая система, которая получает данные, обязана пробросить его дальше.

Uber в 2018 опубликовал, что correlation ID помог сократить MTTR (mean time to resolution) для production инцидентов с 45 до 12 минут. Разница - в том, что инженер сразу видит полный путь запроса, а не собирает его по кусочкам из разных логов.

AsyncLocalStorage в Node.js позволяет не передавать context явно через каждый вызов функции. Контекст живёт в async-scope автоматически - всё что запускается в рамках одного async-flow получает один контекст без явной передачи.

Что происходит с correlation ID при переходе WebSocket-сообщения в Kafka?

Propagation стандарты: W3C и B3

До 2019 года каждая система трейсинга имела свой формат заголовков. Zipkin использовал `X-B3-TraceId`, Jaeger - `uber-trace-id`, AWS X-Ray - `X-Amzn-Trace-Id`. Когда запрос шёл через три компании с разными системами - цепочка рвалась.

W3C Trace Context (RFC утверждён в 2021) стандартизировал это. Два заголовка: `traceparent` содержит version, trace-id, parent-id и flags. `tracestate` - вендор-специфичные данные. Теперь Jaeger понимает трейс из Zipkin и наоборот.

Для WebSocket контекст передаётся иначе - нет заголовков на уровне сообщения. Два паттерна: **envelope** (обернуть каждое сообщение в объект с полем `traceContext`) и **initial handshake** (передать trace ID при подключении, дальше использовать per-message span IDs). Envelope дороже по размеру, handshake не даёт видеть путь конкретного сообщения.

Какие два заголовка определяет стандарт W3C Trace Context?

Инструменты: Jaeger, Tempo, OpenTelemetry

OpenTelemetry (OTel) - это не инструмент хранения, это стандарт инструментации. SDK для Node.js, Python, Go - они генерируют spans и экспортируют их куда скажешь. Куда - Jaeger, Grafana Tempo, Honeycomb, Datadog - это уже бэкенды хранения и визуализации.

Sampling - ключевое решение. Сохранять 100% трейсов нереально при масштабе Slack (26 млн concurrent connections). Типичный подход: **tail-based sampling** - решение принимается после завершения трейса. Ошибки и медленные запросы (p95) - 100%. Нормальные - 1-5%.

Grafana Tempo хранит трейсы в object storage (S3, GCS) без индексации - это радикально дешевле Jaeger с Elasticsearch. Поиск по trace ID мгновенный, поиск по атрибутам требует Grafana Loki как индекс поверх. Для большинства production-сценариев Tempo дешевле в 5-10 раз.

  • **Jaeger** - open source, Elasticsearch backend, развитый UI для анализа трейсов, хорошо для self-hosted
  • **Grafana Tempo** - дешёвое хранение в S3, интеграция с Loki и Prometheus, облачный SaaS
  • **Honeycomb** - tail-based sampling из коробки, columnar storage, аналитика по произвольным атрибутам
  • **Datadog APM** - полный observability stack, дорого, но не требует самостоятельного управления инфраструктурой

Логирование request ID в каждом сервисе - это то же самое что distributed tracing

Логи с correlation ID дают что-то, но не структурированную иерархию spans с latency на каждом шаге и связями parent-child

Чтобы найти медленный шаг в цепочке из 7 сервисов, нужна временная шкала spans с точными timestamps. Grep по correlation ID в разных лог-системах даёт только факт прохождения через сервис, но не timing и не граф зависимостей.

Что такое tail-based sampling в distributed tracing?

Итоги

  • WebSocket требует per-message spans внутри root span соединения - иначе теряется детализация по конкретным сообщениям
  • Correlation ID пробрасывается через все транспортные границы (HTTP headers, Kafka headers, gRPC metadata) и никогда не теряется
  • W3C Trace Context (traceparent + tracestate) - стандарт 2021 года, заменивший зоопарк форматов Zipkin/Jaeger/X-Ray
  • Tail-based sampling позволяет сохранять 100% трейсов с ошибками и только 1-10% нормального трафика

Связанные темы

Distributed tracing - часть более широкой observability-системы

  • Структурированное логирование — Логи и трейсы связываются через trace ID для полной картины
  • Метрики и Prometheus — Трейсы дают детализацию, метрики - агрегаты; вместе это полный observability stack
  • Kafka и async messaging — Async-границы - основная сложность propagation; correlation ID решает разрыв трейса
  • Service Mesh (Istio/Linkerd) — Service mesh автоматически добавляет tracing на уровне sidecar без изменений в коде сервисов

Вопросы для размышления

  • Как изменится стратегия sampling при переходе от 10K до 10M concurrent WebSocket-соединений?
  • Что происходит с трейсом когда клиент переподключается после обрыва - новый трейс или продолжение старого?
  • Как передавать trace context в браузерном WebSocket-клиенте, если нельзя добавить произвольные заголовки при подключении?

Связанные уроки

  • dist-06-ordering
Distributed Tracing

0

1

Войти