본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 3장: 분산 추적(Distributed Tracing)
2026년 2월 12일·인프라·

3장: 분산 추적(Distributed Tracing)

스팬의 내부 구조와 종류, 부모-자식 관계, 샘플링 전략(Head/Tail/Rate)을 학습하고 Python으로 분산 추적을 직접 구현합니다.

15분858자8개 섹션
monitoringobservability
공유
opentelemetry3 / 11
1234567891011
이전2장: OpenTelemetry 아키텍처 심층 분석다음4장: 메트릭(Metrics) 수집과 분석

학습 목표

  • 스팬(Span)의 내부 구조와 필드를 이해합니다
  • 부모-자식 관계로 구성되는 트레이스의 구조를 파악합니다
  • 스팬 종류(SpanKind)에 따른 역할 차이를 학습합니다
  • Head Sampling, Tail Sampling, Rate Limiting 전략을 비교합니다
  • Python SDK를 활용한 분산 추적 실습을 수행합니다

트레이스와 스팬 — 기본 개념

트레이스(Trace)란

**트레이스(Trace)**는 분산 시스템에서 하나의 요청이 여러 서비스를 거치는 전체 경로를 나타내는 방향성 비순환 그래프(DAG)입니다. 각 트레이스는 고유한 128비트 Trace ID로 식별되며, 이 ID가 서비스 간 전파되어 하나의 요청을 추적할 수 있게 합니다.

스팬(Span)의 내부 구조

**스팬(Span)**은 트레이스를 구성하는 개별 작업 단위입니다. 하나의 스팬은 다음 필드를 포함합니다.

필드설명
trace_id128비트 트레이스 식별자
span_id64비트 스팬 식별자
parent_span_id부모 스팬의 ID (루트 스팬은 비어 있음)
name스팬 이름 (작업 설명)
kind스팬 종류 (Client, Server, Internal 등)
start_time시작 시각 (나노초 정밀도)
end_time종료 시각
status상태 (Unset, Ok, Error)
attributes키-값 쌍의 속성
events시간 기록이 있는 이벤트 (예외, 로그 등)
links다른 스팬과의 인과 관계
span_structure.py
python
from opentelemetry import trace
 
tracer = trace.get_tracer("order-service", "1.0.0")
 
with tracer.start_as_current_span("process-order") as span:
    # 속성 추가
    span.set_attribute("order.id", "ORD-2026-001")
    span.set_attribute("order.total", 45000)
    span.set_attribute("customer.tier", "premium")
    
    # 이벤트 추가 (특정 시점의 기록)
    span.add_event("payment-validated", {
        "payment.method": "credit-card",
        "payment.amount": 45000,
    })
    
    try:
        process_payment()
    except Exception as e:
        # 에러 상태 기록
        span.set_status(trace.StatusCode.ERROR, str(e))
        span.record_exception(e)
        raise

부모-자식 관계와 트레이스 구조

스팬은 부모-자식 관계를 형성하여 트레이스의 계층 구조를 만듭니다. 최상위 스팬을 **루트 스팬(Root Span)**이라 하며, 전체 요청의 시작과 끝을 나타냅니다.

위 다이어그램에서 각 막대가 하나의 스팬이며, 들여쓰기가 부모-자식 관계를 나타냅니다. "GET /orders" 스팬이 루트이고, "process-order"가 자식, "validate-payment"과 "SELECT orders"가 손자 스팬입니다.

스팬 컨텍스트(SpanContext)

SpanContext는 스팬의 불변 식별 정보를 담는 객체로, 서비스 간 전파되는 핵심 데이터입니다.

span_context.py
python
from opentelemetry import trace
 
# 현재 활성 스팬의 컨텍스트 가져오기
current_span = trace.get_current_span()
ctx = current_span.get_span_context()
 
print(f"Trace ID: {format(ctx.trace_id, '032x')}")
print(f"Span ID: {format(ctx.span_id, '016x')}")
print(f"Trace Flags: {ctx.trace_flags}")
print(f"Is Remote: {ctx.is_remote}")

스팬 링크(Span Links)

일반적인 부모-자식 관계로 표현할 수 없는 인과 관계에는 **스팬 링크(Span Links)**를 사용합니다. 대표적인 예시는 메시지 큐를 통한 비동기 처리입니다.

