Prometheus·Grafana·Micrometer로 SLO 모니터링 구축

· 약 13분 읽기 · 조회 1 #SLO#Prometheus#Grafana#Micrometer#Spring Boot#모니터링

SLO는 "얼마나 자주 실패해도 되는가"를 숫자로 정해 두는 약속이고, 에러 버짓은 그 약속에서 아직 쓸 수 있는 실패의 양이다. 숫자를 정해도 대시보드와 경보가 그 숫자를 계산하지 않으면 SLO는 문서에만 남는다. 이 글은 Spring Boot 애플리케이션에서 Micrometer로 요청 지표를 남기고, Prometheus로 SLI를 계산하고, 번 레이트(burn rate)로 경보를 걸고, Grafana에 남은 에러 버짓을 올리는 과정을 순서대로 정리한다.

조용한 관제실에서 운영 엔지니어가 아날로그 계기판이 가득한 벽을 지켜보고, 가장 큰 계기의 바늘이 붉은 구간 가까이 다가가 있는 장면

한눈에 보기

단계 하는 일 결과물
1. 정의 SLI·SLO·측정 기간을 정한다 "30일 동안 요청의 99.9%가 5xx 없이 응답" 같은 문장
2. 계측 Micrometer가 http.server.requests를 히스토그램으로 남긴다 /actuator/prometheus 지표
3. 수집 Prometheus가 주기적으로 긁어 간다(scrape) 시계열 저장
4. 계산 recording rule로 창(window)별 오류 비율을 미리 계산한다 job:slo_error_ratio:rate1h 같은 지표
5. 경보 짧은 창과 긴 창의 번 레이트가 함께 넘을 때만 알린다 Alertmanager 알림
6. 공개 Grafana에 남은 에러 버짓과 번 레이트를 올린다 팀이 같이 보는 대시보드

Spring Boot 애플리케이션의 Micrometer 지표를 Prometheus가 수집하고, recording rule로 SLI를 계산한 뒤 번 레이트 경보 규칙이 Alertmanager로 알림을 보내고 Grafana가 에러 버짓을 보여 주는 흐름

평균 응답 시간과 퍼센타일의 차이는 P95와 P99가 평균 응답 시간보다 중요한 이유에서 다뤘다. 이 글은 그 퍼센타일을 목표로 삼은 뒤, 목표를 얼마나 빨리 잃고 있는지 재는 쪽에 집중한다.

SLI·SLO·에러 버짓을 먼저 정한다

도구를 설치하기 전에 문장 세 개를 정한다.

  • SLI(지표): 좋은 이벤트 수 ÷ 전체 이벤트 수. 예를 들어 "5xx가 아닌 응답 수 ÷ 전체 응답 수"다. 비율로 정의해야 트래픽이 늘거나 줄어도 같은 기준으로 비교할 수 있다.
  • SLO(목표): SLI가 측정 기간 동안 지켜야 할 값. 예: "30일 동안 가용성 SLI 99.9% 이상", "30일 동안 요청의 99%가 300ms 안에 응답".
  • 에러 버짓: 1 − SLO. 99.9%라면 요청의 0.1%다. 예시로 30일 요청이 1,000만 건이면 1만 건까지 실패해도 목표는 지켜진다.

