ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Distributed Tracing: Trace, Span으로 분산 시스템 추적하기
    이코에코(Eco²)/Applied 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 타임라인

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango