Sloth
SLO 명세 하나에서 Prometheus 규칙과 burn-rate 알림을 생성하는 도구
SLO는 정하기보다 쓸 만한 알림으로 바꾸는 쪽이 더 성가시다. Sloth는 짧은 SLO 명세 하나에서 Prometheus recording rule과 multi-burn-rate 알림 규칙 전부를 생성해, 팀마다 제각각으로 손코딩하던 PromQL을 없앤다.
Intro
알림을 “에러율이 5%를 넘으면 울려라”로 짜 본 적이 있다면, 그게 왜 잘 안 되는지도 알 것이다.
임계값을 낮게 잡으면 잠깐 튄 스파이크에도 새벽에 깨고, 높게 잡으면 서비스가 천천히 망가지는 동안 아무도 모른다.
그리고 이 임계값은 서비스마다, 팀마다 다르게 박힌다.
SRE가 이 문제에 내놓은 답이 error budget과 burn rate 기반 알림이다.
“지금 에러율이 얼마냐”가 아니라 “허용된 실패 예산을 얼마나 빠르게 태우고 있느냐”를 본다.
문제는 이 방식이 개념은 명쾌한데 PromQL로 옮기면 꽤 성가시다는 점이다.
창을 여러 개 겹쳐야 하고, recording rule을 창마다 만들어야 하고, burn rate 배수를 알림마다 계산해 넣어야 한다.
Sloth는 이 반복을 걷어낸다.
SLO 목표와 SLI 쿼리만 적으면, 나머지 규칙 전부를 표준 형태로 생성해 준다.
개념부터 짚고 Sloth가 무엇을 대신 써 주는지 따라가 보자.
SLI, SLO, Error Budget
- SLI(Service Level Indicator): 서비스가 얼마나 잘 동작하는지를 나타내는 지표. 보통 비율이다. 예를 들어
정상 응답 수 / 전체 응답 수. - SLO(Service Level Objective): 그 SLI가 일정 기간 지켜야 할 목표. 예를 들어 “30일 동안 99.9%”.
- Error budget:
1 − SLO. 목표가 99.9%면 예산은 0.1%다. 30일간 요청의 0.1%까지는 실패해도 좋다는 허가서다.
error budget이라는 관점이 중요한 이유는, 실패를 0으로 만드는 게 목표가 아니라고 못 박기 때문이다.
0.1%라는 예산이 있고, 알림은 “에러가 났다”가 아니라 “이 예산을 위험한 속도로 쓰고 있다”에서 울려야 한다.
Burn Rate
burn rate는 예산을 태우는 속도다.
burn rate 1은 예산을 기간 전체에 걸쳐 딱 맞게 소진하는 속도다.
99.9% SLO라면 30일 내내 에러율이 정확히 0.1%로 유지되는 상태가 burn rate 1이다.
burn rate가 n이면 예산을 n배 빠르게 태운다.
숫자로 보면 감이 온다. 99.9% SLO 기준으로,
| burn rate | 실제 에러율 | 예산 소진까지 |
|---|---|---|
| 1 | 0.1% | 30일 |
| 10 | 1% | 3일 |
| 1000 | 100% | 약 43분 |
여기서 알림 설계의 방향이 갈린다. burn rate가 높을수록 사고는 급하고, 급한 사고일수록 빨리 울려야 한다. 그러니 알림은 하나의 임계값이 아니라 burn rate 구간별로 여러 개여야 한다. 빠르게 타면 즉시 호출(page), 느리게 새면 티켓으로 남긴다.
Multiwindow Multi-Burn-Rate
그런데 burn rate 하나만 보면 또 다른 문제가 생긴다. 짧은 창(예: 5분)으로만 재면 반응은 빠르지만 잠깐의 스파이크에 오탐이 잦고, 긴 창(예: 1시간)으로만 재면 정확하지만 사고가 끝난 뒤에도 한참을 계속 운다.
Google SRE 워크북이 권하는 해법이 multiwindow multi-burn-rate(MWMB)다. 긴 창과 짧은 창을 짝지어 둘 다 임계를 넘을 때만 울린다. 긴 창이 정밀도를 책임지고, 짧은 창이 사고가 끝나면 알림을 빠르게 내린다.
Sloth가 생성하는 알림은 이 짝을 네 쌍 깐다.
| 심각도 | burn rate | 긴 창 | 짧은 창 | 의미 |
|---|---|---|---|---|
| Page | 14.4 | 1시간 | 5분 | 1시간 만에 예산 2%를 태우는 속도 |
| Page | 6 | 6시간 | 30분 | 6시간에 예산 5% |
| Ticket | 3 | 1일 | 2시간 | 하루에 예산 10% |
| Ticket | 1 | 3일 | 6시간 | 사흘에 걸친 완만한 누수 |
위 두 쌍은 급해서 사람을 호출하고, 아래 두 쌍은 티켓으로 쌓아 근무 시간에 처리한다. 14.4 같은 숫자는 마법이 아니라 계산값이다. 30일 예산의 2%를 1시간에 태우려면 0.02 × (30일 / 1시간) = 0.02 × 720 = 14.4가 나온다. 이 산수를 알림마다 손으로 넣는 게 딱 Sloth가 대신하는 일이다.
Sloth Spec
Sloth에 주는 입력은 이게 전부다.
version: "prometheus/v1"
service: "myservice"
labels:
owner: "myteam"
slos:
- name: "requests-availability"
objective: 99.9 # 목표 백분율
description: "HTTP 응답 가용성"
sli:
events:
error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}]))
total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}]))
alerting:
name: "MyServiceHighErrorRate"
page_alert:
labels: { severity: "pageteam" }
ticket_alert:
labels: { severity: "slack", slack_channel: "#alerts-myteam" }
적는 것은 세 가지다. objective로 목표를, sli.events로 “실패는 무엇이고 전체는 무엇인지”를, alerting으로 page와 ticket이 어디로 갈지를 정한다. {{.window}}는 Sloth가 창마다 알아서 채운다. SLI를 이벤트 비율 대신 이미 계산된 비율로 주고 싶으면 sli.raw.error_ratio_query를 쓴다.
Generated Rules
이 명세 하나에서 Sloth가 뽑아내는 규칙은 세 부류다.
첫째, SLI recording rule을 창별로 만든다. 5분, 30분, 1시간, 2시간, 6시간, 1일, 3일, 그리고 30일 집계까지 여러 창의 에러 비율을 미리 계산해 둔다.
- record: slo:sli_error:ratio_rate5m
expr: |
(sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[5m])))
/
(sum(rate(http_request_duration_seconds_count{job="myservice"}[5m])))
labels: { sloth_service: myservice, sloth_slo: requests-availability, sloth_window: 5m }
둘째, 메타데이터 rule을 만든다. slo:objective:ratio(0.999), slo:error_budget:ratio(0.001), 남은 예산, 현재 burn rate 같은 값을 상수 또는 파생 지표로 노출해 대시보드에서 바로 쓸 수 있게 한다. Sloth 저장소가 제공하는 공식 Grafana 대시보드가 이 지표들을 그대로 소비한다. SLI 추이, 남은 error budget, 현재 burn rate까지, 아래 화면 전체가 명세 한 장에서 나온다.

