이코에코(Eco²) Knowledge Base/Applied

Distributed Tracing: Trace, Span으로 분산 시스템 추적하기

mango_fr 2025. 12. 25. 22:22

참고: OpenTelemetry Documentation
참고: W3C Trace Context


들어가며

분산 시스템에서 하나의 요청이 여러 서비스를 거쳐 처리된다.

문제가 발생했을 때 "어디서 느려졌는지", "어디서 에러가 났는지" 찾기가 매우 어렵다.

┌─────────────────────────────────────────────────────────────┐
│               분산 시스템 디버깅의 어려움                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  사용자 요청: "스캔 결과가 안 와요"                         │
│                                                             │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐               │
│  │ API GW   │──▶│ Scan API │──▶│ RabbitMQ │               │
│  └──────────┘   └──────────┘   └────┬─────┘               │
│                                      │                      │
│                                      ▼                      │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐               │
│  │  Kafka   │◀──│ Celery   │──▶│Vision API│               │
│  └────┬─────┘   │ Worker   │   └──────────┘               │
│       │         └──────────┘                                │
│       ▼                                                     │
│  ┌──────────┐   ┌──────────┐                               │
│  │Character │──▶│   My     │                               │
│  │ Consumer │   │ Consumer │                               │
│  └──────────┘   └──────────┘                               │
│                                                             │
│  문제:                                                      │
│  • 로그가 각 서비스에 흩어져 있음                          │
│  • 어떤 요청이 어디까지 갔는지 모름                        │
│  • 병목이 어디인지 찾기 어려움                             │
│  • "이 에러가 어떤 요청 때문인지?" 연결 불가               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Distributed Tracing은 이 문제를 해결한다. 요청에 고유 ID를 부여하고, 모든 서비스가 이 ID를 전파하여 전체 흐름을 하나의 타임라인으로 시각화한다.


핵심 개념

Trace, Span, Context

┌─────────────────────────────────────────────────────────────┐
│                Trace, Span, Context                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Trace: 하나의 요청 전체 흐름                               │
│  ─────────────────────────────                              │
│  trace_id: abc-123                                          │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                                                     │   │
│  │  Span A: API Gateway (parent=none)                  │   │
│  │  ├─────────────────────────────────────────────┤   │   │
│  │  │ 0ms                                    100ms│   │   │
│  │                                                     │   │
│  │    Span B: Scan API (parent=A)                      │   │
│  │    ├───────────────────────────────────┤           │   │
│  │    │ 10ms                          90ms│           │   │
│  │                                                     │   │
│  │      Span C: RabbitMQ Publish (parent=B)            │   │
│  │      ├─────────┤                                   │   │
│  │      │20ms 30ms│                                   │   │
│  │                                                     │   │
│  │      Span D: Celery Task (parent=B)                 │   │
│  │      ├─────────────────────────────┤               │   │
│  │      │ 30ms                    80ms│               │   │
│  │                                                     │   │
│  │        Span E: Vision API (parent=D)                │   │
│  │        ├───────────────┤                           │   │
│  │        │ 35ms      60ms│                           │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Span: 하나의 작업 단위                                     │
│  • span_id: 고유 식별자                                    │
│  • parent_span_id: 부모 Span                               │
│  • operation_name: 작업 이름                               │
│  • start_time, end_time: 시작/종료 시각                    │
│  • attributes: 추가 정보 (user_id, task_id 등)            │
│  • events: Span 내 이벤트 (로그)                           │
│  • status: OK, ERROR                                       │
│                                                             │
│  Context: trace_id + span_id + 전파 정보                   │
│  • 서비스 간 전달되는 메타데이터                           │
│  • HTTP Header, Kafka Header, Task Header로 전파           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

W3C Trace Context 표준

┌─────────────────────────────────────────────────────────────┐
│                W3C Trace Context                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  HTTP Header로 Context 전파:                                │
│                                                             │
│  traceparent: 00-{trace_id}-{span_id}-{flags}              │
│               │    │          │        │                   │
│               │    │          │        └─ 01=sampled       │
│               │    │          └─ 현재 span_id (16자리)     │
│               │    └─ trace_id (32자리)                    │
│               └─ version                                   │
│                                                             │
│  예시:                                                      │
│  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-         │
│               00f067aa0ba902b7-01                          │
│                                                             │
│  tracestate: vendor1=value1,vendor2=value2                 │
│  → 벤더별 추가 정보 (optional)                             │
│                                                             │
│  장점:                                                      │
│  • 업계 표준 (모든 벤더 지원)                              │
│  • 언어/프레임워크 독립적                                  │
│  • 자동 전파 라이브러리 풍부                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

