[대규모 시스템] 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. 시리즈 종합 체크리스트
- 캐싱으로 읽기 부하와 DB 압박을 줄였다. (Day 1)
- 메시지 큐로 트래픽을 흡수하고 컴포넌트를 분리했다. (Day 2)
- 로드밸런싱과 무상태 설계로 수평 확장했다. (Day 3)
- 레이트 리미팅으로 시스템을 보호하고 공정성을 확보했다. (Day 4)
- 관측성·SLO·장애 대응으로 복잡한 시스템을 보고 지켰다. (Day 5)
시리즈 마무리
대규모 시스템 설계에 마법은 없다. 핵심 교훈은 세 가지다. 첫째, 병목을 측정하고 그 계층만 해결한다 — 추측이 아니라 데이터로. 둘째, 모든 선택은 트레이드오프다 — 일관성 vs 가용성, 비용 vs 성능, 정확도 vs 단순함(분산 시스템 Day 4의 연장). 셋째, 실패를 전제로 설계한다 — 캐시는 미스 나고, 큐는 쌓이고, 서버는 죽고, 클라이언트는 폭주한다.
캐싱→큐→로드밸런싱→레이트리밋→관측성 다섯 단계는 독립된 기법이 아니라 한 시스템의 층위다. 각 층이 한 종류의 부하를 흡수하고, 관측성이 그 모두를 꿰뚫어 본다. 이 사고의 틀을 갖추면, 새로운 규모의 문제를 만나도 “어느 층의 문제이고 어떤 트레이드오프를 택할 것인가”로 분해할 수 있다.