Workspace IndexAlgorithms › Day 48

Distributed Tracing and Sampling Strategy TODO

Algorithms · Day 48 / 100 · C. Concurrency & Performance Engineering (Day 36-51)

Concept

Distributed tracing is an observability technique that groups the path a single request takes across multiple services into a unit called a trace, recording each unit of work as a span with a parent-child relationship. For spans from different processes to be grouped into the same trace, the trace ID and parent span ID must be propagated through request headers, and this propagation convention has to be standardized for heterogeneous systems to link up. Storing every request is too costly, so sampling is necessary: head-based sampling makes a probabilistic decision at the start of a trace and propagates that decision downstream so the trace doesn't get fragmented. Tail-based sampling waits until the trace finishes and looks at the whole thing before deciding, so it can selectively keep error or slow requests — but it requires buffering until completion, which costs memory and structural complexity. Either way, the sampling rate has to be recorded alongside the data so aggregate metrics can be reconstructed without bias.

Failures tend to happen in the tail rather than the average, and pure random sampling alone means the slow and failed request traces you actually need for post-mortem analysis end up missing.

Code & Formula

# 분산 트레이싱과 샘플링 전략 — trace_id 전파로 스팬을 하나의 트레이스로 묶고,
# 헤드 기반 샘플링(시작 시 결정)과 테일 기반 샘플링(완료 후 에러/느린 트레이스만 선별)을 비교한다.

import random
import uuid

random.seed(3)

class Span:
    def __init__(self, trace_id, name, duration_ms, error=False, parent=None):
        self.trace_id = trace_id
        self.span_id = uuid.uuid4().hex[:8]
        self.parent = parent.span_id if parent else None
        self.name = name
        self.duration_ms = duration_ms
        self.error = error

def make_trace(slow=False, error=False):
    trace_id = uuid.uuid4().hex[:8]
    root = Span(trace_id, "api.handle", random.uniform(5, 15))
    child = Span(trace_id, "match.execute", random.uniform(3, 8), parent=root)
    tail = Span(trace_id, "db.settle", 200 if slow else random.uniform(2, 6),
                error=error, parent=child)
    return [root, child, tail]

def head_sample_decision(trace_id, rate=0.1):
    """트레이스 시작 시점에 확률적으로 결정하고, 이 결정을 모든 자식 스팬에 전파한다."""
    return (int(trace_id, 16) % 1000) < rate * 1000

def tail_sample_decision(spans, latency_threshold_ms=100):
    """트레이스가 끝난 뒤 전체를 보고, 에러거나 느리면 남긴다."""
    total = sum(s.duration_ms for s in spans)
    has_error = any(s.error for s in spans)
    return has_error or total > latency_threshold_ms

traces = (
    [make_trace() for _ in range(20)]
    + [make_trace(slow=True) for _ in range(2)]
    + [make_trace(error=True) for _ in range(2)]
)

head_kept = sum(1 for t in traces if head_sample_decision(t[0].trace_id))
tail_kept_important = sum(
    1 for t in traces
    if tail_sample_decision(t) and (any(s.error for s in t) or sum(s.duration_ms for s in t) > 100)
)
important_total = sum(1 for t in traces if any(s.error for s in t) or sum(s.duration_ms for s in t) > 100)
head_kept_important = sum(
    1 for t in traces
    if head_sample_decision(t[0].trace_id) and (any(s.error for s in t) or sum(s.duration_ms for s in t) > 100)
)

print(f"전체 트레이스: {len(traces)}, 그중 중요한(에러/느림) 트레이스: {important_total}")
print(f"헤드 샘플링(10%)으로 보존된 트레이스: {head_kept}, 그중 중요한 것: {head_kept_important}")
print(f"테일 샘플링으로 보존된 중요한 트레이스: {tail_kept_important} / {important_total}")
print("-> 테일 샘플링은 저장량을 줄이면서도 중요한 트레이스를 놓치지 않는다.")

Exercise

Set up two or more services, propagate trace context with OpenTelemetry, and check directly — with the head sampling rate turned down — whether a given trace cuts off partway through.

Practical Connection

The flow of a user order from the API through the matching engine to transaction submission and receipt confirmation has long, asynchronous spans because of chain confirmation latency, so recording the transaction hash as a span attribute and preserving failed traces via tail sampling is critical for incident analysis.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

분산 트레이싱과 샘플링 전략 TODO

Algorithms · Day 48 / 100 · C. 동시성·성능 엔지니어링 (Day 36–51)

개념

