Skip to content
Published on

Prometheus PromQL高度なクエリとRecording Rulesの最適化:SLI / SLOベースの通知スキーム構築ガイド

Authors
  • Name
    Twitter

Prometheus PromQL Recording Rules

##入り

PrometheusはCNCFの卒業プロジェクトとしてKubernetesエコシステムの事実上標準的なメトリック収集システムです。小規模なクラスタでいくつかのrate()クエリを実行することは難しくありませんが、数百のマイクロサービスから数千万の時系列をカバーする実稼働環境では、PromQLクエリの設計と最適化がシステムの安定性に依存します。ダッシュボードのロードに30秒かかり、通知の評価が遅れて障害検出が遅くなり、PrometheusサーバーのCPUとメモリがクエリ負荷で飽和する状況は、一定規模以上の組織で必ず向き合うことになる問題だ。

この記事では、PromQL高度なクエリパターン(rate、histogram_quantile、predict_linear、subquery)の正しい使い方から、Recording Rulesを活用したクエリパフォーマンスの最適化、ネーミングコンベンションの設計、SLI定義と計算、SLOベースのマルチウィンドウマルチバーンレート通知スキーム、アラートマネージャールーティングディレクティブ、グループ化ケース、そして運営チェックリストまで扱う。

PromQL高度なクエリパターン

基本的な rate() と sum() by() を超えて、実稼働環境で実際に必要な高度なクエリパターンをクリーンアップします。

rate() と irate() の正しい選択

rate()は指定された時間範囲全体にわたる1秒あたりの平均変化率を計算し、irate()は最新の2つのデータポイント間の瞬間変化率を計算します。通知ルールでは rate() を使用する必要があります。 irate()はスパイクに敏感に反応するため、ダッシュボードの視覚化には便利ですが、通知に使用すると短い瞬間のノイズにも発火し、誤検出が急激に増加します。```promql

알림 규칙 - 반드시 rate() 사용

rate()는 범위 전체의 평균이므로 일시적 스파이크에 안정적

sum(rate(http_requests_total-1[5m])) by (service) / sum(rate(http_requests_total[5m])) by (service)

대시보드 시각화 - irate()로 실시간 반응성 확보

$__rate_interval은 Grafana가 scrape interval 기반으로 자동 계산

sum(irate(http_requests_total-1[__rate_interval])) by (service) / sum(irate(http_requests_total[__rate_interval])) by (service)

rate()의 range window 선택 기준

- scrape_interval의 최소 4배 이상 (15s interval이면 최소 [1m])

- 알림용: [5m] 이상 권장 (데이터 포인트 누락에 대한 내성 확보)

- 너무 넓으면 감지가 느려지고, 너무 좁으면 노이즈가 증가


### histogram_quantile() 深い活用

ヒストグラムベースのパーセンタイル計算はSLI定義の中心です。注意すべき点は、histogram_quantile()がバケット境界間を線形補間することです。バケット設計はクエリの精度に直接影響します。```promql
# 서비스별 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で最も一般的な間違いの1つなので、注意が必要です。

### predict_linear() を利用した容量予測

predict_linear()は、時系列データに単純な線形回帰を適用して、将来の時点の値を予測します。ディスク、メモリ、証明書の有効期限など、リソースの枯渇を事前に検出するための重要な機能です。```promql
# 디스크가 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()の入力レンジウィンドウが短すぎるとノイズに敏感になり、長すぎると最近の変化を反映できません。通常、予測期間の半分以上をレンジウィンドウに設定します。 24時間後を予測するには、少なくとも12時間のデータを参照するのが合理的です。

### Subqueryと高度な時間操作

Subqueryは、range vector関数の結果に再び時間範囲を適用する手法です。複雑な時系列解析には強力ですが、クエリコストが高いため、Recording Rulesで線計算することが望ましい。```promql
# 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サーバーの負荷軽減の3つの効果を同時に提供します。

### いつ Recording Rules が必要か

Recording Rulesを導入する必要がある時点の明確な基準が必要です。

1. **同じクエリが3箇所以上で繰り返し使用される場合。**ダッシュボード 複数のパネル、通知ルール、他のRecording Rulesなどで同じrate()式を繰り返し評価するとリソースの無駄だ。
2. **クエリ実行時間が2秒を超える場合。** Prometheus`/api/v1/query`エンドポイントでクエリの実行時間を確認できます。 2秒を超えるとダッシュボードのロードと通知の評価に体感できる遅延が発生する。
3. **高カーディナリティメトリックを集計する場合** 数十万の時系列を毎回リアルタイムで集計する代わりに、Recording Rulesで事前に集計しておくとクエリ時点の負荷が劇的に減少する。
4. **通知ルールで複雑な式を使用する場合**通知評価サイクル(基本1分)ごとに重いクエリが実行されると、Prometheusサーバーの信頼性に直接的な脅威となります。

### Recording Rules vs Raw Queries パフォーマンス比較

