- Published on
Prometheus PromQL 고급 쿼리와 Recording Rules 최적화: SLI/SLO 기반 알림 체계 구축 가이드
- Authors
- Name
- 들어가며
- PromQL 고급 쿼리 패턴
- Recording Rules 설계 원칙
- 네이밍 컨벤션: level:metric:operation
- SLI 정의와 계산
- SLO 기반 에러 버짓 알림
- Alertmanager 라우팅과 그룹핑
- 고카디널리티 대응
- 성능 최적화
- 트러블슈팅
- 운영 시 주의사항
- 실패 사례와 복구
- 체크리스트
- 참고자료

들어가며
Prometheus는 CNCF 졸업 프로젝트로서 Kubernetes 생태계의 사실상 표준 메트릭 수집 시스템이다. 소규모 클러스터에서 몇 개의 rate() 쿼리를 실행하는 것은 어렵지 않지만, 수백 개의 마이크로서비스에서 수천만 개의 시계열을 다루는 프로덕션 환경에서는 PromQL 쿼리의 설계와 최적화가 시스템 안정성을 좌우한다. 대시보드 로딩에 30초가 걸리고, 알림 평가가 지연되어 장애 감지가 느려지며, Prometheus 서버의 CPU와 메모리가 쿼리 부하로 포화되는 상황은 일정 규모 이상의 조직에서 반드시 마주하게 되는 문제다.
이 글에서는 PromQL 고급 쿼리 패턴(rate, histogram_quantile, predict_linear, subquery)의 올바른 사용법부터, Recording Rules를 활용한 쿼리 성능 최적화, 네이밍 컨벤션 설계, SLI 정의와 계산, SLO 기반 Multi-Window Multi-Burn-Rate 알림 체계, Alertmanager 라우팅과 그룹핑 전략, 고카디널리티 대응, 실전 트러블슈팅 사례, 그리고 운영 체크리스트까지 다룬다.
PromQL 고급 쿼리 패턴
기본적인 rate()와 sum() by()를 넘어, 프로덕션 환경에서 실제로 필요한 고급 쿼리 패턴을 정리한다.
rate()와 irate()의 올바른 선택
rate()는 지정된 시간 범위 전체에 걸친 초당 평균 변화율을 계산하고, irate()는 가장 최근 두 데이터 포인트 간의 순간 변화율을 계산한다. 알림 규칙에서는 반드시 rate()를 사용해야 한다. irate()는 스파이크에 민감하게 반응하므로 대시보드 시각화에는 유용하지만, 알림에 사용하면 짧은 순간의 노이즈에도 발화하여 오탐이 급격히 증가한다.
# 알림 규칙 - 반드시 rate() 사용
# rate()는 범위 전체의 평균이므로 일시적 스파이크에 안정적
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
# 대시보드 시각화 - irate()로 실시간 반응성 확보
# $__rate_interval은 Grafana가 scrape interval 기반으로 자동 계산
sum(irate(http_requests_total{status=~"5.."}[$__rate_interval])) by (service)
/
sum(irate(http_requests_total[$__rate_interval])) by (service)
# rate()의 range window 선택 기준
# - scrape_interval의 최소 4배 이상 (15s interval이면 최소 [1m])
# - 알림용: [5m] 이상 권장 (데이터 포인트 누락에 대한 내성 확보)
# - 너무 넓으면 감지가 느려지고, 너무 좁으면 노이즈가 증가
range window를 scrape_interval보다 좁게 설정하면 빈 결과가 반환될 수 있다. Prometheus는 range window 내에 최소 두 개의 데이터 포인트가 있어야 rate()를 계산할 수 있기 때문이다. scrape_interval이 15초라면 최소 30초 이상의 window가 필요하지만, 실무에서는 누락 스크레이프를 감안하여 4배인 1분 이상을 사용한다.
histogram_quantile() 심층 활용
히스토그램 기반 퍼센타일 계산은 SLI 정의의 핵심이다. 주의할 점은 histogram_quantile()이 버킷 경계 사이를 선형 보간(linear interpolation)한다는 것이다. 버킷 설계가 쿼리 정확도에 직접적인 영향을 미친다.
# 서비스별 P99 응답 시간
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)
# 엔드포인트별 P95 응답 시간 (상위 레이블 추가)
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service, endpoint)
)
# Apdex Score 계산 (만족: 0.5s 이하, 허용: 2s 이하)
(
sum(rate(http_request_duration_seconds_bucket{le="0.5"}[5m])) by (service)
+
sum(rate(http_request_duration_seconds_bucket{le="2.0"}[5m])) by (service)
)
/ 2
/
sum(rate(http_request_duration_seconds_count[5m])) by (service)
# Native Histogram (Prometheus 2.53+) 활용
# 버킷 경계를 자동 관리하여 정확도와 효율성 모두 향상
histogram_quantile(0.99, sum(rate(http_request_duration_seconds[5m])) by (le, service))
histogram_quantile()에서 by (le) 절을 누락하면 le 레이블이 집계되어 의미 없는 결과가 반환된다. 이것은 PromQL에서 가장 흔한 실수 중 하나이므로 반드시 주의해야 한다.
predict_linear()을 활용한 용량 예측
predict_linear()은 시계열 데이터에 단순 선형 회귀를 적용하여 미래 시점의 값을 예측한다. 디스크, 메모리, 인증서 만료 등 리소스 고갈을 사전에 감지하는 데 핵심적인 함수다.
# 디스크가 24시간 내에 고갈될 것으로 예측되면 알림
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 24 * 3600) < 0
# PVC 용량 예측 (Kubernetes)
predict_linear(
kubelet_volume_stats_available_bytes[12h], 7 * 24 * 3600
) < 0
# 인증서 만료 예측 (cert-manager)
# 현재 남은 시간이 30일(2592000초) 미만이면 알림
(x509_cert_not_after - time()) < 2592000
# Prometheus TSDB 스토리지 증가율 예측
predict_linear(prometheus_tsdb_storage_blocks_bytes[7d], 30 * 24 * 3600)
> prometheus_tsdb_retention_limit_bytes * 0.9
predict_linear()의 입력 range window가 너무 짧으면 노이즈에 민감해지고, 너무 길면 최근 변화를 반영하지 못한다. 일반적으로 예측 기간의 절반 이상을 range window로 설정한다. 24시간 후를 예측하려면 최소 12시간의 데이터를 참조하는 것이 합리적이다.
Subquery와 고급 시간 조작
Subquery는 range vector 함수의 결과에 대해 다시 시간 범위를 적용하는 기법이다. 복잡한 시계열 분석에 강력하지만, 쿼리 비용이 높으므로 Recording Rules로 선계산하는 것이 바람직하다.
# Subquery: 지난 1시간 동안의 5분 에러율 최대값
max_over_time(
(
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
)[1h:1m]
)
# offset을 활용한 전주 대비 트래픽 비교
sum(rate(http_requests_total[5m])) by (service)
/
sum(rate(http_requests_total[5m] offset 7d)) by (service)
# label_replace()로 레이블 가공
label_replace(
up{job="prometheus"},
"cluster", "$1", "instance", "(.+)\\.example\\.com:.*"
)
# absent()로 메트릭 수집 중단 감지
absent(up{job="payment-service"} == 1)
Recording Rules 설계 원칙
Recording Rules는 자주 사용하는 PromQL 표현식의 결과를 새로운 시계열로 미리 계산하여 저장하는 메커니즘이다. 대시보드 로딩 속도 개선, 알림 평가 안정화, Prometheus 서버 부하 감소의 세 가지 효과를 동시에 제공한다.
언제 Recording Rules가 필요한가
Recording Rules를 도입해야 하는 시점에 대한 명확한 기준이 필요하다.
- 동일 쿼리가 3곳 이상에서 반복 사용될 때. 대시보드 여러 패널, 알림 규칙, 다른 Recording Rules 등에서 같은 rate() 표현식을 반복 평가하면 리소스 낭비다.
- 쿼리 실행 시간이 2초를 초과할 때. Prometheus의
/api/v1/query엔드포인트에서 쿼리 실행 시간을 확인할 수 있다. 2초를 넘으면 대시보드 로딩과 알림 평가에 체감할 수 있는 지연이 발생한다. - 고카디널리티 메트릭을 집계할 때. 수십만 개의 시계열을 매번 실시간으로 집계하는 대신, Recording Rules로 미리 집계해두면 쿼리 시점의 부하가 극적으로 감소한다.
- 알림 규칙에서 복잡한 표현식을 사용할 때. 알림 평가 주기(기본 1분)마다 무거운 쿼리가 실행되면 Prometheus 서버 안정성에 직접적인 위협이 된다.
Recording Rules vs Raw Queries 성능 비교
| 비교 항목 | Raw Query (실시간 계산) | Recording Rule (사전 계산) |
|---|---|---|
| 대시보드 로딩 시간 | 2-30초 (시계열 수에 비례) | 50-200ms (단일 시계열 조회) |
| Prometheus CPU 부하 | 쿼리마다 전체 시계열 스캔 | 평가 주기(30s-1m)마다 1회 계산 |
| 알림 평가 안정성 | 쿼리 지연 시 평가 지연 가능 | 사전 계산된 값 참조로 안정적 |
| 스토리지 비용 | 추가 비용 없음 | 새 시계열 저장으로 소량 증가 |
| 유연성 | 즉시 쿼리 수정 가능 | 규칙 변경 후 반영까지 시간 필요 |
| 고카디널리티 처리 | 매번 전체 시계열 연산 | 집계된 결과만 저장하여 효율적 |
| 적합한 사용처 | 탐색적 쿼리, ad-hoc 분석 | 대시보드, 알림, SLI/SLO 계산 |
Recording Rules 기본 구성
# prometheus/recording_rules.yaml
groups:
# 그룹명은 논리적 단위로 구분 (파일당 1-3개 그룹이 적정)
- name: http_request_rates
# interval: 평가 주기 (생략 시 global.evaluation_interval 사용)
# SLI용 Recording Rules는 30초 이하 권장
interval: 30s
rules:
# 서비스별 초당 총 요청 수
- record: service:http_requests:rate5m
expr: |
sum(rate(http_requests_total[5m])) by (service)
# 서비스별 초당 에러 요청 수
- record: service:http_requests_errors:rate5m
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
# 서비스별 에러율
- record: service:http_error_rate:ratio_rate5m
expr: |
service:http_requests_errors:rate5m
/
service:http_requests:rate5m
# 서비스별 P99 응답 시간
- record: service:http_request_duration_seconds:p99_rate5m
expr: |
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)
# 서비스별 P95 응답 시간
- record: service:http_request_duration_seconds:p95_rate5m
expr: |
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)
Recording Rules는 계층적으로 참조할 수 있다. 위 예시에서 service:http_error_rate:ratio_rate5m은 두 개의 하위 Recording Rules를 참조한다. 이렇게 하면 기본 rate() 계산이 한 번만 실행되고, 여러 상위 규칙에서 재사용된다.
네이밍 컨벤션: level:metric:operation
Recording Rules의 네이밍은 Prometheus 공식 문서에서 권장하는 level:metric:operations 패턴을 따른다. 이 컨벤션은 규칙의 의미를 이름만으로 파악할 수 있게 해준다.
네이밍 규칙 구조
level:metric:operations
level - 집계 수준 (어떤 레이블 차원이 남아있는가)
metric - 원본 메트릭 이름
operations - 적용된 연산 (rate, ratio, p99 등)
실전 네이밍 예시
# level = "job" (job 레이블 기준 집계)
# metric = "http_requests" (원본: http_requests_total)
# operation = "rate5m" (5분 rate 적용)
- record: job:http_requests:rate5m
expr: sum(rate(http_requests_total[5m])) by (job)
# level = "cluster" (클러스터 전체 집계, 레이블 없음)
# metric = "http_requests"
# operation = "rate5m"
- record: cluster:http_requests:rate5m
expr: sum(rate(http_requests_total[5m]))
# level = "service" + "endpoint"
# metric = "http_request_duration_seconds"
# operation = "p99_rate5m"
- record: service_endpoint:http_request_duration_seconds:p99_rate5m
expr: |
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service, endpoint)
)
# SLI용 Recording Rule
# level = "service"
# metric = "sli_availability"
# operation = "ratio_rate5m"
- record: service:sli_availability:ratio_rate5m
expr: |
sum(rate(http_requests_total{status!~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
# 복합 연산: ratio(비율)를 나타내는 Recording Rule
# "ratio"가 operation에 포함되면 0-1 범위의 비율 값임을 명시
- record: instance:node_cpu:utilization_ratio
expr: |
1 - avg without(cpu, mode)(
rate(node_cpu_seconds_total{mode="idle"}[5m])
)
네이밍 시 주의사항:
- level에는 남아있는 레이블의 차원을 표기한다.
by (service)집계라면service:,by (job, instance)라면job_instance:형태로 쓴다. - metric에는
_total,_bytes,_seconds같은 타입 접미사를 유지하되, Counter의_total은 rate 적용 후 제거해도 된다. - operation에
rate5m,ratio,p99등 적용된 연산을 구체적으로 기술한다. - 콜론(
:)은 Recording Rules에서만 사용하고, 원본 메트릭 이름에는 절대 사용하지 않는다.
SLI 정의와 계산
SLI(Service Level Indicator)는 서비스 품질을 정량적으로 측정하는 지표다. "좋은 이벤트의 비율"로 표현하며, 0에서 1 사이의 값을 갖는다. SLI 정의가 부정확하면 SLO와 알림 체계 전체가 무의미해지므로, 이 단계에 가장 많은 시간을 투자해야 한다.
SLI 유형별 PromQL 정의
# prometheus/sli_recording_rules.yaml
groups:
- name: sli_definitions
interval: 30s
rules:
# === 가용성(Availability) SLI ===
# "좋은 요청" = 5xx가 아닌 모든 응답
# 4xx는 클라이언트 오류이므로 서버 가용성에서 제외하지 않음
- record: service:sli_availability:ratio_rate5m
expr: |
sum(rate(http_requests_total{status!~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
# === 지연시간(Latency) SLI ===
# "좋은 요청" = 300ms 이내에 응답된 요청
- record: service:sli_latency:ratio_rate5m
expr: |
sum(rate(http_request_duration_seconds_bucket{le="0.3"}[5m])) by (service)
/
sum(rate(http_request_duration_seconds_count[5m])) by (service)
# === 복합 SLI ===
# "좋은 요청" = 5xx가 아니고 AND 300ms 이내에 응답
# 가용성과 지연시간 SLI의 교집합
- record: service:sli_combined:ratio_rate5m
expr: |
min without() (
service:sli_availability:ratio_rate5m,
service:sli_latency:ratio_rate5m
)
# === 30일 Rolling Window SLI ===
# Error Budget 계산의 기준이 되는 장기 SLI
- record: service:sli_availability:ratio_rate30d
expr: |
sum(rate(http_requests_total{status!~"5.."}[30d])) by (service)
/
sum(rate(http_requests_total[30d])) by (service)
# === Error Budget 잔량 (퍼센트) ===
# SLO 99.9% 기준, 남은 error budget 비율
- record: service:error_budget_remaining:ratio
expr: |
1 - (
(1 - service:sli_availability:ratio_rate30d)
/
(1 - 0.999)
)
SLI 정의 시 흔한 실수와 교정
- 헬스체크 트래픽을 SLI에 포함하는 것. Kubernetes liveness/readiness probe 요청은 사용자 트래픽이 아니므로 제외해야 한다.
http_requests_total{handler!="/healthz", handler!="/readyz"}필터를 적용한다. - 내부 재시도를 성공으로 카운트하는 것. 사용자가 체감하는 것은 최초 요청의 결과다. 서버 내부에서 3번 재시도해서 성공한 것은 사용자 관점에서 "느린 성공"이다.
- 모든 4xx를 에러로 분류하는 것. 404 Not Found는 서버가 정상 동작하면서 반환하는 응답이다. 422 Validation Error도 마찬가지다. 서버의 가용성 문제로 볼 수 있는 것은 429 Too Many Requests 정도이며, 이마저도 rate limiting이 의도적이라면 정상이다.
- 평균 응답 시간을 SLI로 사용하는 것. 평균은 tail latency를 숨긴다. P50이 50ms인 서비스의 P99가 5초일 수 있다. SLI는 반드시 "threshold 이내에 응답한 비율"로 정의해야 한다.
SLO 기반 에러 버짓 알림
SLO(Service Level Objective)는 SLI에 대한 목표 수준이다. "가용성 SLI가 30일 동안 99.9% 이상이어야 한다"가 SLO의 전형적인 형태다. SLO를 기반으로 한 알림은 전통적인 threshold 알림보다 정밀하고, 비즈니스 영향도와 직접 연결된다.
Multi-Window Multi-Burn-Rate 알림
Google SRE Workbook에서 권장하는 이 알림 방식은 "현재 에러 발생 속도로 계속 진행되면, error budget이 얼마나 빨리 소진되는가"를 기준으로 알림을 결정한다. Burn rate는 error budget 소진 속도의 배수를 의미한다.
| Burn Rate | Budget 소진 시간 (30일 기준) | Long Window | Short Window | Severity | 의미 |
|---|---|---|---|---|---|
| 14.4 | 2.08일 | 1h | 5m | critical | 급성 장애, 즉시 대응 |
| 6 | 5일 | 6h | 30m | warning | 심각한 성능 저하 |
| 2 | 15일 | 3d | 6h | info | 완만한 품질 저하, 주간 리뷰 |
| 1 | 30일 | 30d | 3d | ticket | 정상 소진 속도, 모니터링 |
Multi-Window의 핵심은 오탐 방지다. Long window만 사용하면 이미 해소된 과거 장애에 대해서도 알림이 발생한다. Short window를 AND 조건으로 추가하여 "현재도 문제가 진행 중인가"를 확인한다.
알림 규칙 구현
# prometheus/slo_alerting_rules.yaml
groups:
- name: slo_burn_rate_alerts
rules:
# ============================================================
# SLO: 가용성 99.9% (30일 윈도우)
# Error Budget = 0.1% = 43.2분/30일
# ============================================================
# --- Critical: Burn Rate 14.4, 1h/5m 윈도우 ---
# 이 속도면 약 2일 만에 월간 error budget 전량 소진
- alert: SLOAvailabilityBurnRateCritical
expr: |
(
1 - (sum(rate(http_requests_total{status!~"5.."}[1h])) by (service)
/ sum(rate(http_requests_total[1h])) by (service))
) > (14.4 * 0.001)
and
(
1 - (sum(rate(http_requests_total{status!~"5.."}[5m])) by (service)
/ sum(rate(http_requests_total[5m])) by (service))
) > (14.4 * 0.001)
for: 2m
labels:
severity: critical
slo_type: availability
burn_rate: '14.4'
alert_window: '1h/5m'
annotations:
summary: >-
{{ $labels.service }}: 가용성 SLO burn rate critical (14.4x)
description: >-
서비스 {{ $labels.service }}의 에러율이 SLO(99.9%) 대비
14.4배 속도로 error budget을 소진 중입니다.
약 2일 내에 전체 월간 budget이 소진됩니다. 즉시 확인하세요.
runbook_url: https://wiki.internal/runbook/slo-critical
dashboard_url: >-
https://grafana.internal/d/slo-overview?var-service={{ $labels.service }}
# --- Warning: Burn Rate 6, 6h/30m 윈도우 ---
- alert: SLOAvailabilityBurnRateHigh
expr: |
(
1 - (sum(rate(http_requests_total{status!~"5.."}[6h])) by (service)
/ sum(rate(http_requests_total[6h])) by (service))
) > (6 * 0.001)
and
(
1 - (sum(rate(http_requests_total{status!~"5.."}[30m])) by (service)
/ sum(rate(http_requests_total[30m])) by (service))
) > (6 * 0.001)
for: 5m
labels:
severity: warning
slo_type: availability
burn_rate: '6'
alert_window: '6h/30m'
annotations:
summary: >-
{{ $labels.service }}: 가용성 SLO burn rate high (6x)
description: >-
서비스 {{ $labels.service }}의 에러율이 SLO(99.9%) 대비
6배 속도로 error budget을 소진 중입니다.
약 5일 내에 전체 budget이 소진됩니다.
# --- Info: Burn Rate 2, 3d/6h 윈도우 ---
- alert: SLOAvailabilityBurnRateSlow
expr: |
(
1 - (sum(rate(http_requests_total{status!~"5.."}[3d])) by (service)
/ sum(rate(http_requests_total[3d])) by (service))
) > (2 * 0.001)
and
(
1 - (sum(rate(http_requests_total{status!~"5.."}[6h])) by (service)
/ sum(rate(http_requests_total[6h])) by (service))
) > (2 * 0.001)
for: 30m
labels:
severity: info
slo_type: availability
burn_rate: '2'
alert_window: '3d/6h'
annotations:
summary: >-
{{ $labels.service }}: 가용성 SLO burn rate elevated (2x)
description: >-
서비스 {{ $labels.service }}의 에러율이 SLO 대비
2배 속도로 error budget을 소진 중입니다.
약 15일 내에 budget이 소진되며, 주간 리뷰에서 확인이 필요합니다.
# ============================================================
# SLO: 지연시간 (P99 < 300ms) 99.9% (30일 윈도우)
# ============================================================
- alert: SLOLatencyBurnRateCritical
expr: |
(
1 - (sum(rate(http_request_duration_seconds_bucket{le="0.3"}[1h])) by (service)
/ sum(rate(http_request_duration_seconds_count[1h])) by (service))
) > (14.4 * 0.001)
and
(
1 - (sum(rate(http_request_duration_seconds_bucket{le="0.3"}[5m])) by (service)
/ sum(rate(http_request_duration_seconds_count[5m])) by (service))
) > (14.4 * 0.001)
for: 2m
labels:
severity: critical
slo_type: latency
burn_rate: '14.4'
annotations:
summary: >-
{{ $labels.service }}: 지연시간 SLO burn rate critical (14.4x)
description: >-
서비스 {{ $labels.service }}의 P99 응답시간이 300ms SLO를 위반하는
비율이 14.4배 속도로 error budget을 소진 중입니다.
Recording Rules로 알림 규칙 최적화
위의 알림 규칙들은 동일한 rate() 계산을 여러 번 반복한다. Recording Rules로 중간 결과를 사전 계산하면 Prometheus 부하를 크게 줄일 수 있다.
# prometheus/slo_recording_rules.yaml
groups:
- name: slo_error_rates
interval: 30s
rules:
# 각 윈도우별 에러율을 Recording Rule로 사전 계산
- record: service:http_error_rate:ratio_rate5m
expr: |
1 - (sum(rate(http_requests_total{status!~"5.."}[5m])) by (service)
/ sum(rate(http_requests_total[5m])) by (service))
- record: service:http_error_rate:ratio_rate30m
expr: |
1 - (sum(rate(http_requests_total{status!~"5.."}[30m])) by (service)
/ sum(rate(http_requests_total[30m])) by (service))
- record: service:http_error_rate:ratio_rate1h
expr: |
1 - (sum(rate(http_requests_total{status!~"5.."}[1h])) by (service)
/ sum(rate(http_requests_total[1h])) by (service))
- record: service:http_error_rate:ratio_rate6h
expr: |
1 - (sum(rate(http_requests_total{status!~"5.."}[6h])) by (service)
/ sum(rate(http_requests_total[6h])) by (service))
- record: service:http_error_rate:ratio_rate3d
expr: |
1 - (sum(rate(http_requests_total{status!~"5.."}[3d])) by (service)
/ sum(rate(http_requests_total[3d])) by (service))
- name: slo_alerts_optimized
rules:
# Recording Rules를 참조하여 알림 규칙을 간결하고 효율적으로 작성
- alert: SLOAvailabilityBurnRateCritical
expr: |
service:http_error_rate:ratio_rate1h > (14.4 * 0.001)
and
service:http_error_rate:ratio_rate5m > (14.4 * 0.001)
for: 2m
labels:
severity: critical
slo_type: availability
burn_rate: '14.4'
- alert: SLOAvailabilityBurnRateHigh
expr: |
service:http_error_rate:ratio_rate6h > (6 * 0.001)
and
service:http_error_rate:ratio_rate30m > (6 * 0.001)
for: 5m
labels:
severity: warning
slo_type: availability
burn_rate: '6'
Alertmanager 라우팅과 그룹핑
Alertmanager는 Prometheus에서 발생한 알림을 수신하여 중복 제거, 그룹핑, 라우팅, 사일런싱을 수행한 뒤 적절한 수신자에게 전달한다. SLO 기반 알림 체계에서는 severity와 서비스 소유권에 따른 정밀한 라우팅이 핵심이다.
Alertmanager 설정
# alertmanager/alertmanager.yml
global:
resolve_timeout: 5m
slack_api_url: 'https://hooks.slack.com/services/T00/B00/XXXX'
# 알림 수신 시 라우팅 트리를 순회하며 매칭되는 수신자에게 전달
route:
# 기본 그룹핑: 서비스와 SLO 타입별로 묶음
group_by: ['service', 'slo_type', 'alertname']
# 첫 알림 대기 시간 (동일 그룹의 알림을 모아서 발송)
group_wait: 30s
# 같은 그룹에 새 알림 추가 시 재발송 간격
group_interval: 5m
# 동일 알림 반복 발송 간격
repeat_interval: 4h
# 기본 수신자
receiver: 'slack-default'
routes:
# Critical: PagerDuty + Slack (즉시 대응 필요)
- match:
severity: critical
receiver: 'pagerduty-critical'
group_wait: 10s
repeat_interval: 1h
continue: true # 다음 라우트도 계속 평가
- match:
severity: critical
receiver: 'slack-critical'
group_wait: 10s
# Warning: Slack 전용 채널 (업무 시간 내 대응)
- match:
severity: warning
receiver: 'slack-warning'
repeat_interval: 8h
# Info: 주간 다이제스트로 수집 (즉시 대응 불필요)
- match:
severity: info
receiver: 'slack-info'
repeat_interval: 24h
# 특정 서비스를 담당 팀 채널로 라우팅
- match_re:
service: 'payment-.*'
receiver: 'slack-payment-team'
routes:
- match:
severity: critical
receiver: 'pagerduty-payment'
# Inhibition: 상위 severity 알림이 활성 상태면 하위 알림 억제
inhibit_rules:
- source_match:
severity: critical
target_match:
severity: warning
equal: ['service', 'slo_type']
- source_match:
severity: critical
target_match:
severity: info
equal: ['service', 'slo_type']
receivers:
- name: 'pagerduty-critical'
pagerduty_configs:
- service_key: '<PAGERDUTY_SERVICE_KEY>'
severity: critical
description: '{{ .GroupLabels.service }}: {{ .CommonAnnotations.summary }}'
details:
firing: '{{ .Alerts.Firing | len }}'
dashboard: '{{ (index .Alerts 0).Annotations.dashboard_url }}'
runbook: '{{ (index .Alerts 0).Annotations.runbook_url }}'
- name: 'slack-critical'
slack_configs:
- channel: '#alerts-critical'
send_resolved: true
title: '{{ .Status | toUpper }}: {{ .GroupLabels.service }}'
text: >-
{{ range .Alerts }}
*{{ .Annotations.summary }}*
{{ .Annotations.description }}
{{ end }}
- name: 'slack-warning'
slack_configs:
- channel: '#alerts-warning'
send_resolved: true
- name: 'slack-info'
slack_configs:
- channel: '#alerts-info'
send_resolved: false
- name: 'slack-default'
slack_configs:
- channel: '#alerts-default'
- name: 'slack-payment-team'
slack_configs:
- channel: '#team-payment-alerts'
- name: 'pagerduty-payment'
pagerduty_configs:
- service_key: '<PAYMENT_PAGERDUTY_KEY>'
그룹핑 전략의 핵심
group_by에 너무 많은 레이블을 포함하면 알림이 과도하게 분산되어 전체 상황 파악이 어렵고, 너무 적으면 관련 없는 알림이 한 메시지에 뭉쳐서 가독성이 떨어진다. ['service', 'slo_type', 'alertname'] 조합이 대부분의 환경에서 적정한 수준이다.
continue: true 설정은 하나의 알림이 여러 수신자에게 전달되어야 할 때 사용한다. critical 알림이 PagerDuty와 Slack 모두로 전달되어야 하는 경우가 대표적이다.
고카디널리티 대응
고카디널리티(High Cardinality)는 Prometheus 운영에서 가장 빈번하게 마주하는 성능 문제의 원인이다. 하나의 메트릭에 대해 레이블 조합이 수만 개를 넘으면 Prometheus 서버의 메모리와 CPU가 급격히 증가한다.
카디널리티 진단
# TSDB 상태 확인 - 시계열 수가 가장 많은 메트릭 Top 10
topk(10, count by (__name__)({__name__=~".+"}))
# 특정 메트릭의 레이블별 카디널리티 확인
count(http_requests_total) by (method)
count(http_requests_total) by (status)
count(http_requests_total) by (handler)
# 전체 시계열 수 추이 (급증하면 문제의 징후)
prometheus_tsdb_head_series
# 메모리 사용량 추이
process_resident_memory_bytes{job="prometheus"}
고카디널리티 레이블의 전형적 사례
피해야 할 레이블: user_id, request_id, trace_id, session_id, ip_address, url_path(비정규화된 전체 경로). 이들은 고유 값이 무한에 가까우므로 메트릭 레이블로 적합하지 않다. 대신 트레이스 시스템(Jaeger, Tempo)이나 로그 시스템(Loki)에서 처리해야 한다.
Recording Rules를 활용한 카디널리티 감소
# 고카디널리티 메트릭을 Recording Rules로 집계하여 카디널리티 감소
groups:
- name: cardinality_reduction
interval: 1m
rules:
# handler별로 수천 개의 시계열을 service 단위로 집계
# handler 레이블을 제거하여 카디널리티를 1/100로 감소
- record: service:http_requests:rate5m
expr: |
sum(rate(http_requests_total[5m])) by (service, method, status)
# endpoint별 세분화된 히스토그램을 service 단위로 집계
- record: service:http_request_duration_seconds_bucket:rate5m
expr: |
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
Prometheus 자체의 메모리 사용량을 줄이는 것이 아니라(원본 시계열은 여전히 수집됨), 쿼리 시점의 연산 부하를 줄이는 것이 Recording Rules의 역할이다. 원본 메트릭의 카디널리티를 줄이려면 relabel_configs에서 불필요한 레이블을 수집 시점에 드롭하거나, 애플리케이션 코드에서 레이블을 정리해야 한다.
성능 최적화
Prometheus 서버 튜닝
# prometheus.yml - 성능 관련 설정
global:
scrape_interval: 15s
evaluation_interval: 15s
# query_log_file로 느린 쿼리를 기록하여 최적화 대상 식별
query_log_file: /prometheus/query.log
# Recording Rules 파일 로드
rule_files:
- /etc/prometheus/recording_rules/*.yaml
- /etc/prometheus/alerting_rules/*.yaml
# 쿼리 엔진 설정 (Prometheus 2.x 커맨드 라인 플래그)
# --query.max-concurrency=20 동시 쿼리 수 제한
# --query.timeout=2m 단일 쿼리 타임아웃
# --query.max-samples=50000000 단일 쿼리 최대 샘플 수
# --storage.tsdb.retention.time=30d 데이터 보존 기간
# --storage.tsdb.retention.size=100GB 데이터 보존 크기 제한
Grafana 대시보드 최적화를 위한 Recording Rules 활용
대시보드에서 Recording Rules를 활용한 쿼리와 Raw Query의 차이는 패널 수가 늘어날수록 극적으로 벌어진다.
{
"dashboard": {
"title": "SLO Overview Dashboard",
"panels": [
{
"title": "Service Availability (SLI)",
"type": "gauge",
"targets": [
{
"expr": "service:sli_availability:ratio_rate30d",
"legendFormat": "{{ service }}"
}
],
"fieldConfig": {
"defaults": {
"thresholds": {
"steps": [
{ "value": 0, "color": "red" },
{ "value": 0.995, "color": "yellow" },
{ "value": 0.999, "color": "green" }
]
},
"min": 0.99,
"max": 1,
"unit": "percentunit"
}
}
},
{
"title": "Error Budget Remaining",
"type": "timeseries",
"targets": [
{
"expr": "service:error_budget_remaining:ratio * 100",
"legendFormat": "{{ service }}"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"thresholds": {
"steps": [
{ "value": 0, "color": "red" },
{ "value": 20, "color": "orange" },
{ "value": 50, "color": "green" }
]
}
}
}
},
{
"title": "P99 Latency by Service",
"type": "timeseries",
"targets": [
{
"expr": "service:http_request_duration_seconds:p99_rate5m",
"legendFormat": "{{ service }}"
}
],
"fieldConfig": {
"defaults": {
"unit": "s"
}
}
}
]
}
}
트러블슈팅
증상 1: Recording Rules 평가 지연 또는 실패
Prometheus 로그에 rule group took longer than evaluation interval이 반복적으로 출력되는 경우.
원인: Recording Rules 그룹의 평가 시간이 evaluation_interval(기본 15초)을 초과한다. 해당 그룹 내의 규칙 수가 너무 많거나, 개별 규칙의 쿼리가 무거운 것이 원인이다.
진단:
# Recording Rules 평가 지연 확인
prometheus_rule_group_duration_seconds{rule_group="slo_error_rates"}
# 평가 시간이 interval을 초과한 그룹 식별
prometheus_rule_group_duration_seconds > 15
# 규칙별 평가 실패 횟수
prometheus_rule_evaluation_failures_total
# 마지막 평가 시간 확인
prometheus_rule_group_last_duration_seconds
해결:
- 무거운 규칙을 별도 그룹으로 분리하고, 해당 그룹의 interval을 늘린다(30s 또는 1m).
- 하나의 규칙에서 너무 많은 시계열을 참조하는 경우, 하위 Recording Rules를 먼저 만들어 계층적으로 계산한다.
- 규칙의 쿼리를 최적화한다. 예를 들어
{__name__=~".+"}같은 포괄적 셀렉터를 구체적인 메트릭 이름으로 교체한다.
증상 2: 알림이 발화되지 않음
SLI가 SLO 아래로 떨어졌지만 burn rate 알림이 발생하지 않는 경우.
원인 체크리스트:
forduration 확인.for: 5m은 조건이 5분 연속 충족되어야 알림이 FIRING 상태로 전환된다는 뜻이다. 짧은 스파이크는 PENDING에서 해소될 수 있다.- burn rate threshold 확인. SLO 99.9%에 대한 burn rate 14.4의 threshold는
14.4 * 0.001 = 0.0144다. 에러율이 이 수치에 근접하지만 미달하면 발화하지 않는다. - 시계열 존재 여부 확인. 트래픽이 전혀 없는 서비스는 rate()가
NaN을 반환하므로 비교 연산이 성립하지 않는다. - Alertmanager 연결 확인. Prometheus 설정의
alerting.alertmanagers섹션이 올바르게 구성되어 있는지, Alertmanager가 정상 동작하는지 확인한다.
# 현재 PENDING 상태인 알림 확인
ALERTS{alertstate="pending"}
# 현재 FIRING 상태인 알림 확인
ALERTS{alertstate="firing"}
# Alertmanager 전송 실패 확인
prometheus_notifications_errors_total
prometheus_notifications_dropped_total
증상 3: Prometheus 메모리 사용량 급증
원인: 새로운 Recording Rules가 추가된 후 시계열 수가 급증한 경우. Recording Rules는 새로운 시계열을 생성하므로, 규칙 수와 결과 카디널리티에 비례하여 메모리가 증가한다.
진단:
# 시계열 총 수 추이
prometheus_tsdb_head_series
# Recording Rules가 생성한 시계열 수
count({__name__=~".+:.+"})
# 메모리 사용량
process_resident_memory_bytes{job="prometheus"} / 1024 / 1024 / 1024
해결: Recording Rules의 결과 시계열 수를 사전에 추정한다. sum() by (service) 규칙이 50개 서비스를 대상으로 하면 50개의 새 시계열이 생성된다. 5개 윈도우에 대해 각각 규칙을 만들면 250개다. 규모를 예측하고, 불필요한 세분화를 줄여야 한다.
운영 시 주의사항
Recording Rules 배포 절차
Recording Rules의 변경은 Prometheus 서버에 직접적인 영향을 미치므로, 코드 변경과 동일한 수준의 리뷰 프로세스를 거쳐야 한다.
- PromQL 문법 검증.
promtool check rules recording_rules.yaml명령으로 문법 오류를 사전에 검출한다. - Unit Test 실행. promtool은 Recording Rules와 Alerting Rules에 대한 unit test를 지원한다.
- 결과 카디널리티 추정. 새 규칙이 생성할 시계열 수를 사전에 계산하고, Prometheus 서버의 메모리 여유분을 확인한다.
- Canary 배포. 가능하다면 복제된 Prometheus 인스턴스에 먼저 적용하여 평가 시간과 리소스 영향을 확인한다.
# 규칙 파일 문법 검증
promtool check rules /etc/prometheus/recording_rules/slo_rules.yaml
# Unit Test 실행
promtool test rules /etc/prometheus/tests/slo_rules_test.yaml
# 설정 전체 검증
promtool check config /etc/prometheus/prometheus.yml
promtool Unit Test 예시
# tests/slo_rules_test.yaml
rule_files:
- ../recording_rules/slo_rules.yaml
- ../alerting_rules/slo_alerts.yaml
evaluation_interval: 1m
tests:
# Recording Rule 출력 값 검증
- interval: 1m
input_series:
- series: 'http_requests_total{service="api", status="200"}'
values: '0+100x10' # 분당 100씩 증가 (10분)
- series: 'http_requests_total{service="api", status="500"}'
values: '0+1x10' # 분당 1씩 증가 (10분)
promql_expr_test:
- expr: service:sli_availability:ratio_rate5m{service="api"}
eval_time: 10m
exp_samples:
- labels: 'service:sli_availability:ratio_rate5m{service="api"}'
value: 0.9901 # 100/(100+1) 근사값
alert_rule_test:
- eval_time: 10m
alertname: SLOAvailabilityBurnRateCritical
exp_alerts: [] # 이 수준의 에러율로는 critical 알림 발화 안 함
실패 사례와 복구
사례 1: Recording Rules 순환 참조로 Prometheus 크래시
상황: Recording Rule A가 Rule B를 참조하고, Rule B가 Rule A를 참조하는 순환 참조가 발생했다. Prometheus 2.x에서는 이를 명시적으로 거부하지 않고, 평가 시 무한 루프에 빠져 OOM으로 크래시되었다.
교훈: Recording Rules 간의 의존 관계를 DAG(Directed Acyclic Graph)로 관리해야 한다. promtool check rules는 순환 참조를 감지하지 못하므로, 코드 리뷰 단계에서 의존 관계를 수동으로 확인하거나, 자동화 스크립트로 DAG 검증을 수행해야 한다.
복구 절차:
- Prometheus를 안전 모드(규칙 파일 없이)로 재시작한다.
- 순환 참조를 식별하고 제거한다.
promtool check rules로 문법을 재검증한 뒤 규칙 파일을 복원한다.
사례 2: 과도한 Recording Rules로 TSDB WAL 폭발
상황: 한 번에 2,000개의 Recording Rules를 추가한 후, 각 규칙이 평균 500개의 시계열을 생성하여 총 100만 개의 새 시계열이 추가되었다. TSDB의 WAL(Write-Ahead Log)이 급격히 증가하고, 컴팩션이 따라가지 못해 디스크가 가득 찼다.
교훈: Recording Rules를 대량으로 추가할 때는 반드시 배치 단위로 점진적으로 적용해야 한다. 한 번에 추가하는 규칙 수를 100개 이하로 제한하고, 각 배치 적용 후 prometheus_tsdb_head_series와 디스크 사용량을 확인한다.
사례 3: SLO 알림 오탐으로 인한 알림 피로
상황: burn rate 알림의 for duration을 설정하지 않아(기본값 0), 순간적인 에러 스파이크마다 알림이 발화되었다. 하루 50건 이상의 알림이 발생하여 온콜 엔지니어가 모든 알림을 무시하게 되었고, 실제 장애 발생 시 대응이 지연되었다.
교훈: Critical 알림에는 최소 for: 1m을, Warning에는 for: 5m을 설정한다. Multi-Window의 short window가 오탐 방지 역할을 하지만, for duration은 추가적인 안전장치로 반드시 설정해야 한다.
체크리스트
Recording Rules 설계 체크리스트
- 네이밍 컨벤션(level:metric:operations)을 준수하는가
- 동일 rate() 계산이 3곳 이상에서 반복되면 Recording Rule로 추출했는가
- Recording Rules 간 의존 관계가 순환 참조 없이 DAG를 형성하는가
- 새 규칙이 생성할 시계열 수를 사전에 추정했는가
- evaluation_interval 내에 모든 규칙의 평가가 완료되는가
- promtool check rules로 문법 검증을 통과하는가
- Unit Test를 작성했는가
SLI/SLO 체크리스트
- SLI가 사용자 관점에서 정의되어 있는가 (서버 관점이 아닌)
- 헬스체크 트래픽이 SLI 계산에서 제외되어 있는가
- SLO 목표가 현재 서비스 수준에 비해 현실적인가
- Error Budget 잔량을 계산하는 Recording Rule이 있는가
- Multi-Window Multi-Burn-Rate 알림이 구성되어 있는가 (최소 2개 티어)
- 알림에 runbook_url과 dashboard_url annotation이 포함되어 있는가
- Error Budget Policy가 문서화되고 팀 합의가 이루어졌는가
Alertmanager 체크리스트
- severity별 라우팅이 구성되어 있는가 (critical, warning, info)
- Critical 알림은 PagerDuty 등 즉시 알림 채널로 라우팅되는가
- Inhibition rules로 하위 severity 중복 알림이 억제되는가
- group_by 설정이 적절한가 (너무 세분화되거나 너무 포괄적이지 않은가)
- repeat_interval이 severity에 따라 차등 설정되어 있는가
- 서비스 소유 팀별 라우팅이 구성되어 있는가
운영 체크리스트
- Prometheus의 query.log를 활성화하여 느린 쿼리를 모니터링하는가
- prometheus_rule_group_duration_seconds를 모니터링하는가
- prometheus_tsdb_head_series의 급증을 모니터링하는가
- Recording Rules 변경 시 코드 리뷰 프로세스를 거치는가
- promtool unit test가 CI 파이프라인에 포함되어 있는가
- 분기별 SLO 목표 리뷰와 Error Budget Policy 리뷰가 예정되어 있는가
참고자료
- Prometheus Recording Rules - 공식 문서
- Prometheus Best Practices: Recording Rules
- Google SRE Workbook - Alerting on SLOs
- Prometheus Query Functions - 공식 문서
- Awesome Prometheus Alerts - 커뮤니티 알림 규칙 모음
- Prometheus Alertmanager Configuration - 공식 문서
- Google SRE Workbook - Error Budget Policy
- Prometheus Naming Best Practices