IT/SRE

SLO burn rate alert가 4시간 반 동안 안 울린 밤

gfrog 2026. 7. 4. 06:44
SMALL

지난 화요일 새벽에 사고가 하나 있었다. 결제 API의 error rate가 슬금슬금 올라가고 있었는데, 우리가 그렇게 자랑하던 multi-window multi-burn-rate alert는 새벽 5시가 다 되어서야 울렸다. CS팀이 먼저 알아챈 걸 사후에 확인했다. 그 시간까지 error budget의 30% 가까이가 이미 타버린 상태였다.

사실 이 alert 룰은 팀 내부에서 두 달 전에 큰 리팩터링을 마쳤던 것이라 더 뼈아팠다. Google SRE 워크북에 나온 그 표를 그대로 옮겨놓고, PromQL도 두 번 세 번 리뷰했었다. 근데 왜 안 울렸을까.

룰은 정석대로였다

우리가 쓰던 fast burn 알람은 이런 모양이었다.

- alert: PaymentApiErrorBudgetBurnFast
  expr: |
    (
      sum(rate(http_requests_total{service="payment-api",code=~"5.."}[1h]))
      /
      sum(rate(http_requests_total{service="payment-api"}[1h]))
      > (14.4 * 0.001)
    )
    and
    (
      sum(rate(http_requests_total{service="payment-api",code=~"5.."}[5m]))
      /
      sum(rate(http_requests_total{service="payment-api"}[5m]))
      > (14.4 * 0.001)
    )
  for: 2m
  labels:
    severity: page

SLO는 99.9% 가용성, 그러니까 error budget 0.001. burn rate 14.4배 = 30일 예산의 2%가 1시간에 타는 속도. 이게 1시간 창과 5분 창 양쪽에서 동시에 참일 때만 페이지 나가게 걸어둔 거였다. 워크북대로.

그런데 우리가 놓친 게 있었다.

traffic이 얇으면 이 공식 자체가 못 쓰는 물건이 된다

사고 났던 화요일 새벽 2시~4시 구간, payment API로 들어오던 초당 요청 수가 평균 3~5 QPS였다. 5분 창으로 잡으면 그 안에 요청이 900개~1500개. 근데 실제로 실패하고 있던 요청은 그 중에 15~25건.

이 정도 traffic에서 error rate 계산하면 어떻게 되냐면, 0.014 근처에서 왔다갔다 하는데 노이즈가 워낙 커서 5분 window의 값이 14.4 * 0.001 = 0.0144를 안정적으로 넘질 못했다. 넘었다가, 안 넘었다가. for: 2m이 요구하는 "2분 내내 참"을 만족을 못 시켰다.

낮에는 이게 문제가 안 됐다. 초당 QPS가 200~400 나오는 시간대에서는 5분 창에 요청 6만~12만 개가 쌓이니까 노이즈가 상쇄된다. 그런데 새벽 low-traffic에는 창을 아무리 좁혀도 통계적으로 의미 있는 신호가 안 나온다.

이거 사실 워크북에도 경고가 있다. "low-traffic services에는 이 접근 자체가 문제가 될 수 있다"고. 근데 우리는 24/7 결제 서비스니까 traffic이 항상 넉넉할 거라고 넘겼다. 로우 트래픽 시간대의 절대량이 그렇게까지 낮은지 실측을 안 해본 게 문제였다. 대시보드에서 QPS를 낮에 봤지, 새벽 시간대에 봤을 리가 없지.

왜 hourly window는 안 울렸냐

Fast burn은 그렇다 치고, 우리는 slow burn (6시간 창, burn rate 6배)도 걸어뒀다. 이건 왜 안 울렸을까.

이것도 비슷한 이유였는데, 조금 다른 각도였다. 새벽에 error rate가 실제로 6배 burn rate를 넘어가긴 넘어갔다. 근데 그 넘어가는 시점이 새벽 1시부터라, 6시간 창이 값을 축적하기까지 시간이 걸린다. 6시간 window 안에서 실패 요청 비율이 threshold를 넘으려면 최소 몇 시간의 지속적인 실패가 쌓여야 하는데, 지속적이지도 않았다. 30분마다 실패율이 튀었다가 잠깐 가라앉기를 반복. Retry 로직이 부분적으로 살려주고 있었어서.

그러다 새벽 4시 반쯤 되어서야 6시간 창이 완전히 채워지면서 alert가 나갔다. 지연이 3시간에 육박했다는 뜻이다. 이걸 slow burn이 "정상 동작"이라고 부를 수 있나?

그래서 뭘 바꿨나

일단 low-traffic window용 별도 alert를 추가했다. 요청 수 자체가 임계값 아래일 때는 error rate가 아니라 error count 기반으로 판단하는 룰.

- alert: PaymentApiLowTrafficErrors
  expr: |
    (
      sum(increase(http_requests_total{service="payment-api"}[10m])) < 2000
      and
      sum(increase(http_requests_total{service="payment-api",code=~"5.."}[10m])) > 20
    )
  for: 5m

10분 안에 요청이 2000개 미만인데 그 중 20개가 5xx면 알람. 이 숫자는 palm-of-hand로 정한 거라 아직 튜닝 중이다. 며칠 굴려보고 false positive가 심하면 조정할 예정이다.

두 번째로, fast burn alert의 short window를 5분에서 10분으로 늘렸다. 노이즈에 덜 흔들리도록. 그러면 detection이 조금 느려지긴 하는데, 애초에 새벽에 5분 window가 신호를 못 잡고 있었으니 실질적으로 손해가 아니었다.

세 번째로, error budget burn을 traffic 정규화 없이 raw error count의 절대 추세로도 보는 대시보드를 하나 새로 만들었다. 얼럿까지는 안 걸었지만 온콜 야간 점검 때 훑어보는 용도로. 이게 궁극적으로 필요한지는 아직 논의 중이다.

배운 것

SLO alert는 traffic profile에 종속적이다. 이 말이 워크북에는 짧게 한 줄로 나와있는데, 실제로 사고를 겪고 나니까 이게 얼마나 큰 함정인지 체감이 된다. 특히 하루 중에도 QPS가 100배 차이 나는 서비스라면, 낮 기준으로 세팅한 window 크기와 threshold가 새벽에는 완전히 다르게 동작한다.

또 하나, alert 룰을 짤 때 "정상 동작 시나리오"만 시뮬레이션했지 "실제로 알람이 울려야 하는 시나리오"를 재현해본 적이 없다는 걸 깨달았다. 지난 주부터는 pre-prod에서 traffic replay로 low-traffic 시간대 시뮬레이션 돌리는 걸 사이드 잡으로 하고 있다. 이 얘기는 정리되면 따로 써보려고 한다.

혹시 low-traffic 시간대의 SLO 알람을 다르게 접근하는 분 있으면 댓글로 알려주세요. 우리 팀도 이거 정답을 못 찾은 상태라 궁금하다.

BIG