본문으로 건너뛰기

실전 고수 팁 — 모니터링 설계 전략

모니터링 도구를 잘 설치하는 것과, 모니터링을 잘 설계하는 것은 전혀 다른 이야기입니다. 수천 개의 알람이 울리는데 정작 중요한 장애를 놓치거나, 화려한 대시보드를 만들었지만 장애 시 아무 도움이 안 되는 상황은 현장에서 흔히 볼 수 있습니다. 이 챕터에서는 실제 프로덕션 환경에서 검증된 모니터링 설계 전략을 다룹니다.

알람 피로(Alert Fatigue) 줄이기​

알람 피로란 너무 많은 알림에 노출된 팀이 정작 중요한 알람을 무시하게 되는 현상입니다. 2019년 Google의 SRE 보고서에 따르면, 한 번에 너무 많은 알람이 발생하면 엔지니어들이 알람을 끄거나 무시하는 경향이 있습니다.

알림 우선순위 분류 (Priority Matrix)​

알람을 다음 4가지로 분류합니다.

우선순위정의예시대응 방식
P1 - Critical즉각 서비스 중단 영향전체 서버 다운, DB 연결 불가즉시 전화/SMS + 호출
P2 - High서비스 품질 심각하게 저하에러율 5% 초과, p99 응답시간 10초Slack + 15분 내 대응
P3 - Medium잠재적 문제, 즉각 영향 없음디스크 80% 초과, 메모리 경고Slack + 근무시간 내 대응
P4 - Low정보성, 추세 파악용배포 완료, 인증서 30일 전 만료일일 요약 리포트

임계값 설정 원칙​

잘못된 임계값 설정은 알람 피로의 주요 원인입니다.

# 잘못된 예 — 절대값 기반 임계값 (노이즈가 많음)
- alert: HighResponseTime
expr: http_response_time_seconds > 0.5 # 항상 특정 시간대에 울림

# 올바른 예 — 동적 기준선(baseline) 대비 이상치 탐지
- alert: AnomalousResponseTime
expr: |
http_response_time_seconds
> on (job) group_left()
(avg_over_time(http_response_time_seconds[1h] offset 1w) * 2)
for: 5m
annotations:
summary: "Response time is 2x higher than same time last week"

# 올바른 예 — 증가율 기반
- alert: ResponseTimeSpike
expr: |
rate(http_request_duration_seconds_sum[5m])
/ rate(http_request_duration_seconds_count[5m])
> 2
for: 3m
labels:
severity: warning

임계값 설정의 황금 규칙:

  • for 절 필수: 순간적인 스파이크로 인한 오탐을 방지합니다. 최소 2~5분 지속 조건을 붙입니다.
  • 충분한 히스토리 데이터 수집 후 설정: 최소 2주치 데이터를 분석한 후 p95/p99를 기준으로 설정합니다.
  • 알람 수를 줄이는 것이 목표: 알람이 너무 많으면 절반을 끄고, 그래도 너무 많으면 또 절반을 끕니다.

SLO, SLI, SLA — 모니터링의 목표 설정​

개념 정의​

SLA(Service Level Agreement) 는 서비스 제공자와 고객 간에 체결하는 계약입니다. 예: "월 가동률 99.9% 미만 시 요금 환불 제공"

SLO(Service Level Objective) 는 팀 내부에서 달성하고자 하는 목표치입니다. SLA보다 약간 엄격하게 설정하여 완충 역할을 합니다. 예: "월 가동률 99.95% 유지"

SLI(Service Level Indicator) 는 SLO를 측정하기 위한 실제 지표입니다. 예: "성공적으로 처리된 요청 수 / 전체 요청 수"

SLA: 99.9%    (고객과의 계약, 어기면 패널티)
↑
SLO: 99.95% (내부 목표, 여유 버퍼 포함)
↑
SLI: 실제 측정값 (Prometheus 등으로 계산)

Prometheus로 SLI 계산​

# prometheus/rules/slo.yml
groups:
- name: slo-rules
interval: 30s
rules:
# ─── SLI: 가용성 (Availability) ─────────────────────────────────────
# 5xx를 제외한 성공률 계산
- record: job:http_requests:success_rate5m
expr: |
sum(rate(http_requests_total{status!~"5.."}[5m])) by (job)
/
sum(rate(http_requests_total[5m])) by (job)

