Skip to main content

관찰성 기초

시스템이 정상인지 아닌지를 아는 것은 모니터링입니다. 시스템이 비정상인지를 파악할 수 있는 것은 **관찰성(Observability)**입니다. 관찰성은 시스템의 외부 출력(Metrics, Logs, Traces)을 통해 내부 상태를 추론하는 능력을 의미합니다. ML 모델이 갑자기 느려졌을 때, 추론 결과가 이상할 때, GPU 메모리가 부족할 때 — 원인을 신속하게 파악하려면 관찰성이 갖춰져 있어야 합니다.

학습 목표

  • 모니터링과 관찰성의 차이를 이해하고, 3축(Metrics, Logs, Traces)의 역할을 설명할 수 있다
  • Prometheus 메트릭 타입을 구분하고 기초 PromQL 쿼리를 작성할 수 있다
  • 구조화 로그의 필요성과 로그 수집 파이프라인을 설계할 수 있다
  • 분산 트레이싱의 핵심 개념(Span, Trace ID)과 OpenTelemetry 아키텍처를 이해한다

왜 중요한가

LLM 서빙 환경에서는 전통적인 웹 서비스와 다른 관찰성 요구사항이 있습니다. TTFT(Time To First Token), TPS(Tokens Per Second), GPU 메모리 사용률, KV Cache 적중률 같은 ML 특화 메트릭이 필요합니다. 모델이 환각(hallucination)을 일으키는 빈도, 프롬프트 길이 분포, 토큰 비용 추적 같은 LLM 고유의 관찰성 요구사항은 기존 APM 도구만으로는 충족되지 않습니다. 데이터 파이프라인에서도 관찰성은 핵심입니다. 학습 데이터가 드리프트했는지, 전처리 단계에서 데이터 손실이 발생했는지, 모델 성능이 점진적으로 저하되고 있는지 — 이 모든 것을 Metrics, Logs, Traces의 조합으로 감지합니다.

모니터링 vs 관찰성

구분모니터링 (Monitoring)관찰성 (Observability)
질문”시스템이 정상인가?""왜 비정상인가?”
접근사전 정의된 메트릭/임계치임의의 질문에 답할 수 있는 데이터
대시보드미리 만든 차트로 감시즉석에서 데이터를 탐색
알림알려진 장애 패턴 감지예상치 못한 장애 원인 추적
데이터Metrics 위주Metrics + Logs + Traces 통합
            ┌─────────────────────────────────────┐
            │          Observability               │
            │                                      │
            │   ┌─────────┐  ┌──────┐  ┌───────┐  │
            │   │ Metrics │  │ Logs │  │Traces │  │
            │   │ (숫자)  │  │(이벤트)│ │(경로) │  │
            │   └─────────┘  └──────┘  └───────┘  │
            │        ↕          ↕         ↕        │
            │        상관관계 (Correlation)         │
            └─────────────────────────────────────┘
관찰성의 3축은 독립적이지 않습니다. 알림은 Metrics에서 발생하고, Logs에서 상세 맥락을 확인하며, Traces에서 요청 경로를 추적합니다. 이 3가지를 **상관관계(correlation)**로 연결하는 것이 진정한 관찰성입니다.

Metrics (메트릭)

메트릭은 시간에 따른 숫자 데이터입니다. 시스템의 현재 상태를 정량적으로 표현합니다.

Prometheus 메트릭 타입

타입설명특징사용 예시
Counter단조 증가 카운터0에서 시작, 리셋 시에만 0으로총 요청 수, 총 에러 수, 처리 토큰 수
Gauge증감 가능한 현재값올라갈 수도, 내려갈 수도현재 GPU 사용률, 메모리 사용량, 큐 깊이
Histogram값의 분포 (버킷 기반)le 레이블로 버킷 경계 정의응답 시간 분포, 프롬프트 길이 분포
Summary값의 분포 (분위수 기반)클라이언트 측 분위수 계산p50/p95/p99 지연시간 (비권장, Histogram 선호)

메트릭 작명 규칙

# 형식: {네임스페이스}_{서브시스템}_{이름}_{단위}
# 예시:
http_requests_total              # Counter — 총 HTTP 요청 수
http_request_duration_seconds    # Histogram — 요청 지연시간(초)
gpu_memory_usage_bytes           # Gauge — GPU 메모리 사용량(바이트)
model_inference_tokens_total     # Counter — 처리한 총 토큰 수

