1. Distributed tracing
Анатомия сквозного запроса
«Отладка кода вдвое сложнее его написания. Поэтому, если вы пишете код настолько изощренно, насколько способны, вам по определению не хватит сообразительности его отладить».
— Брайан Керниган
В предыдущих разделах мы подробно изучили метрики (которые первыми бьют тревогу «что-то сломалось») и логи (которые фиксируют детальные текстовые отчеты о событиях). Однако по мере того, как архитектура наших приложений эволюционирует от монолитов к распределенным микросервисам, мы сталкиваемся с новым классом непостижимых проблем.
Представьте классическую ситуацию: пользователь жалуется, что нажатие кнопки «Оформить заказ» в мобильном приложении зависает на мучительные 10 секунд.
- Метрики сервиса
frontendпоказывают всплеск задержки P99, но метрики шлюзаpaymentsи сервисаordersвыглядят абсолютно спокойно и зелено. Где именно теряются драгоценные секунды? - Логи сервиса
frontendлаконично сообщают:Error: timeout calling orders service. Но почему случился таймаут? Застрял сетевой пакет в виртуальном коммутаторе Kubernetes? Заблокировалась таблица в PostgreSQL? Или сервис аутентификации медленно проверял JWT-токен?
В монолитном приложении программист просто снял бы стек вызовов (Stack Trace) и за пару секунд увидел цепочку функций. Но в микросервисах классический стек вызовов физически разорван сетевыми границами. Чтобы восстановить его воедино, необходима Распределенная трассировка (Distributed Tracing).
Проблема невидимости в микросервисах
В монолите стек вызовов поддерживается самим процессором: инструкция ассемблера CALL кладет адрес возврата на стек (RSP), а отладчик легко проходит по цепочке стековых кадров.
В микросервисной архитектуре вызов функции превращается в сериализацию JSON или Protobuf, передачу через сокет TCP, маршрутизацию в Service Mesh и ожидание ответа. Каждый микросервис живет в своем собственном адресном пространстве на разных физических серверах. Без единого сквозного идентификатора система превращается в непроницаемый «черный ящик».
Distributed Tracing выполняет роль виртуального распределенного стека вызовов, пронизывающего все узлы и сервисы кластера.
Основные концепции
Трейсинг восстанавливает путь запроса через множество сервисов, баз данных и очередей.
flowchart TD
A["Span: Frontend HTTP<br>TraceID: abc-123"]
A --> B["Span: Auth Service<br>ParentID: A"]
A --> C["Span: Orders Service<br>ParentID: A"]
C --> D["Span: DB Query<br>ParentID: C"]
C --> E["Span: Cache Lookup<br>ParentID: C"]
style A fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#fff
style B fill:#312e81,stroke:#818cf8,color:#fff
style C fill:#312e81,stroke:#818cf8,color:#fff
style D fill:#881337,stroke:#f43f5e,stroke-width:2px,color:#fff
style E fill:#065f46,stroke:#10b981,color:#fff
1. Trace (Трейс)
Это полная история одного запроса от начала до конца. Trace объединяет все операции, произошедшие в рамках одного логического сценария (например, «Пользователь нажал кнопку Купить»).
Идентификатор трейса — Trace ID. Это глобальный уникальный ключ, который путешествует с запросом через все сервисы.
Физическая аналогия: трек-номер посылки в международной почтовой службе. Посылка меняет поезда, самолеты и склады, но ее единый трек-номер позволяет увидеть всю цепочку перемещений.
2. Span (Спан)
Это атомарная единица работы внутри трейса. Спан описывает, что происходило в конкретном месте.
- Имя:
HTTP GET /checkout - Время начала (Start Timestamp).
- Длительность (Duration).
- Parent ID: ID родительского спана (кто вызвал этот код).
- Атрибуты (Key-Value теги, например
http.status_code = 200,db.statement = "SELECT ..."). - События (Events: легковесные логи внутри спана с таймстемпом).
Трейс — это направленный ациклический граф (DAG), или дерево спанов, где у каждого дочернего спана есть ссылка на Parent ID.
Under the Hood: Context Propagation
Самая сложная часть трейсинга — это не сбор данных, а их проброс (Propagation). Как Trace ID попадает из сервиса A в сервис B?
В Go основным механизмом переноса контекста является context.Context. Когда вы используете библиотеку OpenTelemetry, она делает следующее:
- Injection (Инъекция): Перед отправкой HTTP-запроса в сервис B, клиент извлекает текущий Trace ID и Span ID из
context.Contextи вставляет их в HTTP-заголовки.- Стандартный заголовок:
traceparent(формат W3C Trace Context).
- Стандартный заголовок:
- Extraction (Экстракция): Сервис B получает HTTP-запрос. Middleware (HTTP Interceptor) проверяет наличие заголовка
traceparent, извлекает оттуда Trace ID и сохраняет его в свойcontext.Context.
Структура официального бинарного и текстового стандарта консорциума W3C:
HTTP Headers:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
^ ^-------------------------------- ^----------------- ^
| Trace ID (hex) Parent Span ID Flags
Version
00: Версия спецификации W3C Trace Context.4bf92f3577b34da6a3ce929d0e0e4736: 16-байтный (32 символа hex) глобальный Trace ID.00f067aa0ba902b7: 8-байтный (16 символов hex) Span ID вызывающей стороны (Parent Span ID).01: Битовые флаги трассировки (01означает, что данный трейс сэмплирован для записи).
Если вы забудете передать context.Context в вызов функции или HTTP-клиент, цепочка прервется. Начнется новый трейс, и вы потеряете связь между сервисами.
Mechanical Sympathy: Стоимость трейсинга
Трейсинг не бесплатен. Каждый спан требует ресурсов.
- CPU: Создание спана, генерация ID, запись timestamp. В Go это относительно дешево, но при 100,000 RPS накладные расходы могут составить 1-3% CPU.
- Memory: Спан — это объект в памяти. Он живет, пока запрос обрабатывается. Долгие запросы «висят» в памяти.
- Network: Данные о спанах нужно отправить в коллектор (Jaeger/Tempo).
Семплирование (Sampling)
Чтобы не утонуть в данных, применяют сэмплирование.
- Head-based Sampling: Решение о том, сохранять ли трейс, принимается в самом начале запроса (случайным образом, например, 1% трафика). Это дешево, но можно пропустить редкие ошибки.
- Tail-based Sampling: Решение принимается в конце. Сохраняются только «интересные» трейсы (с ошибками или очень медленные). Это требует буферизации всех спанов в памяти коллектора, что сложно и дорого.
Итог
Distributed Tracing — это «МРТ» для микросервисов. Он превращает разрозненные логи и метрики в связную историю жизни запроса.
- Trace ID связывает все события одного запроса.
- Span описывает конкретную операцию и её длительность.
- Context Propagation — механизм передачи ID между сервисами (через HTTP headers или gRPC metadata).
В следующей статье мы рассмотрим стандарт, который объединил в себе всё лучшее из мира телеметрии — OpenTelemetry: 2. OpenTelemetry.