span_links.py
python
from opentelemetry import trace
 
tracer = trace.get_tracer("consumer-service")
 
# 메시지 큐에서 받은 여러 메시지의 원본 스팬과 링크 생성
links = [
    trace.Link(
        context=producer_span_context_1,
        attributes={"messaging.message.id": "msg-001"}
    ),
    trace.Link(
        context=producer_span_context_2,
        attributes={"messaging.message.id": "msg-002"}
    ),
]
 
# 배치 처리 스팬에 링크 추가
with tracer.start_as_current_span("process-batch", links=links) as span:
    span.set_attribute("messaging.batch.message_count", 2)
    process_messages()

스팬 종류(SpanKind)

OpenTelemetry는 다섯 가지 스팬 종류를 정의하여 스팬의 역할을 명확히 합니다.

SpanKind설명예시
CLIENT원격 서비스에 요청을 보내는 측HTTP 클라이언트, gRPC 클라이언트
SERVER원격 요청을 받아 처리하는 측HTTP 서버 핸들러, gRPC 서버
INTERNAL프로세스 내부 작업비즈니스 로직, 데이터 변환
PRODUCER비동기 메시지를 보내는 측Kafka 프로듀서, RabbitMQ 퍼블리셔
CONSUMER비동기 메시지를 받는 측Kafka 컨슈머, 큐 워커
span_kinds.py
python
from opentelemetry.trace import SpanKind
 
# SERVER 스팬 -- 들어오는 HTTP 요청 처리
with tracer.start_as_current_span("GET /api/orders", kind=SpanKind.SERVER):
    
    # INTERNAL 스팬 -- 비즈니스 로직
    with tracer.start_as_current_span("validate-order", kind=SpanKind.INTERNAL):
        validate()
    
    # CLIENT 스팬 -- 외부 서비스 호출
    with tracer.start_as_current_span("payment-api-call", kind=SpanKind.CLIENT):
        call_payment_service()
    
    # PRODUCER 스팬 -- 메시지 발행
    with tracer.start_as_current_span("send-notification", kind=SpanKind.PRODUCER):
        publish_to_queue()
Info

SpanKind는 백엔드 UI에서 트레이스를 시각화할 때 중요한 역할을 합니다. Jaeger나 Tempo는 CLIENT-SERVER 쌍을 인식하여 서비스 간 호출 관계를 자동으로 매핑합니다. 올바른 SpanKind 설정은 서비스 토폴로지 시각화의 정확성을 좌우합니다.


샘플링 전략

프로덕션 환경에서 모든 요청의 스팬을 100% 수집하면 저장 비용과 네트워크 부하가 급격히 증가합니다. **샘플링(Sampling)**은 수집할 트레이스를 선별하여 비용과 관측 가능성 사이의 균형을 맞추는 전략입니다.

Head Sampling — 시작 시점에 결정

Head Sampling은 트레이스가 시작되는 시점에 수집 여부를 결정합니다. 루트 스팬이 생성될 때 확률적으로 샘플링 여부가 정해지며, 이 결정이 모든 다운스트림 서비스로 전파됩니다.

head_sampling.py
python
from opentelemetry.sdk.trace.sampling import (
    TraceIdRatioBased,
    ParentBased,
)
 
# 10% 확률로 샘플링 (새로운 트레이스에만 적용)
sampler = ParentBased(
    root=TraceIdRatioBased(0.1),  # 루트 스팬: 10% 샘플링
    # 부모가 샘플링된 경우 자식도 항상 샘플링
)

장점: 구현이 간단하고 SDK 수준에서 동작하여 오버헤드가 최소입니다. 단점: 에러가 발생한 트레이스가 샘플링에서 제외될 수 있습니다. 중요한 트레이스를 놓칠 위험이 있습니다.

Tail Sampling — 완료 후 결정

Tail Sampling은 트레이스가 완료된 후 전체 스팬 데이터를 분석하여 보존 여부를 결정합니다. 에러가 발생했거나 지연 시간이 긴 트레이스만 선별적으로 보존할 수 있습니다.

