🏗️ Section 17 · Question #22

What is distributed tracing

In a distributed topology, a single user click can trigger 10 to 20 downstream network calls across different hosts.


🟢 Junior Level

Distributed Tracing is an observability technique used to profile, visualize, and monitor the end-to-end execution journey of a single user request as it traverses a complex distributed graph of microservices, databases, caches, and message brokers.

Client: POST /checkout
       │
       ▼
[API Gateway] ─────────────── (Total End-to-End Latency: 240 ms) ─────────┐
  │                                                                       │
  ├──► [Order Service] ────── (180 ms) ───────────────────────────────┐   │
  │      │                                                            │   │
  │      ├──► [Payment Service] ─── (120 ms: BOTTLENECK IDENTIFIED! 🐢) ──┤   │
  │      │                                                            │   │
  │      └──► [Inventory Service] ─ (30 ms) ──────────────────────────┘   │
  │                                                                       │
  └──► [Notification Service] (Asynchronous via Kafka: 15 ms) ────────────┘

Why Traditional Logging Fails in Microservices

In a distributed topology, a single user click can trigger 10 to 20 downstream network calls across different hosts.

  • Isolated Log Files: If a request slows down or fails, independent log streams scattered across dozens of container instances are impossible to correlate manually.
  • Waterfall Latency Visibility: Distributed tracing provides a single visual timeline (Waterfall Chart) showing exactly which microservice, database query, or external third-party API introduced latency, down to the exact millisecond and line of code.

🟡 Middle Level

1. Tracing Anatomy: Trace vs Span (Google Dapper Model)

The data model for distributed tracing originates from Google’s seminal Dapper (2010) research paper:

Trace (Call Tree, Trace ID: 4bf92f3577b34da6a3ce929d0e0e4736)
├── Span A: HTTP POST /checkout (Root Span, API Gateway)
│   ├── Span B: OrderService.createOrder()
│   │   ├── Span C: SQL INSERT INTO orders
│   │   └── Span D: HTTP POST /charge (PaymentService)
│   │       └── Span E: External Stripe API Call
│   └── Span F: Kafka.send("order-events")
  1. Trace: The complete Directed Acyclic Graph (DAG) of spans representing an entire end-to-end distributed transaction from initial client ingress to final response. Identified by a globally unique 128-bit Trace ID.
  2. Span: A single contiguous, timed unit of work executed within an individual service boundary. A span contains:
    • Operation Name: E.g., SELECT * FROM orders or POST /charge.
    • Timestamps: Start time and end time (calculating precise Duration).
    • Span ID & Parent Span ID: Hex identifiers establishing hierarchical parent-child relationships.
    • Attributes (Tags): Key-value metadata describing the span (e.g., http.status_code=200, db.system=postgresql).
    • Events (Logs): Timestamped textual annotations recording discrete milestones within the span.
    • Status: OK or ERROR (with captured exception class and stack trace).

2. Context Propagation Protocol: W3C TraceContext

To maintain trace continuity across heterogeneous microservices, tracing context is propagated across network boundaries using the standard W3C TraceContext HTTP header:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
              │                  │                      │          │
           Version            Trace ID              Parent Span ID  Flags (01 = Sampled)

3. Spring Boot 3 Implementation: Micrometer Tracing & OpenTelemetry

[!WARNING] Spring Cloud Sleuth is deprecated and removed in Spring Boot 3.x! The modern enterprise standard is Micrometer Tracing with an OpenTelemetry (OTel) or Brave bridge:

<!-- pom.xml: Spring Boot 3 Micrometer Tracing with OpenTelemetry -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>

Programmatic Custom Span Instrumentation:

@Service
@RequiredArgsConstructor
public class PaymentProcessingService {

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

    public PaymentResult processPayment(PaymentOrder order) {
        // Create an explicit child span to isolate business execution latency
        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); // Automatically records exception metadata in the span
            throw ex;
        } finally {
            newSpan.end(); // Stops the clock and flushes telemetry
        }
    }
}

🔴 Senior Level

1. Sampling Strategies at Scale: Head-Based vs Tail-Based Sampling

At 100,000 requests per second, storing 100% of distributed traces on disk causes massive storage bloat and saturates internal network bandwidth:

Head-Based Sampling (Decided at Ingress):
[Client] ──► [API Gateway] ──(Random 1% Coin Flip)──► Traced
(Major Flaw: A rare 1-in-a-million HTTP 500 error has a 99% probability of being discarded!)

Tail-Based Sampling (Decided at Egress via OpenTelemetry Collector):
[Microservices] ──(Stream 100% of spans to local memory buffer)──► [OpenTelemetry Collector]
                                                                        │
                                                                        ├── Contains Error? ────► PERSIST TO DB! ✅
                                                                        ├── Duration > 1.5s? ───► PERSIST TO DB! ✅
                                                                        └── Fast 200 OK Trace ──► Discard 99%
Strategy Decision Point Advantages Disadvantages
Head-Based Sampling Decided at the root ingress point (API Gateway). Simple, low CPU/memory overhead on services. Discards critical rare errors and anomalous latency spikes before outcome is known.
Tail-Based Sampling Decided at the OpenTelemetry Collector after the trace completes. Guarantees 100% retention of errors and p99 anomalies while discarding uninteresting successful requests. Requires running collector infrastructure with in-memory buffering.

2. Context Propagation Pitfalls: MDC and Reactive Threads

Tracing frameworks inject traceId and spanId into SLF4J’s MDC (Mapped Diagnostic Context) so that structured log entries include correlation IDs ([traceId=4bf92f..., spanId=00f067...]). Because MDC relies under the hood on ThreadLocal, trace context is silently lost when switching threads:

  1. When submitting tasks to CompletableFuture.supplyAsync() or a custom ExecutorService.
  2. Inside Spring @Async methods.
  3. In reactive architectures (Spring WebFlux / Project Reactor), where request processing hops across non-blocking Netty Event Loop threads.

The Modern Spring Boot 3 Solution:

Use Micrometer Context Propagation:

@Configuration
public class AsyncThreadPoolConfig {

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        // Automatically copies MDC and trace context across thread boundaries
        executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
        executor.initialize();
        return executor;
    }
}

For Project Reactor applications, enable automatic context propagation globally:

Hooks.enableAutomaticContextPropagation();

4 Tricky Questions

1. How is the W3C TraceContext traceparent header structured, and what do each of its four delimited fields represent?

Answer: The traceparent header is formatted as four hyphen-delimited fields: version-trace_id-parent_id-trace_flags:

  1. version (2 hex characters): Current standard version is 00. (ff is reserved for future revisions).
  2. trace_id (32 hex characters / 16 bytes): Globally unique identifier for the entire distributed transaction. Cannot be all zeros (00000000000000000000000000000000).
  3. parent_id (16 hex characters / 8 bytes): Identifier of the caller’s active span that spawned the outbound request.
  4. trace_flags (2 hex characters / 8-bit bitmap): Controls tracing behavior. The least significant bit 01 signifies Sampled (the upstream service has chosen to record this trace, instructing downstream services to record it as well).

2. Why is Tail-Based Sampling in an OpenTelemetry Collector fundamentally superior to Head-Based sampling inside application code?

Answer: When an application makes a sampling decision at the start of a request (Head-Based), it has zero foresight regarding whether that specific transaction will succeed, fail with an unexpected null pointer exception, or hang for 10 seconds on a database lock. If the service samples 5% of traffic, it has a 95% chance of silently discarding the exact trace needed to debug an outage. Tail-Based Sampling defers the decision until the entire trace graph finishes executing:

  1. Microservices stream 100% of spans via lightweight OTLP (gRPC) to a local OpenTelemetry Collector daemon.
  2. The collector holds spans in an in-memory ring buffer until the root span terminates.
  3. The collector applies intelligent filtering rules: if any span in the trace has status.code = ERROR or duration > SLA_THRESHOLD, the entire trace is preserved and written to persistent storage (Jaeger, Grafana Tempo). Normal routine traces are aggressively sampled at 1%, maximizing diagnostic value while minimizing storage costs.

3. What are the concrete performance overheads of distributed tracing in Highload systems, and how are they minimized?

Answer:

  1. CPU Utilization: Generating 128-bit cryptographically random IDs, retrieving high-resolution system clock timestamps via System.currentTimeMillis() / clock_gettime, and serializing span data structures.
  2. JVM Heap Allocation: Instantiating Span, Scope, and attribute map objects generates garbage collection pressure on the Eden space.
  3. Network Overhead: In high-volume microservices, transmitting detailed span metadata over HTTP/JSON can consume more bandwidth than the application payload itself. Mitigation: Use lightweight binary Protobuf over gRPC (OTLP), buffer spans in memory using a non-blocking BatchSpanProcessor executed on a background daemon thread, and apply intelligent tail-based sampling.

4. What is the technical difference between a Span Tag (Attribute) and a Span Log (Event)?

Answer:

  • Span Tag / Attribute: A static key-value pair that characterizes the entire span as a whole (e.g., http.route = "/api/v1/orders/{id}", db.name = "customers"). Attributes are indexed by tracing backends to enable fast query filtering (e.g., “Find all traces where http.status_code = 500”).
  • Span Log / Event: A timestamped string annotation that records an instantaneous milestone occurring at a specific millisecond within the span’s lifecycle. For example: [12:00:00.150 - "Cache miss, reading from PostgreSQL"] or [12:00:00.420 - "Received TLS handshake ACK"]. Events are used for in-depth execution milestone analysis within a single span.

🎯 Interview Cheat Sheet

Must Know:

  • Distributed Tracing: Maps the end-to-end execution path and latency breakdown of a single request across microservices.
  • Data Model: Trace (entire DAG) identified by Trace ID; Span (single timed operation) identified by Span ID.
  • Context Header: W3C TraceContext (traceparent: 00-traceid-parentid-01).
  • Spring Boot 3 Standard: Micrometer Tracing + OpenTelemetry (OTel). (Spring Cloud Sleuth is deprecated and unsupported).
  • Log Correlation: Bridges tracing to logging by injecting traceId and spanId into SLF4J’s MDC.
  • Context Propagation Trap: ThreadLocal context in MDC is lost in @Async, CompletableFuture, and WebFlux; resolve via ContextPropagatingTaskDecorator.
  • Sampling: Use Tail-Based Sampling in an OpenTelemetry Collector to capture 100% of errors and p99 slow requests while discarding routine 200 OKs.

Common Follow-up Questions:

  • “How does tracing propagate through Apache Kafka?” → The producer injects the W3C traceparent string into the Kafka RecordHeaders; the @KafkaListener consumer extracts the header to start a child span, preserving end-to-end trace lineage.
  • “What are the standard open-source tracing backends?” → Jaeger, Grafana Tempo, and Zipkin (queried via Grafana).

Red Flags (DO NOT Say):

  • ❌ “Spring Cloud Sleuth is the recommended library for Spring Boot 3 applications.” (Sleuth is dead; Micrometer Tracing is the modern replacement).
  • ❌ “We trace 100% of all requests in production at 100,000 RPS without sampling.” (Will cause network saturation and multi-terabyte storage exhaustion).
  • ❌ “Distributed tracing replaces centralized logging.” (Tracing provides the call graph and timing waterfalls; logs provide detailed semantic debugging context).