|比較項目| Raw Query(リアルタイム計算)| Recording Rule(事前計算)|
| ------------------- | --------------------------- | ------------------------------- |
|ダッシュボードの読み込み時間| 2〜30秒(時系列数に比例)| 50-200ms(シングル時系列照会)|
| Prometheus CPU負荷|クエリごとにフル時系列をスキャン|評価周期(30s-1m)ごとに1回計算
|通知評価の安定性クエリ遅延時の評価遅延可能事前計算された値参照で安定
|ストレージコスト追加料金なし|新しい時系列ストレージで少量増加|
|柔軟性すぐにクエリを変更できます。ルール変更後に反映するまでに時間が必要
|高カーディナリティ処理|毎回全体の時系列演算集計結果のみを保存して効率的
|適切な使用先検索クエリ、アドホック分析|ダッシュボード、通知、SLI / SLOの計算

### Recording Rulesの基本設定```yaml
# 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`は2つのサブRecording Rulesを参照しています。これにより、基本 rate() 計算が一度だけ実行され、複数の親ルールで再使用される。

## ネーミングコンベンション: level:metric:operation

Recording Rulesのネーミングは、Prometheusの公式ドキュメントで推奨されています`level:metric:operations`パターンに従う。このコンベンションは、ルールの意味を名前だけで把握できるようにする。

### ネーミングルール構造```
level:metric:operations

level      - 집계 수준 (어떤 레이블 차원이 남아있는가)
metric     - 원본 메트릭 이름
operations - 적용된 연산 (rate, ratio, p99 등)
```###本番ネーミングの例```yaml
# 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`はレート適用後に除去してもよい。
- operationへ`rate5m`、`ratio`、`p99`等適用された演算を具体的に記述する。
- コロン(`:`)はRecording Rulesでのみ使用され、元のメトリック名には絶対に使用されません。

## SLIの定義と計算

SLI(Service Level Indicator)は、サービス品質を定量的に測定する指標である。 「良いイベントの割合」で表され、0から1の値を持ちます。 SLIの定義が不正確な場合、SLOと通知システム全体が無意味になるため、この段階に最も時間を費やす必要があります。

### SLIタイプによるPromQLの定義```yaml
# 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の定義における一般的なミスと修正

1. **ヘルスチェックトラフィックをSLIに含めること。** Kubernetes liveness/readiness probeリクエストはユーザートラフィックではないため、除外する必要があります。`http_requests_total{handler!="/healthz", handler!="/readyz"}`フィルタを適用します。
2. **内部再試行を成功にカウントすること。**ユーザーが体感するのは最初の要求の結果だ。サーバー内部で3回再試行して成功したのは、ユーザーの観点から「遅い成功」だ。
3. **すべての4xxをエラーに分類すること** 404 Not Foundは、サーバーが正常に動作しながら返す応答です。 422 Validation Errorも同様です。サーバーの可用性問題と見られるのは429 Too Many Requests程度であり、これさえもrate limitingが意図的なら正常である。
4. **平均応答時間をSLIとして使用する。**平均はtail latencyを隠す。 P50が50msのサービスのP99が5秒になる可能性があります。 SLIは、「しきい値以内に応答した割合」と定義する必要があります。

## SLOベースのエラーバジェット通知

SLO(Service Level Objective)はSLIの目標レベルです。 「可用性SLIが30日間99.9%以上でなければならない」がSLOの典型的な形態だ。 SLOに基づく通知は、従来のスレッショルド通知よりも正確で、ビジネス影響度に直接リンクされています。

### Multi-Window Multi-Burn-Rate通知

Google SRE Workbookが推奨するこの通知方法は、「現在のエラー発生速度で進行し続けると、エラーバジェットがどれだけ早く使い果たされるか」に基づいて通知を決定します。 Burn rate は error budget 消耗速度の倍数を意味します。

| Burn Rate |バジェットの消耗時間(30日基準)|ロングウィンドウ|ショートウィンドウ| Severity |意味|
| --------- | ---------------------------- | ----------- | ------------ | -------- | --------------------------- |
| 14.4 | 2.08日| 1時間| 5m | critical |急性障害、即時対応
| 6 | 5日| 6時間| 30メートル| warning |深刻な性能低下
| 2 | 15日| 3d | 6時間|情報|穏やかな品質低下、週間レビュー|
| 1 | 30日| 30d | 3d |チケット|正常消耗速度、モニタリング

Multi-Windowの鍵は誤検知です。 Long windowだけを使用すると、すでに解消された過去の障害についても通知が発生する。ショートウィンドウをAND条件として追加し、「現在も問題が進行中か」を確認する。

###通知ルールの実装```yaml
# 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負荷を大幅に削減できます。```yaml
# 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の設定```yaml
# 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`にあまりにも多くのラベルを含めると通知が過度に分散して全体の状況を把握することが難しく、あまりにも少ないと関連のない通知が1つのメッセージに集まって読みやすくなります。`['service', 'slo_type', 'alertname']`組み合わせがほとんどの環境で適正なレベルである。`continue: true`設定は、1つの通知が複数の受信者に配信される必要がある場合に使用されます。重要な通知がPagerDutyとSlackの両方に渡される必要がある場合が代表的です。

##ゴーカーディナリティ対応