정할 때 붙일 조건

  • 측정 대상 요청을 좁힌다. 헬스체크(/actuator/**)와 정적 파일은 빼야 실제 사용자 경험이 SLI에 남는다.
  • 4xx를 실패로 셀지 정한다. 잘못된 입력에 대한 400은 서비스 실패가 아니므로 보통 5xx만 센다.
  • 가용성과 지연 시간은 SLO를 따로 둔다. 둘을 하나로 묶으면 어느 쪽 때문에 버짓이 줄었는지 알 수 없다.

Micrometer로 요청을 히스토그램으로 남긴다

Spring Boot Actuator는 Spring MVC·WebFlux 요청마다 http.server.requests 타이머를 기록한다. micrometer-registry-prometheus를 추가하면 이 타이머가 Prometheus 형식으로 /actuator/prometheus에 노출된다. 이름은 http_server_requests_seconds_count, _sum, _bucket으로 바뀌고 uri, method, status, outcome 같은 태그가 라벨로 붙는다.

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,prometheus
  server:
    port: 8081            # 지표는 내부 포트로만 연다
  metrics:
    distribution:
      slo:
        http.server.requests: 100ms,300ms,1s
      percentiles-histogram:
        http.server.requests: true
      minimum-expected-value:
        http.server.requests: 5ms
      maximum-expected-value:
        http.server.requests: 10s
  • slo는 지정한 경계값으로 히스토그램 버킷을 만든다. SLO 기준이 300ms라면 le="0.3" 버킷이 반드시 있어야 "300ms 안에 끝난 요청 수"를 정확히 셀 수 있다.
  • percentiles-histogram은 histogram_quantile로 P95·P99를 계산할 수 있도록 미리 정한 버킷을 추가한다. 버킷 하나가 시계열 하나이므로, minimum-expected-value·maximum-expected-value로 범위를 좁혀 시계열 수를 줄인다.
  • 지연 시간 SLO만 필요하고 퍼센타일 그래프가 필요 없다면 slo만 켜도 된다.

인스턴스별 퍼센타일은 합칠 수 없다. management.metrics.distribution.percentiles로 0.95, 0.99를 지정하면 Micrometer가 애플리케이션 안에서 퍼센타일을 계산해 quantile 라벨로 내보낸다. 이 값은 인스턴스마다 따로 계산된 결과라서, 서버 4대의 P99를 평균하거나 최댓값을 골라도 전체 P99가 되지 않는다. 버킷 카운터는 단순한 개수라 인스턴스끼리 더할 수 있고, 더한 히스토그램에서 퍼센타일을 계산하면 전체 값이 나온다. 그래서 SLO 계산에는 클라이언트 퍼센타일이 아니라 히스토그램 버킷을 쓴다.

Prometheus로 수집하고 SLI를 계산한다

수집 설정은 경로만 바꾸면 된다.

# prometheus.yml
scrape_configs:
  - job_name: order-api
    metrics_path: /actuator/prometheus
    scrape_interval: 15s
    static_configs:
      - targets: ['10.0.1.11:8081', '10.0.1.12:8081']

가용성 SLI는 전체 요청 중 5xx가 아닌 비율이다. Micrometer의 outcome 라벨은 5xx를 SERVER_ERROR로 표시한다.

# 최근 5분 오류 비율
sum(rate(http_server_requests_seconds_count{job="order-api", uri!~"/actuator.*", outcome="SERVER_ERROR"}[5m]))
/
sum(rate(http_server_requests_seconds_count{job="order-api", uri!~"/actuator.*"}[5m]))

지연 시간 SLI는 300ms 버킷에 들어온 요청 비율이다. 버킷은 누적값이라 le="0.3"이 "300ms 이하인 요청 수"다.

sum(rate(http_server_requests_seconds_bucket{job="order-api", uri!~"/actuator.*", le="0.3"}[5m]))
/
sum(rate(http_server_requests_seconds_count{job="order-api", uri!~"/actuator.*"}[5m]))

전체 P99 그래프가 필요하면 버킷을 먼저 더하고 계산한다. sum by (le)가 빠지면 인스턴스마다 따로 계산된다.

histogram_quantile(0.99, sum by (le) (rate(http_server_requests_seconds_bucket{job="order-api"}[5m])))

recording rule로 SLI를 고정한다. 같은 식을 대시보드와 경보가 각자 쓰면 조건이 조금씩 달라진다. recording rule로 한 번 계산해 이름을 붙이고, 모두 그 이름만 쓴다. 이름은 레벨:지표:연산 관례를 따른다.

# slo-rules.yml
groups:
  - name: order-api-slo
    rules:
      - record: job:slo_requests:rate5m
        expr: sum by (job) (rate(http_server_requests_seconds_count{job="order-api", uri!~"/actuator.*"}[5m]))
      - record: job:slo_errors:rate5m
        expr: sum by (job) (rate(http_server_requests_seconds_count{job="order-api", uri!~"/actuator.*", outcome="SERVER_ERROR"}[5m]))
      - record: job:slo_error_ratio:rate5m
        expr: job:slo_errors:rate5m / job:slo_requests:rate5m
      - record: job:slo_error_ratio:rate1h
        expr: |
          sum by (job) (rate(http_server_requests_seconds_count{job="order-api", uri!~"/actuator.*", outcome="SERVER_ERROR"}[1h]))
          /
          sum by (job) (rate(http_server_requests_seconds_count{job="order-api", uri!~"/actuator.*"}[1h]))
      # 30m, 6h 창도 같은 모양으로 추가한다

번 레이트로 경보를 건다

번 레이트는 에러 버짓을 쓰는 속도다. 오류 비율 ÷ (1 − SLO)로 계산한다. 번 레이트 1은 측정 기간이 끝날 때 버짓을 딱 다 쓰는 속도이고, 14.4는 그보다 14.4배 빠르다.

"오류율 1% 넘으면 알림" 같은 고정 임계값은 SLO와 연결되지 않는다. 짧게 튀는 오류에도 울리고, 오래 이어지는 낮은 오류는 놓친다. Google의 『The Site Reliability Workbook』 "Alerting on SLOs" 장은 긴 창과 짧은 창을 함께 보는 다중 창·다중 번 레이트 방식을 권한다.

심각도 긴 창 짧은 창 번 레이트 30일 버짓 소모
호출(page) 1시간 5분 14.4 2%
호출(page) 6시간 30분 6 5%
티켓 3일 6시간 1 10%

긴 창은 "충분히 많이 쓰고 있는가"를, 짧은 창은 "지금도 계속 쓰고 있는가"를 본다. 짧은 창 조건 덕분에 장애가 끝나면 경보도 곧 풀린다.

      - alert: OrderApiErrorBudgetBurnFast
        expr: |
          job:slo_error_ratio:rate1h{job="order-api"} > (14.4 * 0.001)
          and
          job:slo_error_ratio:rate5m{job="order-api"} > (14.4 * 0.001)
        labels:
          severity: page
        annotations:
          summary: "order-api 에러 버짓 소모 속도 14.4배 초과 (1h·5m)"

0.001은 99.9% SLO의 에러 버짓이다. 지연 시간 SLO도 같은 방식으로 "느린 요청 비율 = 1 − 300ms 이하 비율"을 번 레이트로 바꿔 경보를 건다.

Grafana에 에러 버짓을 올린다

대시보드는 "지금 괜찮은가"보다 "이번 기간에 얼마나 남았나"를 먼저 보여 준다.

올릴 패널

  • 남은 에러 버짓(Stat): 30일 오류 비율을 버짓으로 나눠 1에서 뺀다. 0% 아래로 내려가면 SLO를 어긴 것이다.
  • 번 레이트(Time series): 1시간·6시간 번 레이트를 그리고 14.4, 6, 1에 기준선을 둔다. 경보가 왜 울렸는지 같은 화면에서 확인할 수 있다.
  • SLI 추이(Time series): 가용성·300ms 이내 비율을 SLO 선과 함께 그린다.
  • 배포 표시(Annotation): 배포 시각을 겹쳐 두면 버짓이 줄어든 원인을 찾는 시간이 줄어든다.

남은 버짓은 5분 요청 수·오류 수 recording rule을 30일 동안 더해 계산한다. 비율을 평균하지 않고 개수를 더해야 트래픽이 많은 시간대가 더 큰 비중을 갖는다.

1 - (
  (sum_over_time(job:slo_errors:rate5m{job="order-api"}[30d])
   / sum_over_time(job:slo_requests:rate5m{job="order-api"}[30d]))
  / 0.001
)

구성 요소별 역할

구성 요소 누가 보나 없으면 생기는 일
SLO 문장(대상·기간·기준) 개발·운영·발주처 장애 때마다 "이 정도면 괜찮은가"를 새로 다툼
slo 히스토그램 버킷 개발 300ms 이내 비율을 근사값으로만 계산
recording rule 운영 대시보드와 경보의 계산식이 조금씩 달라짐
다중 창 번 레이트 경보 당직자 짧은 오류에 자주 울리고 느린 소모는 놓침
에러 버짓 대시보드 팀 전체, 기획 배포를 늦출지 기능을 낼지 근거 없이 결정

자주 하는 실수

  • 클라이언트 퍼센타일을 평균 낸다. 인스턴스별로 따로 계산된 값이라 합친 결과가 전체 퍼센타일이 아니다.
  • SLO 경계값과 버킷이 맞지 않는다. 300ms 버킷이 없으면 histogram_quantile이 구간 안을 선형 보간한 근사값만 준다.
  • 보관 기간이 측정 기간보다 짧다. Prometheus 기본 보관 기간은 15일이라 30일 버짓을 계산할 수 없다. --storage.tsdb.retention.time을 늘리거나 장기 저장소를 둔다.
  • 애플리케이션 지표만 본다. 앞단 L4·Reverse Proxy에서 끊긴 요청은 애플리케이션에 도달하지 않아 SLI에 잡히지 않는다. 앞단 로그나 외부 합성 모니터링으로 보완한다.
  • 사용자 ID 같은 값을 태그로 붙인다. 값마다 시계열이 생겨 Prometheus 메모리가 급격히 늘어난다.

점검표

  • SLI가 "좋은 이벤트 ÷ 전체 이벤트" 비율로 정의되어 있다
  • SLO 문장에 대상 요청, 측정 기간, 기준값이 모두 있다
  • 헬스체크와 정적 파일 요청이 SLI 계산에서 빠져 있다
  • /actuator/prometheus는 내부 포트로만 열려 있다
  • 지연 시간 SLO 기준값과 같은 le 버킷이 노출된다
  • 퍼센타일은 sum by (le)로 버킷을 합친 뒤 계산한다
  • 대시보드와 경보가 같은 recording rule 이름을 쓴다
  • 경보는 긴 창과 짧은 창의 번 레이트를 함께 본다
  • Prometheus 보관 기간이 SLO 측정 기간보다 길다
  • 남은 에러 버짓 패널이 팀 대시보드 맨 위에 있다

SLO 모니터링은 목표 숫자를 정하는 데서 끝나지 않고, 히스토그램으로 남긴 요청을 같은 계산식으로 SLI와 번 레이트로 바꿔 경보와 대시보드가 함께 쓰게 만들 때 비로소 운영 판단의 기준이 된다.

댓글 0개

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.

댓글 쓰기

← 목록으로 돌아가기