Post

[대규모 시스템] Day 5: 관측성과 운영 - 복잡한 시스템을 보고 지키기

[대규모 시스템] Day 5: 관측성과 운영 - 복잡한 시스템을 보고 지키기

이 글은 AI(Claude)의 도움을 받아 작성하고, 작성자가 검토·편집했습니다.

서론: 볼 수 없으면 지킬 수 없다

Day 1~4에서 캐시·큐·로드밸런서·레이트리밋으로 대규모 시스템을 만들었다. 이제 컴포넌트가 수십 개고, 한 요청이 여러 서비스를 거친다. 무언가 느려지거나 실패할 때 “어디서 왜”를 모르면 손쓸 수 없다. 마지막 편은 시스템을 보고(관측성) 지키는(운영) 법이다.

1. 관측성의 세 기둥

1
2
3
4
5
6
7
8
메트릭(Metrics):  숫자 시계열. "얼마나 많이/빠른가"
  요청율·에러율·지연·CPU·큐 길이. 대시보드·알림의 기반. (싸고 집계 쉬움)

로그(Logs):       이벤트 기록. "무슨 일이 있었나"
  구조화된 로그(JSON)로 검색·필터. (상세하나 양이 많고 비쌈)

추적(Traces):     요청의 여정. "어디서 시간을 썼나"
  한 요청이 서비스들을 거친 경로와 각 단계 지연. (분산 디버깅 핵심)

세 가지는 보완 관계다. 메트릭으로 이상을 감지하고, 추적으로 어느 서비스인지 좁히고, 로그로 정확한 원인을 본다.

2. 어떤 메트릭을 볼 것인가: RED와 USE

1
2
3
4
5
6
7
8
9
10
11
RED 메서드 (요청 기반 서비스):
  Rate     - 초당 요청 수
  Errors   - 초당 에러 수 (또는 에러율)
  Duration - 응답 시간 분포

USE 메서드 (자원 기반):
  Utilization - 사용률 (CPU·메모리·디스크)
  Saturation  - 포화도 (큐 길이·대기)
  Errors      - 에러 수

→ 서비스는 RED, 인프라는 USE로 본다

3. 평균이 아니라 분포를 보라

1
2
3
4
5
6
7
8
9
평균 응답시간 50ms "정상"
  → 하지만 p99가 2초라면? 100명 중 1명은 2초를 기다림

반드시 백분위수(percentile)로:
  p50(중앙값) - 전형적 경험
  p95 / p99   - 꼬리 지연(tail latency). 실제 불만의 원인
  p99.9       - 최악의 경험

평균은 이상치를 숨긴다. SLO는 백분위수로 정의한다. (eBPF Day 4와 동일 교훈)

4. 분산 추적

한 요청이 LB → 앱 → 캐시 → 큐 → DB를 거칠 때, 추적 ID를 전파해 전 구간을 잇는다.

1
2
3
4
5
6
7
8
9
Trace (요청 전체) = 여러 Span의 트리
  [Span: API gateway        ] 200ms
    [Span: auth service      ] 15ms
    [Span: user service      ] 170ms
      [Span: redis get       ] 2ms   (캐시 미스)
      [Span: db query        ] 160ms ← 병목 발견!
      [Span: kafka publish   ] 5ms

→ "전체 200ms 중 160ms를 DB 쿼리가 썼다"를 한눈에
1
2
3
4
5
6
7
8
9
# OpenTelemetry: 추적 컨텍스트가 서비스 경계를 넘어 전파됨
from opentelemetry import trace
tracer = trace.get_tracer(__name__)

def get_user(user_id):
    with tracer.start_as_current_span("get_user") as span:
        span.set_attribute("user.id", user_id)
        with tracer.start_as_current_span("db.query"):
            return db.query(user_id)   # 이 span의 지연이 별도 기록됨

표준은 OpenTelemetry다(gRPC Day 5, 분산 시스템 Day 5에서도 등장). 한 번 계측하면 Jaeger·Tempo 등 어디로든 보낸다.

5. SLI / SLO / SLA: 목표를 숫자로

1
2
3
4
5
6
7
8
SLI (Indicator): 측정값. "성공한 요청 비율", "p99 지연"
SLO (Objective): 내부 목표. "월간 가용성 99.9%", "p99 < 300ms"
SLA (Agreement): 고객과의 계약. SLO 위반 시 보상 (대개 SLO보다 느슨)

에러 예산(Error Budget):
  99.9% 목표 = 월 43분의 다운타임 허용
  → 예산이 남으면 빠르게 배포(위험 감수), 소진되면 안정성에 집중
  → 신뢰성과 개발 속도의 객관적 균형점

SLO는 “완벽”이 비현실적임을 인정하고, 허용 가능한 불완전을 숫자로 정의한다.

6. 알림: 증상에 알리고, 원인은 조사하라

1
2
3
4
5
6
7
8
나쁜 알림: "CPU 80%" → 그래서 사용자가 아픈가? 모름. 알림 피로 유발.
좋은 알림: SLO 기반 — "에러율이 예산을 소진 중", "p99가 목표 초과"
          → 사용자 영향이 있을 때만 깨움

원칙:
  - 증상(사용자 영향)에 알림, 원인(CPU 등)은 대시보드로 조사
  - 모든 알림은 행동 가능(actionable)해야 — 아니면 노이즈
  - 알림 피로를 막아야 진짜 장애를 놓치지 않음

7. 장애 대응과 포스트모템

1
2
3
4
5
6
7
8
9
10
장애 대응 흐름:
  감지(알림) → 완화(우선 사용자 영향 차단: 롤백·페일오버·서킷브레이커)
  → 복구 → 원인 분석

비난 없는 포스트모템(blameless postmortem):
  "누가 실수했나"가 아니라 "어떤 시스템이 그 실수를 가능케 했나"
  → 타임라인, 근본 원인, 재발 방지 액션
  → 사람이 아니라 시스템·프로세스를 고친다

카오스 엔지니어링: 장애를 의도적으로 주입해 미리 약점 발견 (분산 Day 5)

8. 시리즈 종합 체크리스트

  1. 캐싱으로 읽기 부하와 DB 압박을 줄였다. (Day 1)
  2. 메시지 큐로 트래픽을 흡수하고 컴포넌트를 분리했다. (Day 2)
  3. 로드밸런싱과 무상태 설계로 수평 확장했다. (Day 3)
  4. 레이트 리미팅으로 시스템을 보호하고 공정성을 확보했다. (Day 4)
  5. 관측성·SLO·장애 대응으로 복잡한 시스템을 보고 지켰다. (Day 5)

시리즈 마무리

대규모 시스템 설계에 마법은 없다. 핵심 교훈은 세 가지다. 첫째, 병목을 측정하고 그 계층만 해결한다 — 추측이 아니라 데이터로. 둘째, 모든 선택은 트레이드오프다 — 일관성 vs 가용성, 비용 vs 성능, 정확도 vs 단순함(분산 시스템 Day 4의 연장). 셋째, 실패를 전제로 설계한다 — 캐시는 미스 나고, 큐는 쌓이고, 서버는 죽고, 클라이언트는 폭주한다.

캐싱→큐→로드밸런싱→레이트리밋→관측성 다섯 단계는 독립된 기법이 아니라 한 시스템의 층위다. 각 층이 한 종류의 부하를 흡수하고, 관측성이 그 모두를 꿰뚫어 본다. 이 사고의 틀을 갖추면, 새로운 규모의 문제를 만나도 “어느 층의 문제이고 어떤 트레이드오프를 택할 것인가”로 분해할 수 있다.

This post is licensed under CC BY 4.0 by the author.