OpenTelemetry 아키텍처

구성 요소

┌─────────────────────────────────────────────────────────────┐
│              OpenTelemetry Architecture                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Application                                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                                                     │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐│   │
│  │  │   Traces    │  │   Metrics   │  │    Logs     ││   │
│  │  │   (추적)    │  │   (지표)    │  │   (로그)    ││   │
│  │  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘│   │
│  │         │                │                │       │   │
│  │         └────────────────┼────────────────┘       │   │
│  │                          │                         │   │
│  │                          ▼                         │   │
│  │  ┌─────────────────────────────────────────────┐  │   │
│  │  │           OpenTelemetry SDK                  │  │   │
│  │  │                                              │  │   │
│  │  │  • Auto-instrumentation (자동 계측)         │  │   │
│  │  │  • Manual instrumentation (수동 계측)       │  │   │
│  │  │  • Context Propagation (컨텍스트 전파)      │  │   │
│  │  └──────────────────────┬──────────────────────┘  │   │
│  │                          │                         │   │
│  └──────────────────────────┼─────────────────────────┘   │
│                             │ OTLP                        │
│                             ▼                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              OpenTelemetry Collector                 │   │
│  │                                                     │   │
│  │  Receivers ──▶ Processors ──▶ Exporters            │   │
│  │  (수신)        (가공)         (내보내기)            │   │
│  └────────────────────┬────────────────────────────────┘   │
│                       │                                     │
│       ┌───────────────┼───────────────┐                    │
│       ▼               ▼               ▼                    │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐                │
│  │  Jaeger │    │  Tempo  │    │Prometheus│                │
│  │ (Trace) │    │ (Trace) │    │(Metrics) │                │
│  └─────────┘    └─────────┘    └─────────┘                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

SDK vs Collector

구성 요소 역할 위치
SDK 애플리케이션에서 데이터 수집 앱 내부
Collector 데이터 수신, 가공, 내보내기 별도 프로세스/사이드카

Auto-Instrumentation

# 자동 계측: 라이브러리 패치로 코드 수정 없이 추적

# 설치
# pip install opentelemetry-instrumentation-fastapi
# pip install opentelemetry-instrumentation-httpx
# pip install opentelemetry-instrumentation-celery
# pip install opentelemetry-instrumentation-kafka-python

# 자동 계측 활성화
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.celery import CeleryInstrumentor

FastAPIInstrumentor.instrument()
CeleryInstrumentor.instrument()

# 이후 모든 FastAPI 요청, Celery Task가 자동으로 추적됨

컨텍스트 전파 패턴

HTTP → HTTP

┌─────────────────────────────────────────────────────────────┐
│                  HTTP → HTTP 전파                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Service A                        Service B                 │
│  ┌──────────────────┐            ┌──────────────────┐      │
│  │                  │            │                  │      │
│  │  # 자동 전파     │            │  # 자동 추출     │      │
│  │  response =      │            │  @app.get("/")   │      │
│  │    httpx.get(    │──Headers──▶│  def handler():  │      │
│  │      "http://b", │            │    # 자동으로    │      │
│  │    )             │            │    # trace 연결  │      │
│  │                  │            │                  │      │
│  └──────────────────┘            └──────────────────┘      │
│                                                             │
│  Headers:                                                   │
│  traceparent: 00-abc123...-def456...-01                    │
│                                                             │
│  OpenTelemetry SDK가 자동으로:                              │
│  • 요청 시: Header에 Context 주입                          │
│  • 수신 시: Header에서 Context 추출                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

 

 


구조화 로깅과 통합

trace_id를 로그에 주입

