System Design · Observability
Debugging Tools for Distributed Systems
A payment fails in one region. Twelve services touch the request. SSH into a random pod and reading stdout will not find the race. You need a disciplined path from golden metrics to one correlated request.
Start with , jump via an into , read keyed by a , then profile or explain the slow leaf. Keep a ready so you do not turn every pod into a firehose.
Debug journey
Who is sick: route, region, version.
Which edge reddened first.
Exemplar or tail-sampled error.
Fix leaf + verify
Same histogram and error rate.
What actually happens
Monoliths let you set a breakpoint. Distributed systems replace that with propagation: every hop must forward trace context, stamp the same correlation id on logs, and export compatible spans. If the queue consumer starts a new root without a link, you lose the story between “API accepted” and “worker failed.”
Tooling is layered. Fleet and a live answer blast radius. Traces answer causality. Logs answer payload-level detail (with redaction). Profilers and DB explains answer “why is this span wide.” and are rare, high-cost tools for when the stack is lying or the process is dead.
and partial sampling are the silent enemies: two hosts disagree on order, or the only bad request was dropped by head sampling. Design for tail sampling of errors and for NTP-disciplined clocks.
How to choose a debugging tool
1. Is the fleet sick? Metrics and maps first—do not open a random trace.
2. Do you need causality? Tracing with an exemplar or forced debug header.
3. Is the leaf opaque? Profile, EXPLAIN, queue depth—or a scoped canary with richer telemetry—not global debug logging.
Simple rule of thumb
One golden signal → one correlated request → one leaf fix → re-check the same signal. If you cannot navigate that chain, invest in propagation and exemplars before buying another dashboard.
The debug path
Read the diagram definition
flowchart TD
Alert["Alert / user report"] --> Golden["Golden metrics: latency, errors, saturation"]
Golden --> Scope["Scope: region, version, route, tenant"]
Scope --> Map["Service map / dependency edges"]
Map --> Trace["Exemplar or sampled trace"]
Trace --> Logs["Structured logs with correlation ID"]
Logs --> Local["Profile / DB explain / queue depth"]
Local --> Hypothesis["Hypothesis + fix or mitigate"]
Hypothesis --> Verify["Verify on same golden signal"]Incident card (fill in order):
Symptom: p99 /checkout > 2s in us-east, error rate flat
Scope: version 1.14.2, not 1.14.1; only VIP tenants
Map edge: API → Inventory latency up
Exemplar: trace 4bf9… span inventory.GetStock 1.8s
Logs: correlation_id=4bf9… lock_wait_ms=1600
Leaf: contention on stock row / missing index
Mitigate: cache + timeout + degrade recommendations
Fix: index + shorter transaction
Verify: same histogram and inventory span widthContext propagation
Read the diagram definition
flowchart LR
Edge["Edge / gateway"] -->|"traceparent"| API["API"]
API -->|"traceparent"| Pay["Payments"]
API -->|"traceparent"| Inv["Inventory"]
API -->|"baggage? careful"| Rec["Recommendations"]
Pay --> DB1["DB"]
Inv --> Cache["Cache"]
Rec --> MQ["Queue"]
MQ -.->|"links / new root?"| Worker["Async worker"]Use W3C traceparent (and optionally tracestate). Treat inbound baggage as untrusted input—do not copy it wholesale into metric labels. For async workers, either continue the parent context or create a child with an explicit link back to the producer span so UIs can still join the story.
Gateways must forward client debug flags only when authorized (employee device, signed header). Otherwise attackers force 100% sampling and blow your observability budget—a real production outage mode.
Tool kit by layer
Metrics & service maps
Distributed tracing
Structured logs
Leaf tools: profile, EXPLAIN, queues
Heavy tools: canary, mirror, chaos, pcap, core
Worked example: “random” timeouts
Clients see intermittent 504s. Each service dashboard looks “mostly green.” Averages hide a dependency that only fails under fan-out.
Walkthrough:
1. Metrics: gateway 504 rate up; API error budget burning
2. Slice by route: /home fans out to 20 backends
3. Service map: recommendations edge yellow (p99)
4. Exemplar trace: 19 spans fine; recommendations 2.5s then cancel
5. Logs: recommendations — "redis: connection pool exhausted"
6. Leaf: pool size 10, 200 concurrent handlers; queueing
7. Mitigate: timeout 200ms + stale cache + lower fan-out
8. Fix: enlarge pool, pipeline, or dedicated replica
9. Verify: gateway 504 and recommendations p99 recover
Why it felt random:
Only pages that included the recommendations card waited.
Head sampling dropped many failures until tail policy kept errors.Failure modes of the tools themselves
| Failure | Symptom | Mitigation |
|---|---|---|
| Broken propagation | Orphan spans / many roots | Mesh or SDK checks; contract tests |
| Head-sample bias | Errors missing from UI | Tail sample errors/slow; unsampled metrics |
| Time skew | Negative durations; weird order | NTP; causal IDs over wall clocks |
| Log cardinality | Indexer melt / huge bill | Allowlist fields; sample successes |
| Debug header abuse | 100% trace ingest | Authz on force-sample flags |
Trade-offs
| Choice | Gain | Cost |
|---|---|---|
| Always-on traces @ 1% | Cheap baseline | Miss rare bugs without tail policy |
| Tail sampling | Keep errors/slow paths | Collector memory & complexity |
| Verbose debug canary | Rich evidence, small blast radius | Skewed mix if canary traffic differs |
| Request mirroring | Real traffic comparison | Side effects & double load |
| Packet capture | Ground truth on the wire | Privacy, volume, skill cost |
| SSH + printf | Familiar | Does not scale; loses causality |
Choosing and explaining debugging distributed systems
A complete explanation connects the mechanism to a requirement. Describe what improves when you introduce it, which new failure cases become possible, and how you would know that the trade-off is worthwhile.
Explain the core idea
Navigate metrics → map → exemplar trace → correlated logs → leaf profile/EXPLAIN.
Propagation and sampling policy are part of the design, not an afterthought.
Mitigate user pain first, then prove the root cause with the same golden signal.
When it belongs
Designing observability for a multi-service system.
Explaining how you would handle a regional latency incident.
Choosing sampling, canaries, or mirroring for risky deploys.
How it fails
Broken trace context across queues or mesh hops.
Head sampling dropping the only failing requests.
Debug flags usable by the public internet.
Time skew scrambling span order.
Trade-offs to weigh
Telemetry fidelity versus cost and privacy.
Tail sampling power versus collector complexity.
Deep canary verbosity versus representative traffic.
Common misunderstandings
Opening a random trace before scoping with metrics.
Trusting averages when the map shows one red edge.
Logging unbounded fields as indexed labels.
Calling chaos “debugging” without a hypothesis and abort criteria.
Wrap-up
Distributed debugging is a designed path through signals, not heroics on a single VM. Build the path before the incident: context propagation, exemplars, structured logs, and a canary switch.
Exercises
- Missing chapter. API traces end before the email worker. How do you link producer and consumer spans, and what breaks if you do not?
- Tool pick. Error rate flat, p99 up, CPU low. Ordered list of tools for the first fifteen minutes.
- Abuse case. Design authz for a force-sample header so employees can debug without letting bots melt Tempo.
Primary sources
Key takeaway
Debug distributed systems by following one golden signal to one correlated request to one leaf cause. Invest in propagation, exemplars, and guarded debug canaries so that path exists when you need it.