셋째, MWMB 알림 rule이다. 앞의 네 쌍이 그대로 PromQL로 펼쳐진다.
- alert: MyServiceHighErrorRate
expr: |
(
max(slo:sli_error:ratio_rate5m{...} > (14.4 * 0.001)) without (sloth_window)
and
max(slo:sli_error:ratio_rate1h{...} > (14.4 * 0.001)) without (sloth_window)
)
or
(
max(slo:sli_error:ratio_rate30m{...} > (6 * 0.001)) without (sloth_window)
and
max(slo:sli_error:ratio_rate6h{...} > (6 * 0.001)) without (sloth_window)
)
labels: { sloth_severity: page, severity: pageteam }
읽는 법은 이렇다. 0.001은 error budget(1 − 0.999)이고, 14.4 * 0.001은 1.44%라는 에러율 임계다. 5분 창과 1시간 창이 둘 다 이 임계를 넘어야 첫 번째 조건이 참이 된다. 짧은 창 하나가 스파이크로 튀어도, 긴 창이 아직 안 넘었으면 울리지 않는다. ticket 알림도 모양이 같고 2h & 1d(배수 3), 6h & 3d(배수 1) 쌍을 쓴다.
이 서른 몇 줄을 SLO마다 손으로 쓰고, objective를 99.9에서 99.95로 바꿀 때마다 모든 임계를 다시 계산하는 상황을 떠올려 보면, Sloth가 없애는 반복이 무엇인지 분명해진다.
Two Run Modes
Sloth는 두 방식으로 돈다.
CLI로 쓰면 GitOps 파이프라인에 얹기 좋다. sloth generate -i spec.yml -o rules.yml로 명세를 표준 Prometheus 규칙 파일로 변환하고, sloth validate를 CI에 걸어 잘못된 명세를 병합 전에 걸러낸다. 생성물은 그냥 rule YAML이라 기존 Prometheus 배포에 그대로 얹힌다.
Kubernetes 컨트롤러로 쓰면 클러스터 안에서 PrometheusServiceLevel CRD(sloth.slok.dev/v1)를 감시하다가, 새 명세가 올라오면 prometheus-operator의 PrometheusRules 객체로 계속 reconcile한다. 이때 명세 키는 camelCase로 바뀐다(errorQuery, pageAlert). 팀이 SLO를 그냥 매니페스트로 커밋하면 규칙이 자동으로 따라 붙는 형태다.
References
- Sloth — 공식 사이트
https://sloth.dev/ - slok/sloth (GitHub)
https://github.com/slok/sloth - Google SRE Workbook — Alerting on SLOs
https://sre.google/workbook/alerting-on-slos/ - Pyrra (GitHub)
https://github.com/pyrra-dev/pyrra - OpenSLO
https://openslo.com/