高カーディナリティ(High Cardinality)は、Prometheus運営で最も頻繁に向き合う性能問題の原因である。 1つのメトリックに対してラベルの組み合わせが数万を超えると、PrometheusサーバーのメモリとCPUが急激に増加します。

###カーディナリティ診断```promql
# 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を活用したカーディナリティの削減```yaml
# 고카디널리티 메트릭을 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サーバーのチューニング```yaml
# 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の違いは、パネル数が増えるほど劇的に広がる。```json
{
  "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秒)を超えています。そのグループ内のルールの数が多すぎるか、個々のルールのクエリが重いことが原因です。

**診断:**```promql
# 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
```**解決:**

1.重い規則を別々のグループに分け、そのグループの間隔を増やします(30秒または1メートル)。
2. 1つのルールであまりにも多くの時系列を参照する場合は、下位Recording Rulesを最初に作成して階層的に計算します。
3. ルールのクエリを最適化する。例えば`{__name__=~".+"}`同じ包括的なセレクタを特定のメトリック名に置き換えます。

### 症状 2: 通知が発火しない

SLI が SLO を下回ったが、バーンレート通知が発生しない場合。

**原因チェックリスト:**

1.**`for`durationを確認してください。**`for: 5m`は条件が5分連続満たされなければ通知がFIRING状態に切り替わるという意味だ。短いスパイクはPENDINGで解消することができます。
2. **burn rate thresholdの確認。** SLO 99.9%のburn rate 14.4のthresholdは`14.4 * 0.001 = 0.0144`だ。エラー率がこの数値に近いが、満たないと発火しない。
3. **時系列の有無を確認します。** トラフィックがまったくないサービスは rate()`NaN`を返すので、比較演算が成立しない。
4. **Alertmanager接続の確認。** Prometheus設定の`alerting.alertmanagers`セクションが正しく設定されていること、およびAlertmanagerが正常に動作していることを確認してください。```promql
# 현재 PENDING 상태인 알림 확인
ALERTS{alertstate="pending"}

# 현재 FIRING 상태인 알림 확인
ALERTS{alertstate="firing"}

# Alertmanager 전송 실패 확인
prometheus_notifications_errors_total
prometheus_notifications_dropped_total
```### 症状 3: Prometheus メモリ使用量の急増

**原因:**新しいRecording Rulesが追加された後に時系列数が急増した場合。 Recording Rulesは新しい時系列を生成するため、ルールの数と結果のカーディナリティに比例してメモリが増加します。

**診断:**```promql
# 시계열 총 수 추이
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サーバーに直接影響を与えるため、コード変更と同じレベルのレビュープロセスを実行する必要があります。

1. **PromQL文法検証.**`promtool check rules recording_rules.yaml`命令で文法エラーを事前に検出する。
2. **Unit Testの実行** promtoolはRecording RulesとAlerting Rulesのunit testをサポートします。
3. **結果カーディナリティ推定。**新しいルールが生成する時系列数を事前に計算し、Prometheusサーバーのメモリの余裕分を確認します。
4. **Canaryデプロイ。**可能であれば、複製されたPrometheusインスタンスに最初に適用して、評価時間とリソースの影響を確認します。```bash
# 규칙 파일 문법 검증
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の例```yaml
# 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 クラッシュ

**状況:**記録ルールAがルールBを参照し、ルールBがルールAを参照する循環参照が発生しました。 Prometheus 2.xではこれを明示的に拒否せず、評価時に無限ループに陥り、OOMとクラッシュしました。

**レッスン:**Recording Rules間の依存関係をDAG(Directed Acyclic Graph)で管理する必要があります。`promtool check rules`は循環参照を検出しないため、コードレビュー段階で依存関係を手動で確認するか、自動化スクリプトでDAG検証を実行する必要があります。

**回復手順:**

1. Prometheusをセーフモード(ルールファイルなし)に再起動します。
2. 循環参照を識別して除去する。
3.`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通知の誤検出による通知の疲労

**状況:**バーンレート通知の`for`durationを設定せず(デフォルト値0)、瞬間的なエラースパイクごとに通知が発火しました。 1日50件以上の通知が発生し、オンコールエンジニアがすべての通知を無視するようになり、実際の障害発生時に対応が遅れた。

**レッスン:** Critical通知には最小`for: 1m`を、Warningには`for: 5m`を設定します。 Multi-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 - 公式ドキュメント](https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/)
- [Prometheus Best Practices: Recording Rules](https://prometheus.io/docs/practices/rules/)
- [Google SRE Workbook - Alerting on SLOs](https://sre.google/workbook/alerting-on-slos/)
- [Prometheus Query Functions - 公式文書](https://prometheus.io/docs/prometheus/latest/querying/functions/)
- [Awesome Prometheus Alerts - コミュニティ通知ルールのコレクション] (https://samber.github.io/awesome-prometheus-alerts/)
- [Prometheus Alertmanager Configuration - 公式ドキュメント](https://prometheus.io/docs/alerting/latest/configuration/)
- [Google SRE Workbook - Error Budget Policy](https://sre.google/workbook/error-budget-policy/)
- [Prometheus Naming Best Practices](https://prometheus.io/docs/practices/naming/)