분산 트레이싱은 하나의 요청이 여러 서비스를 지나가는 경로를 트레이스라는 단위로 묶고, 각 작업 구간을 부모-자식 관계를 가진 스팬으로 기록하는 관측 기법이다. 서로 다른 프로세스에서 같은 트레이스로 묶이려면 트레이스 식별자와 부모 스팬 식별자를 요청 헤더에 실어 전파해야 하며, 이 전파 규약이 표준화되어 있어야 이종 시스템이 이어진다. 모든 요청을 저장하면 비용이 감당되지 않으므로 샘플링이 필요한데, 헤드 기반 샘플링은 트레이스 시작 시점에 확률적으로 결정을 내리고 그 결정을 하위로 전파해 트레이스가 조각나지 않게 한다. 테일 기반 샘플링은 트레이스가 끝난 뒤 전체를 보고 판단하므로 에러나 느린 요청을 선별해 남길 수 있지만, 완성될 때까지 버퍼링해야 해서 메모리와 구조적 복잡도를 요구한다. 어느 쪽이든 샘플링 비율을 함께 기록해야 집계 지표를 편향 없이 복원할 수 있다.

장애는 대개 평균이 아니라 꼬리에서 발생하는데, 순수 확률 샘플링만 쓰면 정작 필요한 느린 요청과 실패 요청의 트레이스가 남지 않아 사후 분석이 불가능해진다.

코드 · 수식

# 분산 트레이싱과 샘플링 전략 — trace_id 전파로 스팬을 하나의 트레이스로 묶고,
# 헤드 기반 샘플링(시작 시 결정)과 테일 기반 샘플링(완료 후 에러/느린 트레이스만 선별)을 비교한다.

import random
import uuid

random.seed(3)

class Span:
    def __init__(self, trace_id, name, duration_ms, error=False, parent=None):
        self.trace_id = trace_id
        self.span_id = uuid.uuid4().hex[:8]
        self.parent = parent.span_id if parent else None
        self.name = name
        self.duration_ms = duration_ms
        self.error = error

def make_trace(slow=False, error=False):
    trace_id = uuid.uuid4().hex[:8]
    root = Span(trace_id, "api.handle", random.uniform(5, 15))
    child = Span(trace_id, "match.execute", random.uniform(3, 8), parent=root)
    tail = Span(trace_id, "db.settle", 200 if slow else random.uniform(2, 6),
                error=error, parent=child)
    return [root, child, tail]

def head_sample_decision(trace_id, rate=0.1):
    """트레이스 시작 시점에 확률적으로 결정하고, 이 결정을 모든 자식 스팬에 전파한다."""
    return (int(trace_id, 16) % 1000) < rate * 1000

def tail_sample_decision(spans, latency_threshold_ms=100):
    """트레이스가 끝난 뒤 전체를 보고, 에러거나 느리면 남긴다."""
    total = sum(s.duration_ms for s in spans)
    has_error = any(s.error for s in spans)
    return has_error or total > latency_threshold_ms

traces = (
    [make_trace() for _ in range(20)]
    + [make_trace(slow=True) for _ in range(2)]
    + [make_trace(error=True) for _ in range(2)]
)

head_kept = sum(1 for t in traces if head_sample_decision(t[0].trace_id))
tail_kept_important = sum(
    1 for t in traces
    if tail_sample_decision(t) and (any(s.error for s in t) or sum(s.duration_ms for s in t) > 100)
)
important_total = sum(1 for t in traces if any(s.error for s in t) or sum(s.duration_ms for s in t) > 100)
head_kept_important = sum(
    1 for t in traces
    if head_sample_decision(t[0].trace_id) and (any(s.error for s in t) or sum(s.duration_ms for s in t) > 100)
)

print(f"전체 트레이스: {len(traces)}, 그중 중요한(에러/느림) 트레이스: {important_total}")
print(f"헤드 샘플링(10%)으로 보존된 트레이스: {head_kept}, 그중 중요한 것: {head_kept_important}")
print(f"테일 샘플링으로 보존된 중요한 트레이스: {tail_kept_important} / {important_total}")
print("-> 테일 샘플링은 저장량을 줄이면서도 중요한 트레이스를 놓치지 않는다.")

연습

두 개 이상의 서비스를 두고 OpenTelemetry로 트레이스 컨텍스트를 전파해 보고, 헤드 샘플링 비율을 낮춘 상태에서 특정 트레이스가 중간부터 끊기는지 여부를 직접 확인하라.

실무 · Verex 연결

사용자 주문이 API에서 매칭 엔진을 거쳐 트랜잭션 제출과 영수증 확인까지 이어지는 흐름은 체인 확정 지연 때문에 스팬이 길고 비동기라, 트랜잭션 해시를 스팬 속성으로 남기고 실패 트레이스를 테일 샘플링으로 보존하는 설계가 사고 분석에 결정적이다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 47. eBPF로 프로덕션 관측49. 용량 계획·SLO와 에러 예산 →