Active Nerds
Module #3 · expert Article

Debugging in a Highly Distributed Kubernetes Environment: Architecture & Implementation

In-depth architectural breakdown and operational implementation for Debugging in a Highly Distributed Kubernetes Environment: Architecture & Implementation.

40 min read+25 XPPublished 2026-07-06

Prerequisite knowledge: Prometheus, distributed tracing concepts, kubectl proficiency, network fundamentals


Question (Scenario-Based)

Your microservices platform has 200+ services across 3 clusters. Users report intermittent 5-second latency spikes affecting checkout. The issue is non-reproducible locally, affects only ~2% of requests, and disappears before you can investigate. Walk through your systematic debugging approach.


Quick Answer (30 sec revision)

Use distributed tracing (Jaeger/Tempo) to identify which service in the call chain introduces latency, correlate with pod-level metrics in Prometheus, check for noisy-neighbor CPU throttling with kubectl top, inspect network policies for connection delays, and use continuous profiling (Parca/Pyroscope) to catch intermittent CPU spikes. The 2% rate and 5-second spike pattern strongly suggests DNS resolution timeouts or TCP connection pool exhaustion.


Detailed Answer

️ Systematic Debugging Framework

Step 1: OBSERVE    → Gather signals without assumptions
Step 2: HYPOTHESIZE → Form specific, testable theories
Step 3: ISOLATE    → Narrow the blast radius
Step 4: CONFIRM    → Prove the root cause
Step 5: REMEDIATE  → Fix and verify
Step 6: HARDEN     → Prevent recurrence

The 2% + 5-second Pattern: What It Tells You

Before touching any tool, the symptoms themselves are diagnostic clues:

| Symptom | What It Suggests | |---|---| | 2% of requests | Not all pods affected — likely a specific instance or dependency | | 5-second spikes | Matches default DNS timeout (5s) or TCP connect timeout | | Intermittent | Race condition, connection pool exhaustion, or GC pause | | Checkout only | Specific service dependency chain, not cluster-wide | | Non-reproducible locally | Scale-dependent, network-dependent, or timing-dependent |

The 5-second number is a massive clue. The Linux kernel's default DNS resolution timeout is 5 seconds. This is the single most common cause of this exact symptom pattern in Kubernetes.


️ Step-by-Step Debugging

Step 1: Establish Observability Baseline

# Get a high-level view of what's unhealthy RIGHT NOW
kubectl get pods -A | grep -v Running | grep -v Completed | grep -v Succeeded

# Check resource pressure across nodes
kubectl top nodes
kubectl top pods -n checkout --sort-by=cpu

# Check for recent events (often the first clue)
kubectl get events -n checkout \
  --sort-by='.lastTimestamp' \
  --field-selector type=Warning | tail -30

# Check pod restarts (OOMKills show up here)
kubectl get pods -n checkout -o json | \
  jq '.items[] | {
    name: .metadata.name,
    restarts: .status.containerStatuses[0].restartCount,
    reason: .status.containerStatuses[0].lastState.terminated.reason
  }' | grep -A3 '"restarts": [^0]'

Step 2: Distributed Tracing — Find the Slow Span

# Assuming Jaeger is deployed (via Helm)
helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
helm install jaeger jaegertracing/jaeger \
  --namespace observability \
  --set provisionDataStore.cassandra=false \
  --set allInOne.enabled=true \
  --set storage.type=memory   # Use Elasticsearch/Cassandra for production
# Instrument your service with OpenTelemetry (Python example)
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor

# Configure tracer
provider = TracerProvider()
otlp_exporter = OTLPSpanExporter(
    endpoint="http://jaeger-collector.observability.svc.cluster.local:4317"
)
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
trace.set_tracer_provider(provider)

# Auto-instrument HTTP and DB calls
RequestsInstrumentor().instrument()
Psycopg2Instrumentor().instrument()

tracer = trace.get_tracer(__name__)

# Manual span for critical sections
def process_checkout(order_id: str):
    with tracer.start_as_current_span("process_checkout") as span:
        span.set_attribute("order.id", order_id)
        span.set_attribute("order.items", len(order.items))

        # This span will show if DNS resolution is slow
        with tracer.start_as_current_span("resolve_payment_service"):
            payment_url = resolve_service("payment-svc")

        with tracer.start_as_current_span("call_payment_gateway"):
            result = payment_client.charge(order)

        return result
# Query Jaeger for slow traces (using Jaeger API)
curl "http://jaeger.observability.svc.cluster.local:16686/api/traces?\
service=checkout-api&\
operation=process_checkout&\
minDuration=3000000&\
limit=20" | jq '.data[].spans[] | {
  operationName,
  duration: (.duration / 1000),
  tags: .tags
}' | sort -t: -k2 -rn | head -20