# ─── SLI: 지연시간 (Latency) ─────────────────────────────────────────
# p99 응답시간이 300ms 이하인 요청 비율
- record: job:http_requests:latency_ok_rate5m
expr: |
sum(rate(http_request_duration_seconds_bucket{le="0.3"}[5m])) by (job)
/
sum(rate(http_request_duration_seconds_count[5m])) by (job)

# ─── SLO 위반 알람 ────────────────────────────────────────────────────
- alert: SLOAvailabilityBreach
expr: job:http_requests:success_rate5m < 0.9995 # 99.95% 미달
for: 5m
labels:
severity: critical
annotations:
summary: "SLO breach: availability {{ $value | humanizePercentage }}"
description: "Service {{ $labels.job }} availability dropped below 99.95%"

에러 버짓(Error Budget) 기반 알림 전략​

에러 버짓은 SLO를 달성하면서 허용되는 실패의 총량입니다.

월 SLO: 99.9%
월 총 시간: 43,200분
허용 다운타임 = 43,200 × (1 - 0.999) = 43.2분

에러 버짓이 소진되면:
- 새 기능 배포 중단
- 안정화에 집중
#!/usr/bin/env python3
# error_budget_calculator.py
# 에러 버짓 소진율 계산기

def calculate_error_budget(slo_target: float, total_requests: int, failed_requests: int) -> dict:
"""
slo_target: SLO 목표값 (예: 0.999 = 99.9%)
total_requests: 전체 요청 수
failed_requests: 실패 요청 수
"""
allowed_failures = total_requests * (1 - slo_target)
actual_success_rate = (total_requests - failed_requests) / total_requests
error_budget_remaining = (allowed_failures - failed_requests) / allowed_failures * 100

return {
"slo_target": f"{slo_target * 100:.2f}%",
"actual_success_rate": f"{actual_success_rate * 100:.4f}%",
"allowed_failures": int(allowed_failures),
"actual_failures": failed_requests,
"error_budget_remaining": f"{error_budget_remaining:.1f}%",
"status": "OK" if error_budget_remaining > 0 else "EXHAUSTED"
}

if __name__ == "__main__":
# 예시: 이번 달 100만 요청 중 800건 실패
result = calculate_error_budget(
slo_target=0.999,
total_requests=1_000_000,
failed_requests=800
)
for key, value in result.items():
print(f"{key:30s}: {value}")

# 출력:
# slo_target : 99.90%
# actual_success_rate : 99.9200%
# allowed_failures : 1000
# actual_failures : 800
# error_budget_remaining : 20.0%
# status : OK

에러 버짓 소진율에 따른 알람 전략:

# prometheus/rules/error-budget.yml
groups:
- name: error-budget
rules:
# 에러 버짓 50% 소진 → 경고
- alert: ErrorBudgetBurnRateHigh
expr: |
(1 - job:http_requests:success_rate5m)
/ (1 - 0.999) > 5
for: 10m
labels:
severity: warning
annotations:
summary: "Error budget burning 5x faster than expected"

# 에러 버짓 90% 소진 → 긴급
- alert: ErrorBudgetAlmostExhausted
expr: |
(1 - job:http_requests:success_rate5m)
/ (1 - 0.999) > 14.4 # 1시간 내 소진 예상
for: 2m
labels:
severity: critical
annotations:
summary: "Error budget will be exhausted within 1 hour at current rate"

골든 시그널 (Golden Signals)​

Google SRE 팀이 정의한 모든 서비스 모니터링의 핵심 4개 지표입니다.

1. 지연시간 (Latency)​

요청 처리에 걸리는 시간. 성공 요청과 실패 요청을 반드시 구분합니다. 에러는 매우 빠르게 반환될 수 있어, 에러를 포함하면 지연시간이 실제보다 낮게 측정됩니다.

# 지연시간 알람
- alert: HighLatency
expr: |
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket{status!~"5.."}[5m])
) > 1.0
for: 5m
annotations:
summary: "p99 latency for successful requests exceeds 1s"

2. 트래픽 (Traffic)​

서비스에 가해지는 부하의 양. 초당 요청 수(RPS), 초당 바이트 수 등으로 측정합니다.

- alert: UnexpectedTrafficDrop
expr: |
rate(http_requests_total[5m])
< (avg_over_time(rate(http_requests_total[5m])[1h:5m]) * 0.5)
for: 5m
annotations:
summary: "Traffic dropped to less than 50% of 1-hour average — possible upstream issue"

3. 에러율 (Errors)​

실패한 요청의 비율. 명시적 에러(5xx)와 암묵적 에러(잘못된 콘텐츠를 200으로 반환)를 모두 추적해야 합니다.

- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) by (job)
/
sum(rate(http_requests_total[5m])) by (job)
> 0.01 # 에러율 1% 초과
for: 3m

4. 포화도 (Saturation)​

서비스가 얼마나 가득 찼는가. 리소스 한계에 가까워질수록 성능이 저하됩니다.

# CPU 포화도
- alert: HighCPUSaturation
expr: |
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
for: 10m

# JVM 힙 포화도
- alert: JVMHeapPressure
expr: |
jvm_memory_used_bytes{area="heap"}
/ jvm_memory_max_bytes{area="heap"}
> 0.9
for: 5m
annotations:
summary: "JVM heap usage above 90% — GC pressure likely"

Nginx/Tomcat 핵심 모니터링 메트릭 체크리스트​

Nginx 필수 메트릭​

# nginx.conf — stub_status 모듈 활성화
server {
listen 8080;
location /nginx_status {
stub_status on;
allow 127.0.0.1;
allow 172.16.0.0/12; # Docker 네트워크
deny all;
}
}
메트릭의미알람 임계값
nginx_connections_active현재 활성 연결 수worker_connections의 80%
nginx_connections_waitingKeep-alive 대기 연결높으면 타임아웃 설정 검토
nginx_http_requests_total총 요청 수 (RPS 계산)비정상 급증/급락
5xx 에러율서버 에러 비율1% 초과 시 경고
nginx_ingress_upstream_latency업스트림 응답시간p99 > 500ms

Tomcat 필수 메트릭​

<!-- server.xml — JMX 활성화 -->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
maxThreads="200"
minSpareThreads="10"
maxConnections="10000"
acceptCount="100" />
메트릭의미알람 임계값
tomcat_threads_busy현재 처리 중인 스레드maxThreads의 80%
tomcat_threads_config_max최대 스레드 수참고값
tomcat_connections_current현재 연결 수maxConnections의 90%
tomcat_global_request_count처리된 요청 수RPS 추세 모니터링
tomcat_global_error_count에러 발생 수증가 추세 알람
JVM GC 시간 비율GC 오버헤드전체 시간의 5% 초과

대시보드 설계 원칙: 계층별 뷰​

잘 설계된 대시보드는 "무엇이 잘못됐는가"를 빠르게 파악하게 해줍니다. 세 계층으로 구성합니다.

계층 1: 비즈니스 뷰 (Business View)
- 주문 전환율, 매출, 활성 사용자 수
- 비즈니스 임원이 보는 화면
- 5분 자동 갱신

계층 2: 애플리케이션 뷰 (Application View)
- API 엔드포인트별 성능 (Golden Signals)
- 에러율, 지연시간, RPS
- 개발팀/SRE가 보는 화면
- 1분 갱신

계층 3: 인프라 뷰 (Infrastructure View)
- CPU, 메모리, 디스크, 네트워크
- 서버별 세부 메트릭
- 인프라팀이 보는 화면
- 30초 갱신

Grafana 대시보드 변수 활용:

{
"templating": {
"list": [
{
"name": "environment",
"type": "custom",
"options": [
{"text": "Production", "value": "prod"},
{"text": "Staging", "value": "staging"}
]
},
{
"name": "instance",
"type": "query",
"query": "label_values(up{env=\"$environment\"}, instance)"
}
]
}
}

대시보드 설계 안티패턴:

  • 너무 많은 패널 (하나의 대시보드에 20개 초과 금지)
  • 색상 과다 사용 (신호등 원칙: 초록=정상, 노랑=경고, 빨강=위험)
  • 컨텍스트 없는 숫자 (현재값 단독 표시 금지, 반드시 추세선과 함께)

온콜(On-call) 체계 구성 팁​

온콜 체계를 처음 구성할 때 흔히 저지르는 실수는 "모든 사람이 모든 알람을 받게" 설정하는 것입니다.

PagerDuty/OpsGenie 에스컬레이션 정책 예시:

1차 응답자 (Primary On-call):
- P1/P2 알람 즉시 수신
- 5분 내 미응답 시 → 2차 에스컬레이션

2차 응답자 (Secondary On-call):
- 1차 미응답 시 알람 수신
- 10분 내 미응답 시 → 팀장 에스컬레이션

팀장:
- 15분 이상 미대응 시 알람
- 비즈니스 임팩트 판단 후 추가 인력 소집

교대 주기: 1주 (2주 이상은 번아웃 위험)
온콜 보상: 명확한 인정/보상 체계 없으면 지속 불가

온콜 피로 최소화:

