🏗️ Розділ 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

Щоб гетерогенні сервіси розуміли, що вони обробляють один спільний потік, контекст передається через стандартизований HTTP-заголовок 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 або час виконання перевищив поріг SLA — трейс зберігається в базу даних зі 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 байт): Глобальний унікальний ідентифікатор усього трейсу (генерується на вході в систему). Не може складатися лише з нулів.
  3. parent_id (16 hex-символів / 8 байт): ID вхідного батьківського спана.
  4. trace_flags (2 hex-символи / 8 біт): Бітова маска. Молодший біт 01 означає Sampled (трейс підлягає обов’язковому збереженню у сховищі). Стандарт гарантує, що навіть коли сервіси написані різними мовами (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), серіалізація спанів у Protobuf.
  2. Пам’ять (Heap Allocation): Створення об’єктів Span, Scope та Map з атрибутами створює додатковий тиск на Garbage Collector.
  3. Мережевий трафік: Якщо довгий трейс містить 50 спанів, обсяг телеметрії може перевищити розмір корисних бізнес-даних. Мінімізація: Використання Protobuf/gRPC експортера (OTLP), пакетована відправка спанів (BatchSpanProcessor) в окремому фоновому потоці та адаптивне семплювання.

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), сховища та UI — 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» (Це переповнить мережу та диски сховища телеметрії; семплювання є обов’язковим).
  • ❌ «Трасування повністю замінює собою логування» (Трейси показують граф і затримки, а логи розкривають детальний контекст).

Пов’язані теми: