SLO 알람을 멀티 burn rate로 갈아타는 법
P99 레이턴시가 살짝 튀었다고 한밤중에 페이저가 울리는 경험, 다들 한 번쯤 해봤을 것 같다. 우리 팀도 작년에 SLO를 도입하고 단일 burn rate 알람으로 굴리다가 알람 피로도 때문에 결국 6개월 만에 갈아엎었다. 이번 글에서는 그때 갈아탔던 멀티 윈도우, 멀티 burn rate 방식의 셋업 가이드를 정리해본다. Google SRE workbook에 나온 것을 우리 팀 환경에 맞춰 변형한 버전이고, Prometheus 기반이라면 거의 그대로 쓸 수 있다.
단일 burn rate가 왜 안 되냐
먼저 단일 윈도우 알람이 왜 망가지는지부터 짚고 가자. SLO 99.9% 가용성을 가정해보자. 30일 기준 에러 버짓은 약 43분이다. burn rate가 1이면 에러 버짓을 정확히 30일에 걸쳐 다 쓰는 속도다. 14.4 burn rate면 1시간만에 2% 소진, 36 burn rate면 1시간에 5% 소진이다.
이걸 한 가지 윈도우로 잡으면 두 가지 문제가 동시에 터진다.
윈도우를 짧게 잡으면(예: 5분) 일시적인 스파이크에도 알람이 운다. 새벽에 GC 한 번 길게 돌면 그걸로 끝. 윈도우를 길게 잡으면(예: 1시간) 진짜 사고가 났을 때 알람이 늦게 온다. 1시간짜리 윈도우는 큰 사고를 감지하는 데 평균 30분 이상 걸린다. 둘 다 망한다.
Google SRE workbook의 멀티 윈도우 방식은 이걸 두 개의 윈도우 AND 조건으로 푼다. 긴 윈도우로 "이게 진짜 사고냐"를 검증하고, 짧은 윈도우(보통 긴 윈도우의 1/12)로 "지금도 진행 중이냐"를 확인한다.
알람 단계 설계
우리 팀은 페이지(즉시 호출)와 티켓(영업시간 대응)을 분리했다. 표로 정리하면 이렇다.
| 단계 | burn rate | long window | short window | 대응 |
|---|---|---|---|---|
| Fast burn (page) | 14.4 | 1h | 5m | 즉시 호출 |
| Medium burn (page) | 6 | 6h | 30m | 즉시 호출 |
| Slow burn (ticket) | 3 | 24h | 2h | 영업시간 대응 |
| Slow burn (ticket) | 1 | 72h | 6h | 영업시간 대응 |
핵심은 burn rate × long window가 모두 동일하게 "에러 버짓의 일정 비율 소진"을 의미하도록 맞춘 점이다. 14.4 × 1h = 약 2% 소진, 6 × 6h = 5% 소진, 3 × 24h = 10% 소진. 이렇게 잡으면 빠른 사고는 빨리 잡고, 천천히 진행되는 dribble은 며칠치를 보고 잡는다.
Prometheus 룰
실제 룰을 보자. 99.9% 가용성 SLO를 잡은 결제 서비스 예시다.
groups:
- name: payment-slo-burnrate
interval: 30s
rules:
# 5분 / 1시간 / 6시간 / 24시간 / 72시간 burn rate 사전 계산
- record: slo:payment:error_rate:5m
expr: |
sum(rate(http_requests_total{service="payment",code=~"5.."}[5m]))
/
sum(rate(http_requests_total{service="payment"}[5m]))
- record: slo:payment:error_rate:1h
expr: |
sum(rate(http_requests_total{service="payment",code=~"5.."}[1h]))
/
sum(rate(http_requests_total{service="payment"}[1h]))
- record: slo:payment:error_rate:6h
expr: |
sum(rate(http_requests_total{service="payment",code=~"5.."}[6h]))
/
sum(rate(http_requests_total{service="payment"}[6h]))
- record: slo:payment:error_rate:24h
expr: |
sum(rate(http_requests_total{service="payment",code=~"5.."}[24h]))
/
sum(rate(http_requests_total{service="payment"}[24h]))
- record: slo:payment:error_rate:72h
expr: |
sum(rate(http_requests_total{service="payment",code=~"5.."}[72h]))
/
sum(rate(http_requests_total{service="payment"}[72h]))
record 룰로 미리 빼놓는 이유는 알람 평가가 매번 거대한 range를 다시 돌리지 않게 하려는 것이다. 24h, 72h 윈도우를 평가 시점마다 raw 시리즈로 돌리면 Prometheus가 비명을 지른다.
다음은 실제 알람 룰이다.
- alert: PaymentSLOFastBurn
expr: |
slo:payment:error_rate:1h > (14.4 * 0.001)
and
slo:payment:error_rate:5m > (14.4 * 0.001)
for: 2m
labels:
severity: page
slo: payment_availability
annotations:
summary: "Payment SLO fast burn (2% in 1h)"
runbook: "https://wiki.internal/runbooks/payment-slo"
- alert: PaymentSLOMediumBurn
expr: |
slo:payment:error_rate:6h > (6 * 0.001)
and
slo:payment:error_rate:30m > (6 * 0.001)
for: 15m
labels:
severity: page
slo: payment_availability
annotations:
summary: "Payment SLO medium burn (5% in 6h)"
- alert: PaymentSLOSlowBurn
expr: |
slo:payment:error_rate:24h > (3 * 0.001)
and
slo:payment:error_rate:2h > (3 * 0.001)
for: 1h
labels:
severity: ticket
slo: payment_availability
14.4 * 0.001 부분이 좀 어색해 보이는데, 0.001은 SLO 에러 예산(1 - 0.999 = 0.1%)이다. 즉 "에러율이 SLO 예산의 14.4배 속도로 소진되는 중"을 뜻한다. 가독성을 위해 slo_error_budget 같은 라벨로 빼두면 더 깔끔한데, 우리 팀은 그냥 숫자로 둔다. 실수 줄이려고 PR 리뷰에서 한 번 더 본다.
for 절을 짧게 두는 것도 일부러다. AND 조건 자체가 이미 짧은 윈도우 검증을 하고 있어서 추가 for는 안전 장치 정도다. 너무 길게 두면 멀티 윈도우의 빠른 감지 효과가 사라진다.
Alertmanager 라우팅
알람 룰만 잘 쓰는 게 끝이 아니다. Alertmanager 라우팅도 같이 고쳐야 한다. 안 그러면 fast burn이랑 slow burn이 같은 채널로 들어와서 결국 또 시끄러워진다.
route:
receiver: default
group_by: [alertname, slo]
routes:
- matchers:
- severity="page"
receiver: oncall-pagerduty
group_wait: 30s
group_interval: 5m
repeat_interval: 1h
- matchers:
- severity="ticket"
receiver: jira-tickets
group_wait: 5m
group_interval: 30m
repeat_interval: 24h
slow burn 알람을 PagerDuty로 보내지 않는 게 핵심. Jira 티켓이나 Slack 채널로 떨어뜨려서 영업시간에 본다. 이거 안 분리하면 새벽 3시에 "지난 24시간 동안 에러율이 평소보다 살짝 높네요" 알람이 와서 페이저가 운다. 진짜로 겪었다.
검증 — 가짜 사고로 흔들어봐라
룰을 배포하고 끝내지 말고 반드시 검증해라. 우리 팀은 staging에서 의도적으로 5xx를 띄워서 알람이 어느 시점에 트리거되는지 측정한다.
간단한 스크립트로 5분간 5xx를 강제로 발생시킨다.
# staging 서비스에 부하를 살짝 줘서 일부러 5xx 유발
for i in $(seq 1 300); do
curl -s -X POST https://payment-staging/inject-error \
-d '{"rate": 0.5, "duration_sec": 1}' > /dev/null
sleep 1
done
이때 확인할 것은 두 가지. fast burn 알람이 5~10분 안에 뜨는가, 그리고 사고가 끝났을 때 알람이 적절히 해제되는가. resolve 시점이 늦으면 운영자가 사고 종료를 인지하기 어렵다.
추가로, 한 분기에 한 번씩은 burn rate 임계값 자체를 다시 본다. SLO 도달율, 트래픽 패턴, 사고 빈도가 다 변하기 때문에 작년에 잡아둔 임계값이 올해도 맞다는 보장이 없다.
마무리
멀티 윈도우 burn rate로 갈아탄 뒤로 우리 팀의 false positive 페이지가 체감상 70% 정도 줄었다. 정확히 측정한 건 아니라서 좀 흔들리는 숫자긴 한데, 새벽에 잠깨는 횟수로는 확실히 줄었다.
남은 과제도 있다. 여러 SLO를 동시에 추적하면서 어떤 사고가 어떤 SLO에 얼마나 영향을 줬는지 사후 분석하는 게 아직 손이 많이 간다. 이건 다음에 별도로 다뤄볼 생각이다.
혹시 다른 방식으로 SLO 알람 운영하시는 분 있으면 댓글로 공유 부탁드린다.