# Look for spans with duration > 4900ms (near the 5s timeout)
# The FIRST long span in the trace is your culprit

Step 3: DNS Investigation (Most Likely Culprit)

# Test DNS resolution speed from within a pod
kubectl run dns-test --image=tutum/dnsutils \
  --restart=Never --rm -it -- \
  bash -c "for i in {1..100}; do
    time nslookup payment-svc.production.svc.cluster.local;
  done 2>&1 | grep real"

# Check CoreDNS performance metrics
kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl top pods -n kube-system -l k8s-app=kube-dns

# Check CoreDNS logs for errors
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=100 | \
  grep -E "SERVFAIL|timeout|i/o timeout"

# Check CoreDNS ConfigMap for misconfiguration
kubectl get configmap coredns -n kube-system -o yaml
# The most common DNS fix: tune ndots and add search domains
# Default /etc/resolv.conf in pods has ndots:5, causing 5 DNS lookups
# before falling back to the absolute name

# Fix 1: Tune dnsConfig in your deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout-api
spec:
  template:
    spec:
      dnsConfig:
        options:
          - name: ndots
            value: "2"        # Reduce from default 5 to 2
                              # checkout-api.production → tries as FQDN first
          - name: single-request-reopen
            value: ""         # Fix for parallel A/AAAA query race condition
          - name: timeout
            value: "2"        # DNS timeout per attempt (default 5s)
          - name: attempts
            value: "3"        # Retry 3 times before failing
      dnsPolicy: ClusterFirst
# Fix 2: CoreDNS caching tuning
# Edit CoreDNS ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
data:
  Corefile: |
    .:53 {
        errors
        health {
           lameduck 5s
        }
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }
        prometheus :9153
        forward . /etc/resolv.conf {
           max_concurrent 1000
        }
        cache 30 {           # Cache DNS responses for 30 seconds
            success 9984     # Max successful cache entries
            denial 9984      # Max NXDOMAIN cache entries
            prefetch 10      # Prefetch popular entries before expiry
        }
        loop
        reload
        loadbalance
    }
# Fix 3: NodeLocal DNSCache (most impactful for high-traffic clusters)
# Runs a DNS cache DaemonSet on every node, eliminates conntrack issues
kubectl apply -f https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml

# NodeLocal DNSCache intercepts DNS queries at the node level
# Reduces latency from ~5ms to ~0.1ms for cached entries
# Eliminates the conntrack race condition that causes 5s timeouts

Step 4: CPU Throttling Investigation

# Check for CPU throttling (a hidden cause of latency spikes)
# Throttling happens when a container hits its CPU LIMIT, not just request

# Query Prometheus for CPU throttling rate
curl -s "http://prometheus.monitoring.svc.cluster.local:9090/api/v1/query" \
  --data-urlencode 'query=
    rate(container_cpu_cfs_throttled_seconds_total{
      namespace="checkout",
      container="checkout-api"
    }[5m])
    /
    rate(container_cpu_cfs_periods_total{
      namespace="checkout",
      container="checkout-api"
    }[5m]) * 100' | \
  jq '.data.result[] | {pod: .metric.pod, throttle_pct: .value[1]}'

# If throttle_pct > 25%, you have a CPU limit problem
# Solution: Increase CPU limit or remove it (controversial but effective)
# Better solution: Profile the CPU usage and optimize the hot path

Step 5: Network Policy and Connection Pool Investigation

# Check for connection refused / reset errors
kubectl exec -n checkout deploy/checkout-api -- \
  netstat -an | grep -E "CLOSE_WAIT|TIME_WAIT" | wc -l
# High CLOSE_WAIT count = connection pool not releasing connections

# Check network policy isn't dropping packets
kubectl get networkpolicy -n checkout -o yaml

# Use tcpdump to capture actual traffic (requires privileged pod)
kubectl debug node/worker-node-01 -it \
  --image=nicolaka/netshoot -- \
  tcpdump -i eth0 -w /tmp/capture.pcap \
  'host payment-svc and (tcp-rst or tcp-fin)'

# Copy capture for analysis
kubectl cp debug-pod:/tmp/capture.pcap ./capture.pcap
wireshark capture.pcap   # Analyze TCP resets, retransmissions

Step 6: Continuous Profiling with Parca

# Install Parca (continuous profiling)
helm repo add parca https://parca-dev.github.io/parca
helm install parca parca/parca \
  --namespace observability \
  --set parca-agent.enabled=true   # eBPF-based, no code changes needed

