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-клиенте, если нельзя добавить произвольные заголовки при подключении?