전체 비유: 택배 추적 시스템#

분산 추적을 택배 배송 추적에 비유하면 이해하기 쉽습니다:

택배 추적 비유분산 추적역할
운송장 번호Trace ID전체 요청의 고유 식별자
배송 구간별 스캔Span개별 작업 단위 기록
“물류센터 → 허브”Parent-Child Span구간 간 연결 관계
각 구간 소요 시간Duration작업 처리 시간
배송 상태 “집하/배송중/완료”Status성공/실패 상태
물류 센터 간 운송장 전달Context Propagation서비스 간 추적 정보 전달
지연 구간 표시Latency 분석병목 구간 식별
배송 사고 이력Error Span실패 지점 추적

이처럼 택배를 추적하듯, 분산 추적은 “요청이 어떤 서비스를 거쳐 어디서 지연됐는지"를 추적합니다.


대상 독자: 마이크로서비스를 운영하는 개발자, SRE 선수 지식: 관측성 3요소 소요 시간: 약 25-30분 이 문서를 읽으면: 분산 추적을 이해하고 서비스 간 요청 흐름을 분석할 수 있습니다

TL;DR
  • Trace: 하나의 요청 전체 경로 (여러 Span으로 구성)
  • Span: 단일 작업 단위 (시작/종료 시간, 메타데이터)
  • Context Propagation: 서비스 간 Trace ID 전달
  • 샘플링: 전체 트레이스 중 일부만 저장 (비용 최적화)

왜 분산 추적이 필요한가?#

마이크로서비스에서는 하나의 요청이 여러 서비스를 거칩니다. 어디서 지연이 발생했는지 파악하기 어렵습니다.

graph LR
    USER["사용자"] --> GW["API Gateway"]
    GW --> ORDER["주문 서비스"]
    ORDER --> PAYMENT["결제 서비스"]
    ORDER --> INVENTORY["재고 서비스"]
    PAYMENT --> DB1["결제 DB"]
    INVENTORY --> DB2["재고 DB"]

하나의 사용자 요청이 API Gateway를 거쳐 주문, 결제, 재고 서비스와 각 DB로 분산되는 마이크로서비스 구조입니다.

문제: 응답이 느린데 어디가 문제인지 모름

해결: 분산 추적으로 각 구간 소요 시간 확인


핵심 개념#

Trace와 Span#

graph TB
    subgraph "Trace (전체 요청)"
        S1["Span: API Gateway<br>0-250ms"]
        S2["Span: Order Service<br>10-200ms"]
        S3["Span: Payment Service<br>20-180ms"]
        S4["Span: Payment DB<br>30-150ms"]
    end

    S1 --> S2
    S2 --> S3
    S3 --> S4

하나의 Trace 안에서 API Gateway, Order, Payment, DB 순으로 Span이 중첩되며 각 구간의 시간 범위를 보여줍니다.

용어설명
Trace전체 요청 경로 (고유 Trace ID)
Span개별 작업 단위 (고유 Span ID)
Parent Span현재 Span을 호출한 상위 Span
Root Span첫 번째 Span (Parent 없음)

Span 구조#

{
  "traceId": "abc123def456",
  "spanId": "span001",
  "parentSpanId": null,
  "operationName": "HTTP GET /orders",
  "serviceName": "order-service",
  "startTime": 1704700800000,
  "duration": 245,
  "tags": {
    "http.method": "GET",
    "http.status_code": 200,
    "http.url": "/orders/123"
  },
  "logs": [
    {
      "timestamp": 1704700800100,
      "message": "Fetching order from database"
    }
  ]
}

Context Propagation#

서비스 간 Trace ID를 전달하는 방법입니다.

sequenceDiagram
    participant A as Service A
    participant B as Service B
    participant C as Service C

    A->>B: HTTP Request<br>traceparent: 00-abc123-span1-01
    Note over B: Extract context<br>Create child span
    B->>C: HTTP Request<br>traceparent: 00-abc123-span2-01
    Note over C: Extract context<br>Create child span

서비스 간 HTTP 요청 시 traceparent 헤더를 통해 Trace 컨텍스트가 전파되는 과정을 보여줍니다.

W3C Trace Context 형식:

traceparent: 00-{trace-id}-{span-id}-{flags}
traceparent: 00-abc123def456789-fedcba987654321-01

도구 비교#

도구특징적합한 경우
JaegerCNCF 프로젝트, UI 우수Kubernetes 환경
Zipkin가벼움, 쉬운 설치빠른 시작
TempoGrafana 통합, 저비용Grafana 사용 시
AWS X-RayAWS 통합AWS 환경

아키텍처 (Jaeger)#

graph TB
    APP["Application"] --> |"spans"| AGENT["Jaeger Agent"]
    AGENT --> COLLECTOR["Jaeger Collector"]
    COLLECTOR --> STORAGE["Storage<br>(Elasticsearch/Cassandra)"]
    STORAGE --> QUERY["Jaeger Query"]
    QUERY --> UI["Jaeger UI"]

Jaeger의 Application → Agent → Collector → Storage → Query → UI 데이터 흐름을 보여줍니다.


