burn rate alert 튜닝하다 3주 삽질한 이야기
지난달 얘기다. SLO 도입한 지 반 년쯤 지났고, 이제 대시보드에 error budget 남은 % 뜨는 것도 익숙해질 무렵이었다. 그런데 정작 알림이 이상하게 울렸다. 새벽 2시에 페이지 오는데 실제로 보면 이미 회복돼 있고, 반대로 며칠에 걸쳐 조금씩 새는 장애는 누구도 눈치를 못 챘다. 팀 회고 때 한 명이 "우리 burn rate alert가 알림 역할을 못 하고 있는 것 같다"고 얘기했고, 그때부터 3주 동안 정말 여러 번 갈아엎었다.
처음엔 그냥 5분 창이었다
부끄러운 얘기지만, 처음 세팅한 알림은 "최근 5분 error rate가 SLO 임계값의 10배 넘으면 페이지" 정도였다. 이게 왜 문제인지는 세팅한 사람도 알고 있었을 텐데, 그냥 다들 바빠서 방치돼 있었다.
문제는 두 가지였다.
첫째, 트래픽이 적은 새벽에 5분 창이 너무 예민했다. 초당 요청이 몇십 건 되는 시간대에 500 에러 세 개 나면 그게 이미 10x 초과다. 자동 재시도 로직 하나의 튀면 페이지가 온다. 실제로 어느 주말엔 새벽에 세 번 페이지 왔는데, 세 번 다 로그 보니 그냥 스로틀링에 걸린 재시도 폭주였다. 5분 뒤엔 다 정상.
둘째, 반대로 낮에는 트래픽이 워낙 크니까 조금 새는 정도로는 5분 창 임계값을 못 넘겼다. 초당 1000 건 중에 5건씩 실패가 30분간 계속돼도, 5분마다 재계산되는 창은 매번 임계값 아래. 그 사이 error budget은 야금야금 줄어들고 있었다.
Google SRE 책 다시 읽었다
이 시점에 팀에서 SRE workbook 챕터 5, 6을 다시 돌려 읽었다. multi-window multi-burn-rate 알림 얘기. 요지는 이렇다.
- 짧은 창은 responsiveness 담당 (빠르게 감지)
- 긴 창은 precision 담당 (오탐 억제)
- 둘 다 동시에 임계값 넘겼을 때만 페이지
이론은 알고 있었는데 우리 서비스에 어떤 숫자를 넣어야 할지가 문제였다. 워크북엔 예시가 여러 개 있는데, 그대로 갖다 붙이니 우리 환경엔 안 맞았다. 특히 SLO가 99.9% 서비스와 99.99% 서비스가 섞여 있어서, 하나의 룰 세트로 커버가 안 됐다.
첫 번째 시도: 14x/1h + 14x/5m
워크북에서 "fast burn" 대표 예시로 나오는 조합이다. 1시간 창과 5분 창 모두 14배 넘게 소진하면 페이지.
- alert: HighErrorBudgetBurn
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[1h]))
/
sum(rate(http_requests_total[1h]))
) > (14 * (1 - 0.999))
and
(
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
) > (14 * (1 - 0.999))
for: 2m
이게 이론상은 좋은데, 우리 환경에선 두 가지 문제가 있었다.
하나는 배포 직후 5분 창이 자주 튀는 이슈. rolling update 중에 헬스체크 실패로 잠깐 5xx가 튀는 게 있는데, 이게 5분 창에선 15배까지도 순간적으로 뛴다. 그게 배포 창(deployment window) 동안 계속되면 1시간 창도 결국 임계값 근처로 밀린다.
또 하나는 저 위에서 말한 저트래픽 시간대 문제. 새벽에 요청 자체가 10 rps 정도인데 그중 1건 실패면 이미 10%. 임계값 대비 100배. 이건 창을 아무리 늘려도 요청 절대량이 적으니 통계적으로 의미가 없다.
두 번째 시도: 최소 요청 수 게이트
그래서 창 조합을 유지하되, "요청 절대량이 일정 이상일 때만 평가"하는 게이트를 붙였다.
- alert: HighErrorBudgetBurn
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[1h]))
/
sum(rate(http_requests_total[1h]))
) > (14 * (1 - 0.999))
and
(
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
) > (14 * (1 - 0.999))
and
sum(rate(http_requests_total[5m])) > 5
마지막 조건이 게이트. rps 5 이상일 때만 평가한다. 이걸 붙이고 새벽 오탐은 확실히 줄었다. 근데 이번엔 반대 문제가 생겼다. 저트래픽 시간에 진짜 장애가 났을 때 감지가 늦어졌다.
우리 서비스 중 하나가 새벽에 배치 처리를 하는데, 배치 결과를 조회하는 API가 새벽엔 요청 수가 적다. 그 API가 새벽 4시에 통짜로 죽었는데, 알림이 아침 8시에 트래픽 회복되면서 뒤늦게 울렸다. 사용자는 이미 항의 접수 상태.
세 번째 시도: 다중 티어
여기서 팀 내부 논의 끝에 접근을 바꿨다. 하나의 알림으로 다 잡으려 하지 말고, 세 티어로 나누자.
- Tier 1 (Page): 1h+5m, 14배, rps 게이트 있음 — 명확한 심각 장애
- Tier 2 (Page): 6h+30m, 6배, rps 게이트 없음 — 저트래픽 지속 장애
- Tier 3 (Ticket): 3d+6h, 1배, 게이트 없음 — 조용히 새는 유형
Tier 1은 기존 fast burn 그대로. Tier 2는 창을 크게 늘려서 저트래픽에서도 시간이 지나면 통계적으로 의미 있는 수치가 되게 했다. Tier 3은 페이지 안 울리고 JIRA 티켓만 자동 생성. 다음 스탠드업에서 다루는 방식.
# Tier 2: 저트래픽 대비
- alert: SustainedErrorBudgetBurn
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[6h]))
/
sum(rate(http_requests_total[6h]))
) > (6 * (1 - 0.999))
and
(
sum(rate(http_requests_total{status=~"5.."}[30m]))
/
sum(rate(http_requests_total[30m]))
) > (6 * (1 - 0.999))
for: 5m
labels:
severity: page
아직 남은 문제
이 구조로 정착한 지 두 달쯤 됐다. 오탐은 확실히 줄었고, 저트래픽 장애도 4시간이 아니라 30~40분 안엔 잡힌다. 그런데 몇 가지는 아직 애매하다.
하나. 배포 중 5xx 스파이크. 아직도 fast burn tier가 배포 후 몇 분간 아슬아슬하게 임계값 근처를 왔다갔다 한다. 배포 창을 라벨로 마킹해서 해당 시간엔 알림을 죽이는 방법도 검토 중인데, 그러면 배포 직후 실제 장애를 놓칠 위험이 있다. 아직 결론 못 냈다.
둘. SLO 여러 개(예: availability + latency)에서 burn이 동시에 진행될 때 알림이 여러 개 겹치는 문제. Alertmanager inhibit 룰로 묶긴 했는데, 우선순위 정의가 아직 조잡하다.
셋. 서비스마다 SLO 목표가 다르다 보니, 위 룰을 서비스 단위로 템플릿화해서 관리하는 게 만만치 않다. 최근 oneuptime 쪽에서 OpenTelemetry 메트릭으로 multi-burn-rate 룰 만드는 예제가 올라와서 참고하는 중이다. Prometheus 룰을 코드에서 생성하는 파이프라인을 준비 중인데, 이건 다음에 정리해보려고 한다.
마무리
burn rate 알림은 결국 "얼마나 빠르게 감지하고 싶은가 vs 얼마나 많이 페이지 받고 싶은가" 사이의 튜닝이다. 워크북 예시를 그대로 갖다 쓰면 처음엔 잘 도는 것 같아도, 서비스특성(트래픽 패턴, SLO 목표, 배포 븈도)에 맞추다 보면 결국 여러 틴떴를 조햩하게 되더라.
혹시 저트래픽 서비스에서 다른 방식으로 잘 장히시는 분 있으면 방벑 좀 공트해주세요. 아직 닕을 못펧은 부분이다.
태그: SRE, SLO, burn-rate, alerting, prometheus, 관층성