PromQL 기초

함수/연산설명예시
rate()Counter의 초당 변화율rate(http_requests_total[5m])
increase()Counter의 구간 증가량increase(http_requests_total[1h])
histogram_quantile()Histogram에서 분위수 계산histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
sum by()레이블별 합계sum by (status_code) (rate(http_requests_total[5m]))
avg by()레이블별 평균avg by (instance) (gpu_utilization)
topk()상위 N개topk(5, rate(http_requests_total[5m]))
absent()메트릭 존재 여부 확인absent(up{job="model-server"})
# API 성공률 (%) — 최근 5분
(
  sum(rate(http_requests_total{status=~"2.."}[5m]))
  /
  sum(rate(http_requests_total[5m]))
) * 100

# GPU 사용률 상위 3 인스턴스
topk(3, avg by (instance) (gpu_utilization_percent))

# p99 응답 시간
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
rate()은 Counter에, avg()나 현재값은 Gauge에 사용합니다. Counter에 직접 값을 보면 단조 증가하는 숫자만 보이므로, 반드시 rate() 또는 increase()로 변환해야 의미 있는 데이터가 됩니다.

Logs (로그)

로그는 개별 이벤트의 상세 기록입니다. 메트릭이 “무엇이 얼마나”를 알려준다면, 로그는 “무엇이 정확히 일어났는지”를 알려줍니다.

비구조화 vs 구조화 로그

# 비구조화 로그 — 사람은 읽기 쉬우나, 기계 파싱 어려움
2024-03-15 14:23:01 ERROR Failed to load model checkpoint from /models/v2.1
Connection timeout after 30s to model registry

# 구조화 로그 (JSON) — 기계 파싱 용이, 검색/필터링 최적
{"timestamp":"2024-03-15T14:23:01Z","level":"ERROR","service":"model-server",
 "message":"Failed to load model checkpoint","model_path":"/models/v2.1",
 "error_type":"ConnectionTimeout","timeout_seconds":30,
 "trace_id":"abc123","span_id":"def456"}
비교비구조화구조화 (JSON)
가독성높음 (사람)낮음 (사람), 높음 (기계)
검색정규표현식, grep필드 기반 쿼리
저장 효율비일관적일관적 스키마
상관관계어려움trace_id 기반 연결
권장개발/디버깅프로덕션 (항상 권장)

로그 레벨

레벨용도예시
DEBUG상세 디버깅 정보입력 텐서 shape, 중간 연산 결과
INFO정상 동작 기록모델 로딩 완료, 요청 처리 성공
WARNING잠재적 문제GPU 메모리 80% 도달, 느린 쿼리 감지
ERROR오류 발생 (복구 가능)모델 체크포인트 로딩 실패, API 타임아웃
FATAL/CRITICAL치명적 오류 (프로세스 종료)OOM, GPU 하드웨어 오류
프로덕션에서 DEBUG 레벨을 활성화하면 로그 볼륨이 폭증하여 저장 비용과 성능에 심각한 영향을 줍니다. 프로덕션은 INFO 이상, 문제 발생 시 일시적으로 DEBUG로 전환하는 패턴을 사용하세요.

로그 수집 파이프라인

┌──────────┐    ┌──────────────┐    ┌───────────────┐    ┌──────────┐
│ App      │ →  │ Fluent Bit   │ →  │ OpenSearch    │ →  │ Kibana/  │
│ (stdout) │    │ (수집/필터)  │    │ 또는 Loki     │    │ Grafana  │
└──────────┘    └──────────────┘    └───────────────┘    └──────────┘
구성 요소역할특징
Fluent Bit경량 로그 수집기저메모리(~450KB), K8s DaemonSet
Fluentd범용 로그 수집기풍부한 플러그인, 복잡한 라우팅
OpenSearch전문 검색 엔진강력한 검색, 높은 저장 비용
Loki로그 집계 시스템레이블 기반 인덱싱, 저비용

Traces (분산 트레이싱)

트레이스는 하나의 요청이 여러 서비스를 거치는 전체 경로를 기록합니다.

핵심 개념