┌─────────────────────────────────────────────────────────────┐
│              Trace-Log 상관관계                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  문제: 로그와 Trace가 분리되어 있음                        │
│  ─────                                                      │
│  Trace: task-123이 느림                                    │
│  → 해당 task의 상세 로그를 찾으려면?                       │
│  → trace_id로 검색해야 함                                  │
│                                                             │
│  해결: 모든 로그에 trace_id 포함                           │
│  ─────                                                      │
│                                                             │
│  import structlog                                           │
│  from opentelemetry import trace                            │
│                                                             │
│  def add_trace_context(logger, method, event_dict):         │
│      """로그에 trace_id, span_id 자동 추가"""              │
│      span = trace.get_current_span()                        │
│      if span:                                               │
│          ctx = span.get_span_context()                      │
│          event_dict["trace_id"] = format(ctx.trace_id, "032x")
│          event_dict["span_id"] = format(ctx.span_id, "016x")│
│      return event_dict                                      │
│                                                             │
│  structlog.configure(                                       │
│      processors=[                                           │
│          add_trace_context,  # ← 추가                      │
│          structlog.processors.JSONRenderer(),               │
│      ]                                                      │
│  )                                                          │
│                                                             │
│  결과 로그:                                                 │
│  {                                                          │
│    "event": "Processing scan",                             │
│    "task_id": "abc-123",                                   │
│    "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",        │
│    "span_id": "00f067aa0ba902b7",                          │
│    "timestamp": "2025-12-19T10:00:00Z"                     │
│  }                                                          │
│                                                             │
│  → Grafana에서 trace_id로 로그 검색 가능                   │
│  → Trace 뷰에서 바로 관련 로그 확인 가능                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

EFK + Trace 연동

┌─────────────────────────────────────────────────────────────┐
│              EFK + Grafana Tempo 연동                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────┐                                           │
│  │ Application │                                           │
│  │             │                                           │
│  │ Logs ───────┼───▶ Fluentd ───▶ Elasticsearch           │
│  │ (trace_id)  │                        │                  │
│  │             │                        │                  │
│  │ Traces ─────┼───▶ OTel Collector ───▶ Grafana Tempo    │
│  │             │                        │                  │
│  └─────────────┘                        │                  │
│                                          ▼                  │
│                              ┌─────────────────────┐       │
│                              │      Grafana        │       │
│                              │                     │       │
│                              │ ┌─────────────────┐│       │
│                              │ │ Trace View      ││       │
│                              │ │                 ││       │
│                              │ │ Span A ─────────││       │
│                              │ │   Span B ───────││       │
│                              │ │     Span C ─────││       │
│                              │ │                 ││       │
│                              │ │ [View Logs] ────┼┼───┐   │
│                              │ └─────────────────┘│   │   │
│                              │                     │   │   │
│                              │ ┌─────────────────┐│   │   │
│                              │ │ Logs (ES)       │◀───┘   │
│                              │ │                 ││       │
│                              │ │ trace_id로 필터 ││       │
│                              │ └─────────────────┘│       │
│                              └─────────────────────┘       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

시각화 도구

Jaeger

┌─────────────────────────────────────────────────────────────┐
│                       Jaeger                                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  특징:                                                      │
│  • CNCF 프로젝트 (졸업)                                    │
│  • 독립 실행형                                              │
│  • 자체 스토리지 (Cassandra, Elasticsearch)                │
│  • 분산 컨텍스트 전파                                      │
│                                                             │
│  Kubernetes 배포:                                           │
│  # Jaeger Operator 사용                                    │
│  apiVersion: jaegertracing.io/v1                           │
│  kind: Jaeger                                               │
│  metadata:                                                  │
│    name: eco2-jaeger                                       │
│  spec:                                                      │
│    strategy: production                                    │
│    storage:                                                 │
│      type: elasticsearch                                   │
│      options:                                               │
│        es:                                                  │
│          server-urls: http://elasticsearch:9200            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

 

비교

기준 Jaeger Grafana Tempo
스토리지 Cassandra/ES Object Storage
인덱싱 있음 없음
비용 높음 (인덱싱) 낮음
검색 다양한 필터 trace_id만
Grafana 통합 플러그인 네이티브
권장 소규모, 독립 실행 대규모, Grafana 사용 중

참고 자료


부록: Eco² 적용 포인트

AI 파이프라인 전체 추적