collector-tail-sampling.yaml
yaml
processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 100000
    policies:
      # 에러가 있는 트레이스는 100% 보존
      - name: errors-policy
        type: status_code
        status_code:
          status_codes: [ERROR]
      
      # 지연 시간이 500ms 이상인 트레이스 보존
      - name: latency-policy
        type: latency
        latency:
          threshold_ms: 500
      
      # 나머지는 5% 확률 샘플링
      - name: probabilistic-policy
        type: probabilistic
        probabilistic:
          sampling_percentage: 5
Warning

Tail Sampling은 반드시 OTel Collector에서 수행해야 하며, Gateway 모드로 배포된 단일 Collector가 특정 트레이스의 모든 스팬을 수신해야 합니다. 여러 Collector 인스턴스에 스팬이 분산되면 불완전한 트레이스로 잘못된 샘플링 결정이 내려질 수 있습니다. load_balancing 익스포터를 사용하여 Trace ID 기반 라우팅을 구성하세요.

장점: 중요한 트레이스를 확실히 보존합니다. 비용 효율적입니다. 단점: Collector에 메모리 부담이 큽니다. 모든 스팬을 일시적으로 버퍼링해야 합니다.

샘플링 전략 비교

전략결정 시점실행 위치비용정확도
Head Sampling트레이스 시작 시SDK낮음낮음
Tail Sampling트레이스 완료 후Collector높음높음
Rate Limiting초당 처리량 기준SDK/Collector낮음중간

Python 계측 실습

Flask 애플리케이션에 OpenTelemetry 분산 추적을 적용하는 실습을 진행합니다.

의존성 설치

terminal
bash
pip install opentelemetry-api \
    opentelemetry-sdk \
    opentelemetry-exporter-otlp-proto-grpc \
    opentelemetry-instrumentation-flask \
    opentelemetry-instrumentation-requests \
    flask requests

자동 계측 + 수동 스팬 추가

app.py
python
from flask import Flask, jsonify
import requests
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.sdk.resources import Resource
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
 
# SDK 초기화
resource = Resource.create({
    "service.name": "order-service",
    "service.version": "1.0.0",
    "deployment.environment": "development",
})
 
provider = TracerProvider(resource=resource)
provider.add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317"))
)
trace.set_tracer_provider(provider)
 
# 자동 계측 활성화
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()
 
# 수동 계측을 위한 트레이서
tracer = trace.get_tracer("order-service.handlers", "1.0.0")
 
 
@app.route("/orders/<order_id>")
def get_order(order_id):
    # 자동 계측: Flask가 SERVER 스팬을 생성
    
    # 수동 계측: 비즈니스 로직에 커스텀 스팬 추가
    with tracer.start_as_current_span("validate-order") as span:
        span.set_attribute("order.id", order_id)
        validate_order(order_id)
    
    with tracer.start_as_current_span("fetch-order-details") as span:
        span.set_attribute("order.id", order_id)
        # 자동 계측: requests가 CLIENT 스팬을 생성
        details = requests.get(
            f"http://inventory-service:8081/items?order={order_id}"
        )
        span.set_attribute("inventory.item_count", len(details.json()))
    
    return jsonify({"order_id": order_id, "status": "confirmed"})
 
 
def validate_order(order_id):
    with tracer.start_as_current_span("check-inventory") as span:
        span.set_attribute("order.id", order_id)
        # 재고 확인 로직
        span.add_event("inventory-checked", {"available": True})
 
 
if __name__ == "__main__":
    app.run(port=8080)

Docker Compose로 실행 환경 구성

docker-compose.yaml
yaml
services:
  order-service:
    build: .
    ports:
      - "8080:8080"
    environment:
      - OTEL_SERVICE_NAME=order-service
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317
    depends_on:
      - collector
 
  collector:
    image: otel/opentelemetry-collector-contrib:0.100.0
    volumes:
      - ./otel-collector-config.yaml:/etc/otelcol/config.yaml
    ports:
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
    depends_on:
      - jaeger
 
  jaeger:
    image: jaegertracing/all-in-one:1.55
    ports:
      - "16686:16686"  # Jaeger UI
      - "4317"         # OTLP gRPC (Jaeger 네이티브)
otel-collector-config.yaml
yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: "0.0.0.0:4317"
 
processors:
  batch:
    timeout: 1s
    send_batch_size: 1024
 
exporters:
  otlp/jaeger:
    endpoint: "jaeger:4317"
    tls:
      insecure: true
 
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/jaeger]

트레이스 분석 팁

트레이스를 효과적으로 활용하기 위한 실무 팁을 정리합니다.

스팬 이름 규칙

스팬 이름은 카디널리티가 낮아야 합니다. 즉, 고유한 값(ID, 파라미터)을 이름에 포함하지 않습니다.

span_naming.py
python
# 나쁜 예 -- 카디널리티가 높아 집계 불가
tracer.start_as_current_span(f"GET /users/{user_id}")  # user_id마다 다른 스팬 이름
 
# 좋은 예 -- 패턴화된 이름 + 속성으로 상세 정보 분리
with tracer.start_as_current_span("GET /users/:id") as span:
    span.set_attribute("user.id", user_id)

에러 처리 패턴

error_handling.py
python
with tracer.start_as_current_span("risky-operation") as span:
    try:
        result = perform_operation()
        span.set_status(trace.StatusCode.OK)
    except ValueError as e:
        # 비즈니스 에러 -- 경고 수준
        span.set_status(trace.StatusCode.ERROR, f"Validation failed: {e}")
        span.record_exception(e)
        return error_response(400, str(e))
    except Exception as e:
        # 시스템 에러 -- 심각
        span.set_status(trace.StatusCode.ERROR, f"Internal error: {e}")
        span.record_exception(e, attributes={
            "exception.escaped": True,
        })
        raise
Tip

record_exception 메서드는 스팬에 예외 이벤트를 자동 추가합니다. 예외 타입, 메시지, 스택 트레이스가 모두 기록되므로 별도의 로깅 없이도 충분한 디버깅 정보를 얻을 수 있습니다.


정리

이번 장에서는 분산 추적의 핵심 구성 요소인 트레이스와 스팬을 심층적으로 살펴보았습니다. 스팬의 내부 구조, 부모-자식 관계, 다섯 가지 SpanKind의 역할을 학습했으며, Head Sampling과 Tail Sampling의 트레이드오프를 비교했습니다. Python Flask 애플리케이션에 자동 계측과 수동 계측을 적용하는 실습도 진행했습니다.

다음 장에서는 3대 신호의 두 번째인 메트릭(Metrics)을 다룹니다. Counter, Gauge, Histogram 등 메트릭 종류와 카디널리티 관리, 그리고 메트릭에서 트레이스로 연결하는 Exemplars를 학습합니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#monitoring#observability

관련 글

인프라

4장: 메트릭(Metrics) 수집과 분석

OpenTelemetry 메트릭의 종류(Counter, Gauge, Histogram), 카디널리티 관리, Exemplars를 통한 메트릭-트레이스 연결, Prometheus 호환을 학습합니다.

2026년 2월 14일·14분
인프라

2장: OpenTelemetry 아키텍처 심층 분석

OpenTelemetry의 API/SDK/Collector 3계층 구조, W3C TraceContext 기반 컨텍스트 전파, 리소스와 시맨틱 컨벤션, 배포 패턴을 심층적으로 분석합니다.

2026년 2월 10일·16분
인프라

5장: 로그 통합과 상관관계

OpenTelemetry 로그 데이터 모델, 로그-트레이스 상관관계, 기존 로거 브릿지(Python logging, Go slog), 구조화 로그와 로그 레벨 전략을 학습합니다.

2026년 2월 16일·14분
이전 글2장: OpenTelemetry 아키텍처 심층 분석
다음 글4장: 메트릭(Metrics) 수집과 분석

댓글

목차

약 15분 남음
  • 학습 목표
  • 트레이스와 스팬 — 기본 개념
    • 트레이스(Trace)란
    • 스팬(Span)의 내부 구조
  • 부모-자식 관계와 트레이스 구조
    • 스팬 컨텍스트(SpanContext)
    • 스팬 링크(Span Links)
  • 스팬 종류(SpanKind)
  • 샘플링 전략
    • Head Sampling — 시작 시점에 결정
    • Tail Sampling — 완료 후 결정
    • 샘플링 전략 비교
  • Python 계측 실습
    • 의존성 설치
    • 자동 계측 + 수동 스팬 추가
    • Docker Compose로 실행 환경 구성
  • 트레이스 분석 팁
    • 스팬 이름 규칙
    • 에러 처리 패턴
  • 정리