Trace (trace_id: abc-123)
├── Span A: API Gateway (10ms)
│   ├── Span B: Auth Service (3ms)
│   └── Span C: Model Server (150ms)
│       ├── Span D: Tokenizer (5ms)
│       ├── Span E: GPU Inference (120ms)
│       └── Span F: Post-processing (8ms)
개념설명
Trace하나의 요청에 대한 전체 경로. 고유한 Trace ID로 식별
SpanTrace 내 개별 작업 단위. 시작/종료 시간, 속성, 이벤트 포함
Trace ID전체 요청을 관통하는 고유 식별자. 서비스 간 전파
Span ID개별 Span의 고유 식별자
Parent Span현재 Span을 호출한 상위 Span. 트리 구조 형성
AttributesSpan에 첨부된 키-값 메타데이터. model.name, tokens.count

샘플링 전략

전략설명사용 사례
Head-based요청 시작 시 샘플링 결정단순, 일정 비율 수집
Tail-based요청 완료 후 결정에러/느린 요청만 수집, 비용 효율적
Rate limiting초당 최대 N개 트레이스트래픽 급증 시 보호
프로덕션에서 모든 요청을 트레이싱하면 오버헤드가 큽니다. 일반적으로 1-10% 샘플링 + 에러/지연 요청 100% 수집 조합이 효과적입니다.

OpenTelemetry (OTEL)

OpenTelemetry는 Metrics, Logs, Traces를 통합 수집하는 벤더 중립 관찰성 프레임워크입니다.

아키텍처

┌──────────────────────────────────────────────────┐
│                  Application                      │
│  ┌──────────┐  ┌──────────┐  ┌──────────────┐   │
│  │ Tracer   │  │  Meter   │  │   Logger     │   │
│  │ (traces) │  │ (metrics)│  │   (logs)     │   │
│  └──────────┘  └──────────┘  └──────────────┘   │
│           OTEL SDK (자동/수동 계측)                │
└──────────┬───────────────────────────────────────┘
           │ OTLP (OpenTelemetry Protocol)

┌──────────────────────┐
│   OTEL Collector     │
│  ┌────────────────┐  │
│  │   Receivers    │  │  ← OTLP, Prometheus, Jaeger 등
│  │   Processors   │  │  ← 필터, 배치, 속성 추가
│  │   Exporters    │  │  ← Prometheus, Jaeger, Loki 등
│  └────────────────┘  │
└──────────────────────┘

     ┌─────┼──────┐
     ▼     ▼      ▼
 Prometheus Jaeger  Loki

Python 계측 예시

from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

# Tracer 설정
trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer("model-server")

# Meter 설정
metrics.set_meter_provider(MeterProvider())
meter = metrics.get_meter("model-server")

# 커스텀 메트릭 정의
inference_counter = meter.create_counter(
    "model_inference_total",
    description="Total inference requests"
)
inference_duration = meter.create_histogram(
    "model_inference_duration_seconds",
    description="Inference latency"
)

# 트레이싱 + 메트릭 사용
@tracer.start_as_current_span("inference")
def run_inference(prompt: str):
    span = trace.get_current_span()
    span.set_attribute("prompt.length", len(prompt))
    span.set_attribute("model.name", "llama-3-8b")

    start = time.time()
    result = model.generate(prompt)
    duration = time.time() - start

    inference_counter.add(1, {"model": "llama-3-8b", "status": "success"})
    inference_duration.record(duration, {"model": "llama-3-8b"})

    span.set_attribute("output.tokens", result.token_count)
    return result

Prometheus + Grafana 구축

1

Prometheus 설치 (docker-compose)

# docker-compose.yml
services:
  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=15d'

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    volumes:
      - grafana-data:/var/lib/grafana
    depends_on:
      - prometheus

volumes:
  prometheus-data:
  grafana-data:
2

scrape 설정 (prometheus.yml)

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'model-server'
    static_configs:
      - targets: ['model-server:8000']
    metrics_path: '/metrics'

  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']

  - job_name: 'dcgm-exporter'
    static_configs:
      - targets: ['dcgm-exporter:9400']
3

PromQL 쿼리 작성

Prometheus UI(http://localhost:9090)에서 쿼리를 테스트합니다.
# 초당 요청 수
sum(rate(http_requests_total[5m]))

# p95 응답 시간
histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

# 에러율
sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) * 100
4

