🏗️ Раздел 17 · Вопрос #22

Что такое distributed tracing

В микросервисной архитектуре одна кнопка в мобильном приложении запускает цепочку из 10–20 межсервисных вызовов. Если запрос тормозит или падает с ошибкой:


🟢 Junior Level

Distributed Tracing (Распределённая трассировка) — это метод диагностики и мониторинга, который позволяет отследить полный путь прохождения пользовательского запроса через всю цепочку микросервисов, баз данных и очередей сообщений.

Пользователь: POST /checkout
       │
       ▼
[API Gateway] ─────────────── (Общее время: 240 мс) ────────────────┐
  │                                                                 │
  ├──► [Order Service] ────── (180 мс) ─────────────────────────┐   │
  │      │                                                      │   │
  │      ├──► [Payment Service] ─── (120 мс: УЗКОЕ МЕСТО! 🐢) ──┤   │
  │      │                                                      │   │
  │      └──► [Inventory Service] ─ (30 мс) ────────────────────┘   │
  │                                                                 │
  └──► [Notification Service] (Асинхронно через Kafka: 15 мс) ──────┘

Зачем это нужно?

В микросервисной архитектуре одна кнопка в мобильном приложении запускает цепочку из 10–20 межсервисных вызовов. Если запрос тормозит или падает с ошибкой:

  • Обычные логи бессильны: они размазаны по десяткам серверов, и невозможно понять, какая строчка лога в сервисе платежей относится к запросу пользователя из мобильного приложения.
  • Distributed Tracing показывает точный таймлайн (водопад задержек / Waterfall Chart): какой именно сервис вызвал задержку, на какой строчке кода или SQL-запросе произошло зависание и где упала ошибка.

🟡 Middle Level

1. Анатомия трассировки: Trace vs Span

Модель данных основана на фундаментальной статье Google Dapper (2010):

Trace (Дерево вызовов, Trace ID: 4bf92f3577b34da6a3ce929d0e0e4736)
├── Span A: HTTP POST /checkout (Root Span, Gateway)
│   ├── Span B: OrderService.createOrder()
│   │   ├── Span C: SQL INSERT INTO orders
│   │   └── Span D: HTTP POST /charge (PaymentService)
│   │       └── Span E: External Bank API Call
│   └── Span F: Kafka.send("order-events")
  1. Trace (Трейс): Направленный ациклический граф (DAG) спанов, представляющий весь сквозной путь транзакции от клиента до финального ответа. Имеет глобальный уникальный Trace ID.
  2. Span (Спан): Единичный отрезок непрерывной работы внутри одного сервиса. Содержит:
    • Имя операции (например, SELECT * FROM users).
    • Временные метки начала и завершения (Start Time, End Time $\to$ вычисляется Duration).
    • Ссылку на родительский спан (Parent Span ID).
    • Attributes (Теги): Метаданные (http.status_code=200, db.statement="SELECT...").
    • Events (События): Временные отметки логов внутри спана (например, момент получения ответа от сокета).
    • Status: OK или ERROR (с записью стека исключения).

2. Стандарт распространения контекста: W3C TraceContext

Чтобы разные сервисы понимали, что они обрабатывают один и тот же запрос, контекст передается через стандартный заголовок traceparent:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
              │                  │                      │          │
           версия             Trace ID              Parent Span ID  флаги (01 = sampled)

3. Реализация в Spring Boot 3 (Micrometer Tracing + OpenTelemetry)

Важно: Проект Spring Cloud Sleuth полностью устарел и закрыт в Spring Boot 3+. Современный стандарт — Micrometer Tracing с движком OpenTelemetry (OTel) или Brave:

<!-- Зависимости для Spring Boot 3 -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>

Программное создание пользовательских спанов:

@Service
@RequiredArgsConstructor
public class PaymentProcessingService {

    private final Tracer tracer; // io.micrometer.tracing.Tracer