Spring Boot 설정#

의존성 추가#

// build.gradle.kts
dependencies {
    implementation("io.micrometer:micrometer-tracing-bridge-otel")
    implementation("io.opentelemetry:opentelemetry-exporter-otlp")
}

application.yml#

management:
  tracing:
    sampling:
      probability: 1.0  # 개발: 100%, 운영: 0.1 (10%)
  otlp:
    tracing:
      endpoint: http://jaeger:4318/v1/traces

logging:
  pattern:
    level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"

수동 Span 생성#

@Service
@RequiredArgsConstructor
public class OrderService {
    private final Tracer tracer;

    public Order processOrder(OrderRequest request) {
        Span span = tracer.nextSpan().name("processOrder").start();
        try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
            span.tag("order.type", request.getType());
            span.event("Processing started");

            Order order = createOrder(request);

            span.event("Order created");
            return order;
        } finally {
            span.end();
        }
    }
}

샘플링 전략#

모든 트레이스를 저장하면 비용이 급증합니다. 샘플링으로 비용을 최적화합니다.

샘플링 방식#

방식설명적합한 경우
확률 샘플링일정 비율만 수집일반적
Rate Limiting초당 N개만 수집트래픽 급증 시
Tail-based에러/느린 요청 우선문제 분석 중심

권장 샘플링률#

환경샘플링률이유
개발100%모든 요청 추적
스테이징50%충분한 데이터
운영1-10%비용 최적화

에러 시 100% 수집#

# OpenTelemetry Collector 설정
processors:
  tail_sampling:
    policies:
      - name: errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: slow
        type: latency
        latency:
          threshold_ms: 1000
      - name: probabilistic
        type: probabilistic
        probabilistic:
          sampling_percentage: 10

로그/메트릭 연결#

Trace ID로 연결#

graph LR
    METRIC["Metric Alert<br>에러율 급증"]
    LOG["Logs<br>trace_id로 검색"]
    TRACE["Trace<br>상세 흐름"]

    METRIC --> |"시간대 확인"| LOG
    LOG --> |"trace_id 추출"| TRACE

메트릭 알림에서 로그로, 로그에서 trace_id를 추출하여 트레이스로 연결하는 분석 흐름입니다.

로그에 Trace ID 포함#

// Spring Boot 자동 포함
// 로그 패턴: %X{traceId:-}

// 로그 출력 예시
2026-01-12 10:30:00 INFO [order-service,abc123def456,span001] Order created: 12345

Grafana에서 연결#

1. 대시보드에서 에러율 급증 확인
2. Explore → Loki로 이동
3. {service="order-service"} |= "ERROR" 검색
4. 로그에서 trace_id 클릭
5. Tempo/Jaeger로 전체 트레이스 확인

분석 패턴#

병목 구간 찾기#

Trace 분석:
├─ API Gateway (10ms) ✓
├─ Order Service (50ms) ✓
│   ├─ Validation (5ms) ✓
│   └─ Payment Call (2000ms) ← 병목!
│       └─ Payment DB (1800ms) ← 근본 원인
└─ Response (5ms) ✓

에러 추적#

Trace 분석:
├─ API Gateway (10ms) ✓
├─ Order Service (50ms) ✗ Error
│   ├─ Inventory Check (200ms)
│   └─ Error: "Insufficient stock"

서비스 의존성 맵#

Jaeger/Tempo에서 서비스 간 연결 시각화:

graph LR
    GW["API Gateway"] --> ORDER["Order"]
    GW --> USER["User"]
    ORDER --> PAYMENT["Payment"]
    ORDER --> INVENTORY["Inventory"]
    ORDER --> NOTIFICATION["Notification"]
    PAYMENT --> PAYMENT_DB["Payment DB"]
    INVENTORY --> INVENTORY_DB["Inventory DB"]

서비스 간 의존성을 시각화한 맵으로, API Gateway에서 각 서비스와 데이터베이스로의 연결 관계를 보여줍니다.


알림 규칙#

트레이스 기반 알림#

# Prometheus alerting rule (Tempo 연동)
groups:
  - name: tracing
    rules:
      - alert: HighTraceErrorRate
        expr: |
          sum(rate(traces_spanmetrics_calls_total{status_code="STATUS_CODE_ERROR"}[5m]))
          / sum(rate(traces_spanmetrics_calls_total[5m]))
          > 0.05
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High trace error rate"

      - alert: SlowSpans
        expr: |
          histogram_quantile(0.99, sum(rate(traces_spanmetrics_latency_bucket[5m])) by (le, service))
          > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "P99 span latency > 2s"

핵심 정리#

개념설명
Trace전체 요청 경로
Span개별 작업 단위
Context서비스 간 전달되는 추적 정보
Sampling비용 최적화를 위한 선별 수집

구현 체크리스트:

  • OpenTelemetry SDK 추가
  • 샘플링률 설정
  • 로그에 trace_id 포함
  • Jaeger/Tempo 배포
  • Grafana 연동

관련 문서#

다음 단계#

추천 순서문서배우는 것
1OpenTelemetry표준화된 계측
2풀스택 예제통합 실습