┌─────────────────────────────────────────────────────────────┐
│              Eco² AI Pipeline Tracing                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Trace: scan-request-abc                                    │
│  ─────────────────────────────────────────────────────────  │
│                                                             │
│  │ POST /scan (50ms)                                       │
│  ├─ validate_request (5ms)                                 │
│  ├─ create_task (10ms)                                     │
│  ├─ publish_to_rabbitmq (5ms)                              │
│  │                                                         │
│  │ celery.process_image (5000ms)                           │
│  ├─ vision_scan (2000ms)                                   │
│  │  └─ call_vision_api (1900ms)                           │
│  ├─ rule_match (500ms)                                     │
│  ├─ answer_gen (2500ms)                                    │
│  │  └─ call_llm_api (2400ms)                              │
│  │                                                         │
│  │ save_to_event_store (20ms)                              │
│  │                                                         │
│  │ kafka.publish (10ms)                                    │
│  │                                                         │
│  │ character.consumer (100ms)                              │
│  ├─ handle_scan_completed (50ms)                           │
│  │  └─ grant_reward (30ms)                                │
│  └─ publish_character_granted (10ms)                       │
│                                                             │
│  Total: 5180ms                                              │
│  Bottleneck: vision_api (39%), llm_api (46%)               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

OpenTelemetry 설정

# domains/_shared/observability/tracing.py

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.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.celery import CeleryInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor

def setup_tracing(service_name: str):
    """OpenTelemetry 초기화"""

    # 1. Resource 설정 (서비스 식별)
    resource = Resource.create({
        "service.name": service_name,
        "service.namespace": "eco2",
        "deployment.environment": settings.ENVIRONMENT,
    })

    # 2. TracerProvider 설정
    provider = TracerProvider(resource=resource)

    # 3. Exporter 설정 (OTLP → Collector)
    exporter = OTLPSpanExporter(
        endpoint=settings.OTEL_COLLECTOR_ENDPOINT,
        insecure=True,
    )
    provider.add_span_processor(BatchSpanProcessor(exporter))

    # 4. 전역 TracerProvider 설정
    trace.set_tracer_provider(provider)

    # 5. Auto-instrumentation
    FastAPIInstrumentor.instrument()
    CeleryInstrumentor.instrument()
    HTTPXClientInstrumentor.instrument()

    return trace.get_tracer(service_name)


# 사용
tracer = setup_tracing("scan-api")

 

Celery Task Tracing

# domains/scan/tasks/ai_pipeline.py

from opentelemetry import trace
from opentelemetry.propagate import inject, extract

tracer = trace.get_tracer(__name__)

@celery_app.task(bind=True, max_retries=3)
def process_image(self, task_id: str, image_url: str):
    """AI 파이프라인 (추적 포함)"""

    # Celery headers에서 Context 추출 (CeleryInstrumentor가 자동 처리)
    # 수동으로 하려면:
    # context = extract(self.request.headers or {})

    with tracer.start_as_current_span("vision_scan") as span:
        span.set_attribute("task.id", task_id)
        span.set_attribute("task.step", "vision")

        result = vision_api.analyze(image_url)
        span.set_attribute("vision.category", result["category"])

    with tracer.start_as_current_span("rule_match") as span:
        span.set_attribute("task.step", "rule")

        rules = rule_engine.match(result)

    with tracer.start_as_current_span("answer_gen") as span:
        span.set_attribute("task.step", "answer")

        answer = llm_api.generate({"vision": result, "rules": rules})

    return {"classification": result, "answer": answer}

scan_id ↔ trace_id 매핑

# API 응답에 trace_id 포함

from opentelemetry import trace

@app.post("/scan")
async def create_scan(request: ScanRequest):
    span = trace.get_current_span()
    trace_id = format(span.get_span_context().trace_id, "032x")

    task = await scan_service.create(request)

    return {
        "task_id": task.id,
        "status": "processing",
        "trace_id": trace_id,  # ← 디버깅용
    }

# 클라이언트가 trace_id를 받아 Grafana에서 조회 가능

AS-IS vs TO-BE

원칙 AS-IS (gRPC) TO-BE (Command-Event Separation)
추적 범위 gRPC 호출 단위 전체 요청 흐름
Context 전파 gRPC Metadata W3C Trace Context
로그 연결 수동 (request_id) 자동 (trace_id)
비동기 추적 불가 Kafka/Celery 전파
시각화 없음 Grafana Tempo
병목 분석 로그 분석 Span 타임라인