# Parca Agent runs as DaemonSet and profiles ALL processes
# No instrumentation required — uses eBPF

# Access Parca UI
kubectl port-forward -n observability svc/parca 7070:7070

# In the UI:
# 1. Select your checkout-api process
# 2. Set time range to when latency spikes occurred
# 3. Look for flame graph hotspots that appear only during spikes
# 4. Compare "normal" vs "spike" profiles to find the delta

Step 7: Correlation Dashboard — Tying It All Together

# Grafana dashboard panels to correlate symptoms
# Panel 1: Request latency P99 (5m window)
# Query: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{service="checkout-api"}[5m]))

# Panel 2: DNS resolution time
# Query: histogram_quantile(0.99, rate(coredns_dns_request_duration_seconds_bucket[5m]))

# Panel 3: CPU throttling rate
# Query: rate(container_cpu_cfs_throttled_seconds_total{container="checkout-api"}[5m]) / rate(container_cpu_cfs_periods_total{container="checkout-api"}[5m])

# Panel 4: Connection pool saturation
# Query: db_connection_pool_size{service="checkout-api"} - db_connection_pool_available{service="checkout-api"}

# Panel 5: Error rate by pod (to identify specific bad pods)
# Query: rate(http_requests_total{service="checkout-api",status=~"5.."}[5m]) by (pod)

# Correlate all 5 panels on same time axis
# The panel that spikes FIRST (leading indicator) is your root cause

Step 8: kubectl-debug and Ephemeral Containers

# Attach a debug container to a running pod WITHOUT restarting it
# (Kubernetes 1.23+ feature)
kubectl debug -it checkout-api-7d9f8b-xk2pq \
  --image=nicolaka/netshoot \
  --target=checkout-api \
  --namespace=checkout

# Inside the debug container, you share the pod's network namespace
# Run network diagnostics
curl -v http://payment-svc.production.svc.cluster.local/health
dig payment-svc.production.svc.cluster.local
ss -tulpn     # Show listening sockets
strace -p 1   # Trace system calls of PID 1 (the main process)

# Profile memory allocations in real time
# (If the target container has perf installed)
perf top -p $(pgrep checkout-api)

️ Multiple Perspectives on the Root Cause

| Hypothesis | Evidence Pattern | Diagnostic Tool | Fix | |---|---|---|---| | DNS timeout | Exactly 5s spike, 2% rate | time nslookup in pod | NodeLocal DNSCache, ndots tuning | | CPU throttling | Spikes correlate with traffic peaks | Prometheus throttle metric | Increase CPU limit, optimize code | | Connection pool exhaustion | High CLOSE_WAIT, DB errors | netstat, pool metrics | Increase pool size, add backoff | | GC pause (JVM/Go) | Spikes every N minutes | Continuous profiling | Tune GC, reduce heap pressure | | Noisy neighbor | Spikes on specific nodes | kubectl top nodes | Pod anti-affinity, node taints | | Network policy drop | Specific pod pairs affected | tcpdump, policy review | Fix NetworkPolicy rules |


️ Common Mistakes & Misconceptions

  • "I'll add more logging to debug this." — Logging adds latency and can mask the very problem you're debugging. Use tracing and profiling instead.
  • "The issue doesn't exist in staging, so it's a code bug." — Distributed system issues are often emergent at scale. DNS conntrack races, for example, only manifest under high concurrency.
  • "I'll restart the pod to fix it." — Restarting destroys the evidence. Always capture diagnostics (thread dumps, heap dumps, network captures) before restarting.
  • "CPU usage looks normal, so CPU isn't the issue." — CPU usage and CPU throttling are different metrics. A container can be throttled even at 50% usage if it hits its limit in a short burst window.

Key Takeaway

Debugging distributed Kubernetes issues requires correlating signals across multiple observability layers — traces show you where, metrics show you when and how often, and profiling shows you why. The 5-second latency spike pattern is almost always DNS-related in Kubernetes. NodeLocal DNSCache is the single most impactful fix for DNS-related latency in high-traffic clusters, eliminating both the 5-second timeout and the conntrack race condition simultaneously.


Self-Assessment Checklist

  • [ ] Can you explain why the default ndots:5 setting causes extra DNS lookups?
  • [ ] Can you describe the difference between CPU usage and CPU throttling?
  • [ ] Can you explain what NodeLocal DNSCache does and why it's better than CoreDNS alone?
  • [ ] Can you use ephemeral containers to debug a running pod without restarting it?
  • [ ] Can you describe how to use distributed tracing to find the slow span in a call chain?

Finished reading?

Mark as complete to claim your +25 XP and track progress.