# 알람 발생 후 팀 회고 체크리스트
# 1. 이 알람이 진짜 액션이 필요했나? (Yes/No)
# 2. 알람 임계값이 적절했나?
# 3. 런북이 있었나? 도움이 됐나?
# 4. 자동화로 해결 가능한가?
# 5. 재발 방지를 위해 무엇을 해야 하나?

런북(Runbook) 작성 방법​

런북은 장애 발생 시 대응 절차를 문서화한 가이드입니다. 새벽 3시에 잠에서 깬 엔지니어도 따라할 수 있도록 작성해야 합니다.

런북 필수 구성 요소:

# Runbook: High Error Rate on /api/orders

## 알람 조건
- 알람명: OrderServiceHighErrorRate
- 트리거: error_rate > 5% for 3 minutes

## 즉각 진단 명령어
```bash
# 1. 현재 에러율 확인
curl -s http://prometheus:9090/api/v1/query \
--data-urlencode 'query=rate(http_requests_total{status=~"5..",job="order-service"}[5m])' \
| jq '.data.result[].value[1]'

# 2. 최근 에러 로그 확인 (Kibana 쿼리)
# index: tomcat-error-*, 필터: log_level:ERROR AND logger:*OrderController*

# 3. DB 연결 상태 확인
docker exec -it order-service-db mysql -u root -p \
-e "SHOW STATUS LIKE 'Threads_connected'; SHOW PROCESSLIST;"

# 4. 최근 배포 이력 확인
kubectl rollout history deployment/order-service

가능한 원인과 해결 방법​

원인증상해결 방법
DB 연결 풀 고갈DB 관련 에러, 응답시간 급증연결 풀 크기 증가, 슬로우 쿼리 확인
외부 API 장애특정 엔드포인트만 에러서킷 브레이커 확인, 폴백 활성화
메모리 부족OOM, GC 과부하인스턴스 재시작, 메모리 증설
코드 배포 실패배포 직후 에러 급증이전 버전 롤백

에스컬레이션​

  • 15분 내 해결 불가 → 팀장 호출
  • DB 장애 → DBA 팀 호출
  • 외부 서비스 장애 → 해당 팀 비상 연락처 확인

## 로그 보존 정책 및 비용 최적화

로그 저장 비용은 생각보다 빠르게 증가합니다. 티어드 스토리지 전략이 필요합니다.

Hot Tier (최근 7일): SSD, 빠른 검색 → Elasticsearch Hot Node Warm Tier (730일): HDD, 느린 검색 허용 → Elasticsearch Warm Node Cold Tier (3090일): 오브젝트 스토리지 → S3/GCS (Elasticsearch Frozen Index) Archive (90일+): Glacier/Coldline → 장기 보관, 거의 미조회


**로그 레벨별 보존 기간 권장값:**

| 로그 종류 | 보존 기간 | 이유 |
|---|---|---|
| 액세스 로그 | 90일 | 보안 감사, 트래픽 분석 |
| 에러/예외 로그 | 1년 | 재현 분석, 규정 준수 |
| 결제/트랜잭션 | 5년+ | 회계 감사, 법적 요구사항 |
| 디버그 로그 | 7일 | 개발 용도, 장기 보관 불필요 |
| 보안 이벤트 | 2년+ | 보안 감사, 침해 사고 조사 |

**ILM 기반 비용 최적화:**

```bash
# 현재 인덱스별 디스크 사용량 확인
curl -u elastic:changeme123! \
"http://localhost:9200/_cat/indices?v&s=store.size:desc&h=index,store.size,pri.store.size,docs.count"

# 압축으로 스토리지 절약 (Warm tier에서 force merge)
curl -X POST "http://localhost:9200/nginx-access-2026.01.*/_forcemerge?max_num_segments=1" \
-u elastic:changeme123!

# 오래된 인덱스 수동 삭제 (ILM 정책이 없는 경우)
curl -X DELETE "http://localhost:9200/nginx-access-2025.12.*" \
-u elastic:changeme123!

비용 절감 팁:

  • 샘플링: 트래픽이 많은 서비스는 액세스 로그를 10~20%만 수집 (에러 로그는 100% 유지)
  • 필드 최소화: 불필요한 필드는 Logstash에서 제거 후 저장
  • 인덱스 압축: Warm/Cold 티어 이동 시 force merge로 세그먼트 수를 1로 줄임
  • 알람보다 SLO: 모든 것을 알람으로 해결하려 하지 말고, SLO 달성 여부만 집중 모니터링