Grafana 대시보드 구성

Grafana(http://localhost:3000)에서:
  1. Data Sources에 Prometheus 추가 (URL: http://prometheus:9090)
  2. 새 Dashboard 생성 → Panel 추가
  3. 각 Panel에 PromQL 쿼리 입력
  4. 시각화 타입 선택 (Time series, Gauge, Stat, Table 등)
5

알림 규칙 설정

# Grafana Alert Rule 예시
# 조건: 5분간 에러율 > 5%
# 액션: Telegram/Slack/Email 알림
Grafana의 Alerting 메뉴에서 Contact Point 설정 후 Alert Rule을 생성합니다.

운영 메트릭 세트

메트릭유형PromQL 예시임계치 예시
API 성공률Gauge (%)sum(rate(http_requests_total{status=~"2.."}[5m])) / sum(rate(http_requests_total[5m])) * 100> 99.9%
p50 지연시간Histogramhistogram_quantile(0.5, ...)< 100ms
p95 지연시간Histogramhistogram_quantile(0.95, ...)< 500ms
p99 지연시간Histogramhistogram_quantile(0.99, ...)< 1000ms
에러율Gauge (%)sum(rate({status=~"5.."}[5m])) / sum(rate(total[5m]))< 0.1%
큐 깊이Gaugerequest_queue_depth< 100
GPU 사용률Gauge (%)DCGM_FI_DEV_GPU_UTIL알림: > 95%
GPU 메모리Gauge (%)DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTAL * 100알림: > 90%
디스크 사용률Gauge (%)(1 - node_filesystem_avail_bytes/node_filesystem_size_bytes) * 100알림: > 85%
메모리 사용률Gauge (%)(1 - node_memory_MemAvailable_bytes/node_memory_MemTotal_bytes) * 100알림: > 90%

알림 설계

좋은 알림의 원칙

원칙설명나쁜 예 → 좋은 예
행동 가능받은 사람이 즉시 할 수 있는 일이 있어야 함”CPU 높음” → “Pod 스케일링 필요, HPA 확인”
증상 기반원인이 아닌 사용자 영향 기반”디스크 90%” → “API 응답 시간 SLO 위반”
노이즈 억제짧은 스파이크에 반응하지 않음1분 간격 → “5분 이상 지속 시”
에스컬레이션심각도에 따라 단계적 알림warning → Slack, critical → PagerDuty + 전화
런북 연결알림에 대응 절차 문서 포함알림만 → 알림 + 런북 URL

에스컬레이션 단계

Level 1 (INFO):     Slack 채널 알림       ← 참고용, 즉시 대응 불필요
Level 2 (WARNING):  Telegram + Slack      ← 30분 내 확인 필요
Level 3 (CRITICAL): PagerDuty + 전화      ← 즉시 대응 필요
Level 4 (FATAL):    전 팀 호출 + 인시던트  ← 서비스 다운, 전원 동원

LLM 모니터링 특화 메트릭

메트릭설명중요성
TTFT (Time To First Token)첫 토큰 생성까지의 시간사용자 체감 응답 속도의 핵심
TPS (Tokens Per Second)초당 생성 토큰 수처리량(throughput) 지표
토큰 비용입력/출력 토큰당 비용FinOps, 비용 최적화
환각률사실과 다른 응답 비율모델 품질 지표
프롬프트 길이 분포입력 프롬프트 토큰 수 분포리소스 계획, 비용 예측
KV Cache 사용률KV Cache 메모리 점유율vLLM 성능 병목 진단
배치 크기동시 처리 요청 수Continuous Batching 효율
Prefill vs Decode 시간프리필/디코드 단계별 시간병목 단계 식별
# LLM 메트릭 계측 예시
from prometheus_client import Histogram, Counter, Gauge

ttft_histogram = Histogram(
    'llm_time_to_first_token_seconds',
    'Time to first token',
    buckets=[0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0]
)

tokens_per_second = Histogram(
    'llm_tokens_per_second',
    'Output tokens per second',
    buckets=[5, 10, 20, 50, 100, 200]
)

token_cost_counter = Counter(
    'llm_token_cost_dollars_total',
    'Cumulative token cost in dollars',
    ['model', 'direction']  # direction: input/output
)

kv_cache_usage = Gauge(
    'llm_kv_cache_usage_percent',
    'KV cache memory utilization'
)
  • Prometheus: 오픈소스, pull 기반, PromQL, K8s 생태계 표준. 단일 노드 한계 → Thanos/Mimir로 확장
  • InfluxDB: 오픈소스, push 기반, Flux/InfluxQL, IoT/시계열 특화. 자체 클러스터링(Enterprise)
  • Datadog: SaaS, 에이전트 기반, 통합 관찰성. 높은 비용, 낮은 운영 부담 스타트업/소규모는 Prometheus + Grafana, 대규모 조직은 Datadog이나 managed Prometheus(Grafana Cloud)를 고려하세요.
  • Loki: 레이블 기반 인덱싱, 로그 본문은 인덱싱하지 않음, 저비용, Grafana와 네이티브 통합
  • OpenSearch: 전문(full-text) 인덱싱, 강력한 검색, 높은 저장/연산 비용 로그 본문 검색이 빈번하면 OpenSearch, 레이블 기반 필터링으로 충분하면 Loki가 비용 효율적입니다. 대부분의 ML 운영 환경에서는 Loki + Grafana 조합이 적합합니다.
Sidecar 패턴: 각 Pod에 Collector를 사이드카로 배포. 격리 우수, 리소스 오버헤드 큼. DaemonSet 패턴: Node당 1개 Collector. 리소스 효율적, 대부분의 환경에 적합. Gateway 패턴: 중앙 Collector 클러스터. 대규모 환경, 복잡한 라우팅/변환 필요 시. 실무에서는 DaemonSet(수집) + Gateway(처리/라우팅)의 2-tier 구성을 많이 사용합니다.
  • SLI (Service Level Indicator): 측정 가능한 서비스 품질 지표 (예: 가용성 99.95%)
  • SLO (Service Level Objective): 내부 목표 (예: “99.9% 가용성 유지”)
  • SLA (Service Level Agreement): 고객과의 계약 (예: “99.5% 미만 시 크레딧 제공”)
  • 에러 버짓: SLO와 100% 사이의 여유분. 99.9% SLO라면 월 43.2분의 장애 허용 에러 버짓이 남아있으면 배포 속도를 높이고, 소진되면 안정성에 집중하는 것이 SRE의 핵심 원칙입니다.
  1. USE Method (리소스): Utilization, Saturation, Errors — CPU, 메모리, 디스크, 네트워크
  2. RED Method (서비스): Rate, Errors, Duration — API 엔드포인트별
  3. Four Golden Signals: Latency, Traffic, Errors, Saturation — Google SRE 책 기반 대시보드 구성 순서: 최상단에 서비스 전체 상태 → 중간에 개별 서비스 상세 → 하단에 인프라 리소스. 한 대시보드에 Panel 20개 이하로 유지하고, 드릴다운 링크로 상세 대시보드를 연결하세요.
전통적인 APM 도구는 LLM 특화 메트릭을 지원하지 않습니다. 전용 도구를 고려하세요:
  • LangSmith: LangChain 공식, 프롬프트 추적/평가, SaaS
  • Langfuse: 오픈소스, 셀프호스팅 가능, OpenTelemetry 호환
  • Phoenix (Arize): 오픈소스, 트레이싱 + 평가, LLM 특화 시각화 셀프호스팅이 필요하면 Langfuse, LangChain 생태계면 LangSmith를 추천합니다.

체크리스트

  • 모니터링과 관찰성의 차이를 설명할 수 있는가
  • Counter, Gauge, Histogram, Summary 메트릭 타입을 구분할 수 있는가
  • 기초 PromQL(rate, histogram_quantile, sum by)을 작성할 수 있는가
  • 구조화 로그(JSON)의 장점과 필수 필드를 설명할 수 있는가
  • Trace, Span, Trace ID의 관계를 이해하고 있는가
  • OpenTelemetry의 SDK/Collector 아키텍처를 설명할 수 있는가
  • Prometheus + Grafana 기본 스택을 구성할 수 있는가
  • 행동 가능한 알림의 원칙을 이해하고 있는가
  • TTFT, TPS 등 LLM 특화 메트릭의 의미를 설명할 수 있는가
  • USE/RED Method를 활용한 대시보드 설계 원칙을 알고 있는가

다음 문서