    public PaymentResult processPayment(PaymentOrder order) {
        // Создаем дочерний спан для измерения критической бизнес-логики
        Span newSpan = tracer.nextSpan().name("fraud-check-and-charge").start();
        try (Tracer.SpanInScope ws = tracer.withSpan(newSpan.start())) {
            newSpan.tag("order.id", order.id().toString());
            newSpan.tag("order.amount", order.amount().toPlainString());

            return executeBankingTransaction(order);
        } catch (Exception ex) {
            newSpan.error(ex); // Фиксируем исключение прямо в трейсе
            throw ex;
        } finally {
            newSpan.end(); // Завершаем замер времени
        }
    }
}

🔴 Senior Level

1. Стратегии сэмплирования: Head-Based vs Tail-Based Sampling

В системе с трафиком 100 000 RPS сохранять 100% трейсов на диск физически невозможно (это потребует петабайты хранилища и забьет сетевую полосу):

Head-Based Sampling (Сэмплирование на входе):
[Клиент] ──► [API Gateway] ──(Бросок монетки: 1% трафика)──► Трассируется
(Минус: Редкая ошибка 500 с вероятностью 99% будет пропущена!)

Tail-Based Sampling (Сэмплирование на выходе через OTel Collector):
[Микросервисы] ──(Шлют 100% спанов в буфер)──► [OpenTelemetry Collector]
                                                        │
                                                        ├── Ошибка 500? ──────► СОХРАНИТЬ В БД! ✅
                                                        ├── Latency > 2 сек? ─► СОХРАНИТЬ В БД! ✅
                                                        └── Обычный запрос ───► Отбросить 99%
  • Head-Based Sampling: Решение принимается в самом начале запроса на шлюзе (например, случайно выбирать ровно 1% запросов). Недостаток: Если 1 из 10 000 запросов падает с ошибкой, сэмплирование почти наверняка выбросит именно упавший запрос.
  • Tail-Based Sampling (Современный Enterprise-стандарт): Сервисы отправляют спаны в буфер OpenTelemetry Collector. Коллектор собирает весь трейс целиком и применяет бизнес-правила: если в трейсе была хоть одна ошибка ERROR или время ответа превысило 1.5 секунды — трейс сохраняется в БД со 100% гарантией.

2. Потеря Trace Context в асинхронном коде (MDC & Reactive Pitfall)

Идентификаторы traceId и spanId автоматически подставляются в логи через MDC (Mapped Diagnostic Context), который под капотом использует ThreadLocal. Когда контекст мгновенно теряется:

  1. При переходе в CompletableFuture.supplyAsync() или кастомный ExecutorService.
  2. В реактивном стеке Spring WebFlux / Project Reactor, где обработка одного HTTP-запроса скачет по десяткам потоков Netty Event Loop.
  3. В аннотациях @Async.

Решение в Spring Boot 3: Использование модуля Micrometer Context Propagation:

@Configuration
public class AsyncConfig {

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        // TaskDecorator копирует контекст трассировки из родительского потока в дочерний
        executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
        executor.initialize();
        return executor;
    }
}

А для Project Reactor включается глобальный хук:

Hooks.enableAutomaticContextPropagation();

4 Tricky Questions

1. Как устроен заголовок W3C TraceContext traceparent и почему версия зафиксирована как 00?

Ответ: Заголовок имеет строгий формат из 4 полей, разделенных дефисами: version-trace_id-parent_id-trace_flags:

  1. version (2 hex-символа): Текущая версия стандарта равна 00. Значение ff зарезервировано под будущие версии.
  2. trace_id (32 hex-символа / 16 байт): Глобальный уникальный ID всего трейса (генерируется на входе в систему). Не может состоять из одних нулей.
  3. parent_id (16 hex-символов / 8 байт): ID входящего родительского спана.
  4. trace_flags (2 hex-символа / 8 бит): Битовая маска. Младший бит 01 означает Sampled (трейс должен быть сохранен хранилищем). Стандарт гарантирует, что даже если гетерогенные сервисы написаны на 10 разных языках (Java, Go, Rust, Python), контекст не потеряется при сквозных сетевых вызовах.

2. Чем Tail-Based Sampling в OpenTelemetry Collector принципиально лучше сэмплирования на уровне Java-приложения?

Ответ: Java-приложение в момент начала запроса (Head) не знает, упадет ли этот запрос с исключением через 2 секунды в третьем сервисе. Если приложение настроено сэмплировать 5% трафика, оно с вероятностью 95% пропустит уникальную аварию. Tail-Based Sampling переносит решение на уровень внешнего агента (OpenTelemetry Collector):

  1. Все микросервисы без раздумий стримят 100% спанов по легковесному протоколу gRPC (OTLP) на локальный коллектор.
  2. Коллектор держит спаны в кольцевом буфере памяти до завершения трейса.
  3. Если статус трейса ERROR или длительность превысила заданный порог SLA, коллектор отправляет его в постоянное хранилище (Jaeger / Tempo / ClickHouse). Успешные рутинные трейсы агрессивно отбрасываются.

3. Каковы реальные накладные расходы (Overhead) от включения Distributed Tracing в Highload-системах?

Ответ:

  1. CPU Overhead: Генерация криптографически стойких или случайных 128-битных ID, захват текущего времени (системные вызовы System.currentTimeMillis() / clock_gettime), сериализация структур спанов.
  2. Память (Heap Allocation): Создание объектов Span, Scope, Map с атрибутами создает дополнительную нагрузку на Garbage Collector.
  3. Сетевой трафик: Если трейс содержит 50 спанов, объем телеметрии может превысить полезную нагрузку бизнес-данных. Минимизация: Использование Protobuf/gRPC экспортера (OTLP), батчинг отправки спанов (BatchSpanProcessor) в отдельном фоновом демоне, и сэмплирование (Head или Tail).

4. В чем разница между Tracing Span Tag (Attribute) и Span Log (Event)?

Ответ:

  • Tag / Attribute: Статическое свойство, описывающее весь спан целиком (например, http.method = "POST", customer.tier = "VIP"). Атрибуты применяются для фильтрации и поиска трейсов в UI (например, найти все трейсы с http.status_code = 500).
  • Log / Event: Точечное событие, привязанное к конкретной миллисекунде внутри спана (содержит timestamp и текст). Например: «00:00.120: Отправлен TCP пакет», «00:00.340: Получен первый байт ответа». Используется для детального помиллисекундного анализа хода выполнения тяжелой операции.

🎯 Шпаргалка для интервью

Главное:

  • Distributed Tracing отслеживает сквозной путь одного клиентского запроса через граф микросервисов.
  • Trace — полный путь запроса (DAG спанов); Span — единичный отрезок работы внутри одного сервиса с замером времени.
  • Стандарт передачи контекста — W3C TraceContext (заголовок traceparent).
  • Стандарт индустрии — OpenTelemetry (OTel), бэкенды — Jaeger, Grafana Tempo, SigNoz.
  • В Spring Boot 3 вместо устаревшего Spring Cloud Sleuth используется Micrometer Tracing.
  • Log Correlation: Трассировка связывается с логами через автоматическую вставку traceId и spanId в MDC Logback/SLF4J.
  • На высоких нагрузках используется Tail-Based Sampling (отбор 100% ошибок и медленных запросов через OTel Collector).

Красные флаги (Чего говорить НЕЛЬЗЯ):

  • ❌ «Spring Cloud Sleuth — современный инструмент для Spring Boot 3» (Sleuth мертв, в Spring Boot 3 используется Micrometer Tracing).
  • ❌ «В продакшене мы сохраняем 100% трейсов при 50 000 RPS» (Это переполнит сеть и диски хранилища телеметрии; сэмплирование обязательно).
  • ❌ «Трассировка заменяет собой логирование» (Трейсинг показывает граф и тайминги задержек, а логи раскрывают детальный контекст исполнения).

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