blog

DeNAのエンジニアが考えていることや、担当しているサービスについて情報発信しています

2025.10.21 技術記事

Grafana,Prometheus,Slothで実現するSLI/SLO管理入門:Slothの裏側を探るPromQL解読ガイド [DeNA インフラ SRE]

by yuto.ota

#infrastructure #sre #grafana #prometheus #sloth

はじめに

サービスの信頼性を定量的に評価し、管理するためのSLI/SLO。その実現方法には、高機能なSaaS製品を利用する方法もあれば、PrometheusやGrafanaといったオープンソースソフトウェア(OSS)を組み合わせて構築する方法もあります。特に、コスト面の制約や、既にGrafanaなどの監視基盤が稼働している環境では、後者を選択するケースも少なくありません。

しかし、PrometheusとGrafanaでSLI/SLO管理を実現しようとすると、一つの大きな壁に突き当たります。それは、エラーバジェットの計算などに必要となるPromQLをどう書けば良いのかわからないということです。

この課題を解決するツールが「Sloth」です。Slothは、私たちがYAMLでSLI/SLOを定義するだけで、SLI計測に必要なPromQLやPrometheusのルールを自動生成してくれます。これにより、SLI/SLO管理導入のハードルを下げることができます。

https://sloth.dev/

この記事では、単にSlothの使い方を解説するだけではありません。まずGrafanaダッシュボードでどのような可視化が実現できるかを示し、そのダッシュボードを構成するPromQLを解読することで、裏側にあるSlothの仕組みを丁寧に読み解いていきます。

この記事を読み終える頃には、Slothをブラックボックスなツールとして使う以上に、PromQLによるSLI/SLO管理の考え方そのものを理解できるようになることを目指します。

1. SLI/SLO運用の基本と可視化のポイント

本編に入る前に、まずは前提知識となるSLI/SLO運用の基本概念と、モニタリングダッシュボードで何を可視化すべきか、という基本的な考え方を整理します。

結論から言うと、SLI/SLO運用では、主要なものとして「SLI」「SLO」「エラーバジェット」という3つの指標を可視化する必要があるでしょう。

1.1. SLI, SLO, エラーバジェットとは?

まずは、3つの基本用語をおさらいしましょう。

  • SLI (Service Level Indicator / サービスレベル指標)
    • サービスの信頼性を定量的に測るための指標です。例えば、「リクエスト成功率」や「リクエストの応答時間が200ms未満だった割合」などがこれにあたります。
  • SLO (Service Level Objective / サービスレベル目標)
    • SLIが特定の期間において達成すべき目標値です。例えば、「月間のリクエスト成功率を99.9%に保つ」といった具体的な目標を指します。
  • エラーバジェット (Error Budget)
    • SLOを100%から引いた残り、つまり「許容されるエラーの量(予算)」です。SLOが99.9%の場合、エラーバジェットは0.1%になります。開発チームはこの予算の範囲内で、新機能のリリースや意欲的な挑戦を行うことができます。この予算内であれば、サービスはSLOを満たしていると判断されます。

1.2. ダッシュボードで可視化すべき3つの基本指標

これらの概念について、モニタリングダッシュボードではそれぞれ以下のようなグラフになります。

SLI/SLOの可視化 エラーバジェットの可視化

  1. SLI (サービスレベル)
    • 目的: サービスの信頼性が現在どのような状態にあるか、その推移を把握します。
    • 見るもの: 時々刻々と変化するサービスのパフォーマンスを示す曲線グラフです。
  2. SLO (サービスレベル目標)
    • 目的: SLIが目指すべき目標ラインを示します。
    • 見るもの: SLIグラフ上に引かれた一本の直線(例: 99.9%のライン)です。SLIの曲線がこのラインを上回っているかを確認できます。
  3. エラーバジェット残量
    • 目的: SLOの評価期間(例:月間)を通して、目標達成までの余裕がどれだけ残っているかを判断します。
    • 見るもの: 期間全体でのエラーバジェットの消費と残りを示す曲線グラフです。

この記事のゴールは、この「SLI」「SLO」「エラーバジェット残量」という3つの基本指標を、Slothを使ってGrafana上に可視化し、その裏側で動いているPromQLを理解することです。この点を念頭に、次の章からSlothの使い方を見ていきましょう。

2. SlothによるSLI/SLO管理の全体像

この章では、Slothを使ったSLI/SLO管理がどのような流れで実現されるのか、その全体像を解説します。Slothの役割を理解する上で重要なのが、Prometheusの「Recording Rule」という機能です。

結論から言うと、Slothの核心的な役割は、私たちが書いたYAML定義から、SLI/SLO計算に特化したPrometheus Recording Ruleを自動生成することです。いわば、Slothは複雑なRecording Ruleを作るためのヘルパーツールなのです。

そこでまず「Recording Ruleとは何か」を解説し、その上で、Slothを使ってGrafanaでSLI/SLOを可視化するまでの全体の流れを見ていきます。

2.1. Prometheus Recording Ruleとは

Recording Ruleとは、頻繁に使うクエリや実行に負荷のかかる重いクエリの結果を、新しいメトリクスとして事前に計算・保存しておくPrometheusの標準機能です。

https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/

これにより、Grafanaからクエリが発行されるたびに重い計算を実行するのではなく、事前計算された軽量なメトリクスを呼び出すだけで済みます。

  • 主なメリット:
    • 表示の高速化: 事前に計算済みのメトリクスを呼び出すため、ダッシュボードの表示速度が向上します。
    • 再利用性と可読性の向上: クエリを共通化・抽象化できるため、クエリの管理が容易になります。
  • 注意点:
    • ストレージ使用量の増加: 新しい時系列データが作られるため、Prometheusのストレージ消費量は増加します。
    • 過去データへの不適用: ルールの計算式を変更した場合、その変更は未来のデータにしか適用されません。過去のデータに遡って反映させたい場合は、手動でのバックフィル作業が必要になります。

2.2. Slothを使ったSLI/SLO管理の全体フロー

Recording Ruleが何かを理解した上で、改めてSlothを使ったSLI/SLO管理の全体像を見ていきましょう。

フローを図にまとめると以下のようになります。

Slothを使ったSLI/SLO管理の全体フロー

各ステップの役割は次の通りです。

  1. YAML定義 (私たちが行うこと):
    • サービスのSLI/SLOを、人間が読みやすいYAMLファイルに定義します。
  2. Rule生成 (Slothの役割):
    • slothコマンドがYAMLファイルを読み込み、PrometheusのためのRecording Ruleファイル(*.yml)を自動生成します。
  3. メトリクスの事前計算 (Prometheusの役割):
    • Prometheusは、Slothによって生成されたRuleファイルに従い、SLI計算の元となるメトリクス(エラー数や総リクエスト数など)を定期的に事前計算し、新しいメトリクスとして保存します。
  4. 可視化 (Grafanaの役割):
    • Grafanaは、Prometheusによって事前計算された軽量なメトリクスをクエリし、SLIやエラーバジェットのグラフを高速に描画します。

このように、SlothはSLI/SLO管理において「PromQLの作成」と「Recording Ruleの定義」という作業を肩代わりしてくれるのです。このフローを念頭に、次の章ではまずYAML定義からダッシュボードの表示までを一気通貫で見ていき、その上でダッシュボードの裏側を解読していきます。

3. Slothを使ってみる:YAML定義からGrafana可視化まで

この章では、まずSlothの基本的な使い方として、YAML定義からコマンドを実行し、GrafanaダッシュボードをインポートしてSLI/SLOが表示されるまでの一連の流れを解説します。

題材として、Web APIの「リクエスト成功率」を取り上げます。Prometheus上には、http_request_count_total というカウンター型のメトリクスが既に存在することを前提とします。

3.1. STEP1: SLI/SLOをYAMLで定義する

まず、私たちがやるべきことは、サービスのSLOをYAMLファイルに宣言的に定義することです。ここでは例として、特定のAPIパスへのリクエスト成功率を「30日間で99.9%」とするSLOを定義してみましょう。

具体的なYAML定義は以下のようになります。

version: "prometheus/v1"
service: "my_service"
slos:
  - name: "my_slo"
    description: "30日間の評価期間において、リクエストの99.9%が成功する"
    objective: 99.9
    sli:
      events:
        # 「悪いイベント」(5xxエラー)を取得するクエリ
        error_query: sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST", status=~"5.."}[{{.window}}])) by (service)
        # 「全てのイベント」(全リクエスト)を取得するクエリ
        total_query: sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST"}[{{.window}}])) by (service)

SLIを計算するために、error_query(悪いイベント)とtotal_query(全体のイベント)の2つのPromQLを定義します。

  • http_request_count_total{...}
    • これはアプリケーションが起動してから今までのリクエスト総数を記録し続ける、単調増加するカウンターです。この生の数値だけでは「今リクエストが多いのか少ないのか」すら判断できません。
  • rate( ... [{{.window}}])
    • rate()関数は、カウンターの「変化率」、つまり指定した時間範囲({{.window}})における1秒あたりの平均増加量を計算します。これにより、「秒間5.2リクエスト」といったデータに変換されます。
  • sum( ... ) by (service)
    • サービスが複数のサーバー(インスタンス)で稼働している場合、rate() の結果はインスタンスごとに別々のデータとして得られます。sum() を使うことで、それらを service ラベルで集約し、サービス全体での秒間リクエスト数を算出します。

また、[{{.window}}] という部分はSlothのテンプレート変数です。後のステップでRecording Ruleが生成される際に、ここへ 5m1h30d といった時間範囲が挿入されます。

3.2. STEP2: Recording Ruleを生成する

作成したYAMLファイル(例: my_service_slo.yml)をsloth generateコマンドに渡すだけで、Prometheusが解釈できるRecording Ruleファイルが標準出力に生成されます。

sloth generate -i ./my_service_slo.yml > slo_generated_rules.yml
slo_generated_rules.yml

---
# Code generated by Sloth (v0.12.0): https://github.com/slok/sloth.
# DO NOT EDIT.

groups:
- name: sloth-slo-sli-recordings-my_service-my_slo
  rules:
  - record: slo:sli_error:ratio_rate5m
    expr: |
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST", status=~"5.."}[5m])) by (service))
      /
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST"}[5m])) by (service))
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
      sloth_window: 5m
  - record: slo:sli_error:ratio_rate30m
    expr: |
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST", status=~"5.."}[30m])) by (service))
      /
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST"}[30m])) by (service))
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
      sloth_window: 30m
  - record: slo:sli_error:ratio_rate1h
    expr: |
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST", status=~"5.."}[1h])) by (service))
      /
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST"}[1h])) by (service))
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
      sloth_window: 1h
  - record: slo:sli_error:ratio_rate2h
    expr: |
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST", status=~"5.."}[2h])) by (service))
      /
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST"}[2h])) by (service))
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
      sloth_window: 2h
  - record: slo:sli_error:ratio_rate6h
    expr: |
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST", status=~"5.."}[6h])) by (service))
      /
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST"}[6h])) by (service))
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
      sloth_window: 6h
  - record: slo:sli_error:ratio_rate1d
    expr: |
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST", status=~"5.."}[1d])) by (service))
      /
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST"}[1d])) by (service))
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
      sloth_window: 1d
  - record: slo:sli_error:ratio_rate3d
    expr: |
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST", status=~"5.."}[3d])) by (service))
      /
      (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST"}[3d])) by (service))
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
      sloth_window: 3d
  - record: slo:sli_error:ratio_rate30d
    expr: |
      sum_over_time(slo:sli_error:ratio_rate5m{sloth_id="my_service-my_slo", sloth_service="my_service", sloth_slo="my_slo"}[30d])
      / ignoring (sloth_window)
      count_over_time(slo:sli_error:ratio_rate5m{sloth_id="my_service-my_slo", sloth_service="my_service", sloth_slo="my_slo"}[30d])
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
      sloth_window: 30d
- name: sloth-slo-meta-recordings-my_service-my_slo
  rules:
  - record: slo:objective:ratio
    expr: vector(0.9990000000000001)
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
  - record: slo:error_budget:ratio
    expr: vector(1-0.9990000000000001)
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
  - record: slo:time_period:days
    expr: vector(30)
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
  - record: slo:current_burn_rate:ratio
    expr: |
      slo:sli_error:ratio_rate5m{sloth_id="my_service-my_slo", sloth_service="my_service", sloth_slo="my_slo"}
      / on(sloth_id, sloth_slo, sloth_service) group_left
      slo:error_budget:ratio{sloth_id="my_service-my_slo", sloth_service="my_service", sloth_slo="my_slo"}
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
  - record: slo:period_burn_rate:ratio
    expr: |
      slo:sli_error:ratio_rate30d{sloth_id="my_service-my_slo", sloth_service="my_service", sloth_slo="my_slo"}
      / on(sloth_id, sloth_slo, sloth_service) group_left
      slo:error_budget:ratio{sloth_id="my_service-my_slo", sloth_service="my_service", sloth_slo="my_slo"}
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
  - record: slo:period_error_budget_remaining:ratio
    expr: 1 - slo:period_burn_rate:ratio{sloth_id="my_service-my_slo", sloth_service="my_service",
      sloth_slo="my_slo"}
    labels:
      sloth_id: my_service-my_slo
      sloth_service: my_service
      sloth_slo: my_slo
  - record: sloth_slo_info
    expr: vector(1)
    labels:
      sloth_id: my_service-my_slo
      sloth_mode: cli-gen-prom
      sloth_objective: "99.9"
      sloth_service: my_service
      sloth_slo: my_slo
      sloth_spec: prometheus/v1
      sloth_version: v0.12.0

このslo_generated_rules.ymlに、SLI/SLO計算のためのRecording Ruleが詰まっています。中身の詳細は、次の第4章で、Grafanaダッシュボードの表示と照らし合わせながら解読していきます。

3.3. STEP3: Recording RuleをPrometheusに読み込ませる

前の手順で生成されたslo_generated_rules.ymlを、Prometheusに読み込ませます。

Prometheusの設定ファイル(通常はprometheus.yml)に、生成したRecording Ruleファイルのパスを追加します。設定ファイルのrule_filesセクションに以下のように記述します。

# prometheus.yml

rule_files:
  - "/path/to/slo_generated_rules.yml"  # 生成したRecording Ruleファイルのパス

設定ファイルを編集した後、PrometheusプロセスにSIGHUPシグナルを送るか、Prometheusを再起動して設定を反映させます。

# SIGHUPでリロードする場合
kill -SIGHUP <prometheus_pid>

# または、Prometheusを再起動する場合
sudo systemctl restart prometheus

これで、Slothが生成したRecording RuleがPrometheusに読み込まれ、定期的に評価されるようになりました。次のステップでは、これらのメトリクスをGrafanaで可視化していきます。

3.4. STEP4: GrafanaダッシュボードでSLI/SLOを確認する

Slothは公式のGrafanaダッシュボードを提供しています。これをインポートするだけで、すぐにSLI/SLOの可視化を始めることができます。

https://sloth.dev/introduction/dashboards/

dashboard.png

Sloth公式ダッシュボード: https://sloth.dev/introduction/dashboards/ より引用

ダッシュボードをインポートすると、第1章で紹介した「SLI」「SLO」「エラーバジェット残量」のグラフが既に表示されているはずです。

これでSLI/SLOの可視化は完了です。しかし、これらのグラフは一体どのような仕組みで描画されているのでしょうか?次の章では、これらのダッシュボードパネルのPromQLを見て、Slothが生成したRecording Ruleがどのように使われているのかを明らかにしていきます。

4. Grafanaダッシュボードの裏側を探る:PromQL解読ガイド

前章で表示したGrafanaダッシュボードを題材に、その裏側で動いているPromQLを解読します。これにより、YAML定義、Recording Rule、そして最終的なグラフがどのようにつながっているのかを理解し、カスタマイズやトラブルシューティングに応用できる知識を身につけることがゴールです。

4.1. SLI・SLOパネルのPromQLを解読する

SLIパネル

このパネルは、「現在のSLIを示す曲線」と「目標値であるSLOを示す直線」の2つのクエリで構成されています。SLIがSLOのラインを下回っていないかを視覚的に監視するための、最も基本的なグラフです。

  • SLI: サービスの実際のパフォーマンスを示す線。常にこの線が目標値を上回っている状態が理想です。
  • Objective: YAMLで定義したSLO目標値を示す横一直線の線。

SLI曲線のPromQL

現在のSLI(成功率)を示す曲線のPromQLは、以下のようになっています。

1 - (max(slo:sli_error:ratio_rate${sli_window}{sloth_service="${service}", sloth_slo="${slo}"}) OR on() vector(0))

このクエリを分解してみましょう。

  • 1 - (エラー率)

    クエリの基本構造は単純で、1からエラー率を引くことで、成功率(=SLI)を算出しています。 例えば、エラー率が 0.001 (0.1%) であれば、1 - 0.001 = 0.999 (99.9%) となります。

  • slo:sli_error:ratio_rate${sli_window}

    これこそが、Slothが生成したRecording Ruleメトリクスです。ここでは、GrafanaのPromQLから第3章で生成されたRecording Ruleへと逆向きに追跡し、最終的にYAMLで定義した内容がどのように使われているのかを詳しく見ていきましょう。

    まず、Grafanaで使われている slo:sli_error:ratio_rate${sli_window} という名前のメトリクスは、第3.2節で生成した slo_generated_rules.yml の中で、以下のように定義されています(5mの例):

    - record: slo:sli_error:ratio_rate5m
      expr: |
        (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST", status=~"5.."}[5m])) by (service))
        /
        (sum(rate(http_request_count_total{uri=~"/v1/hoge", method="POST"}[5m])) by (service))
      labels:
        sloth_id: my_service-my_slo
        sloth_service: my_service
        sloth_slo: my_slo
        sloth_window: 5m
    

    このRecording Ruleの構造を分解すると:

    • record: slo:sli_error:ratio_rate5m: 計算結果が保存される新しいメトリクス名です。命名規則は slo:sli_error:ratio_rate{時間窓} となっています。
    • expr:: ここに記載されているPromQLが定期的に実行され、結果が上記のメトリクス名で保存されます。この式をよく見ると:
      • 分子: sum(rate(http_request_count_total{...status=~"5.."}[5m])) by (service)
        • ➡︎これはYAMLの error_query で定義した内容です({{.window}}5mに置き換わっています)
      • 分母: sum(rate(http_request_count_total{...}[5m])) by (service)
        • ➡︎これはYAMLの total_query で定義した内容です({{.window}}5mに置き換わっています)
      • つまり、エラー率 = エラー数 / 総数 という計算が、ここで実行されています。

    このように、YAMLで定義した error_querytotal_query が、SlothによってRecording Ruleとして定義され、最終的にGrafanaでPromQLの中でメトリクスとして利用されています。

  • ... OR on() vector(0)

    これはPromQLのテクニックで、左辺のメトリクスが存在しない場合(例えば、リクエストが全くない時間帯など)にデフォルト値として0を返すための記述です。これによりグラフが途切れるのを防ぎます。

SLO直線のPromQL

目標値であるSLOを直線で描画するクエリは、シンプルです。

slo:objective:ratio{sloth_service="${service}", sloth_slo="${slo}"}

この slo:objective:ratio もSlothが生成したRecording Ruleメトリクスです。第3.2節で生成した slo_generated_rules.yml の中で、以下のように定義されています:

- record: slo:objective:ratio
  expr: vector(0.9990000000000001)
  labels:
    sloth_id: my_service-my_slo
    sloth_service: my_service
    sloth_slo: my_slo

このRecording Ruleは、YAMLで定義した objective: 99.9 という目標値を、PromQLが扱いやすい比率形式の 0.999... という固定値に変換し、メトリクスとして保存しています。vector(0.9990000000000001) は、定数値を時系列データとして返すPromQLの関数です。

このメトリクスをGrafanaでクエリすると、常に同じ値(0.999)が返されるため、グラフ上では横一直線の目標ラインとして描画されます。

4.2. エラーバジェット残量パネルのPromQLを解読する

エラーバジェット残量パネル

次に、エラーバジェットが期間内にどれだけ残っているかを示すパネルを見ていきましょう。

ここで一つ注意点があります。私たちがYAMLで定義したSLOの評価期間は「過去30日間(ローリングウィンドウ)」でしたが、Sloth公式ダッシュボードで提供されているエラーバジェットパネルは「今月(カレンダー月)」を基準に計算されています。

運用上、ローリングウィンドウとカレンダー月はどちらも有用であり、チームのSLOのレポートサイクルなどに合わせて好きな方を選択することができます。

この記事では、まず公式ダッシュボードをもとにして「今月」基準のPromQLを解読し、その後に定義通りの「30日間」で可視化する、より簡単な方法を補足します。

「今月」の残量を描画するクエリ

公式ダッシュボードで「今月」のバジェット残量を描画しているPromQLは、少し複雑に見えます。

1 - (
  // --- 分子: 今月、実際に消費したエラーの累積量 ---
  sum_over_time(
    (
      slo:sli_error:ratio_rate1h{sloth_service="${service}",sloth_slo="${slo}"}
      * on() group_left() (
        month() == bool vector(${__to:date:M})
      )
    )[32d:1h]
  )
  / on(sloth_id)
  // --- 分母: 今月、許容されるエラーの総量 ---
  (
    slo:error_budget:ratio{sloth_service="${service}",sloth_slo="${slo}"}
    * on() group_left() (24 * days_in_month())
  )
)

このクエリの構造は 1 - (分子 / 分母) という単純な割り算で、「残量」を計算しています。

  • 分子: 今月、実際に消費したエラーの「累積量
  • 分母: 今月全体で許容されるエラーの「総量

ここでも、Slothが生成したRecording Ruleメトリクスが活用されています。それぞれを詳しく見ていきましょう。

分子の解読:sum_over_time(...)

sum_over_time(
  (
    slo:sli_error:ratio_rate1h{sloth_service="${service}",sloth_slo="${slo}"}
    * on() group_left() (
      month() == bool vector(${__to:date:M})
    )
  )[32d:1h]
)

分子では、slo:sli_error:ratio_rate1h(1時間ごとの実際のエラー率)を、sum_over_time関数を使って月初から現在まで足し上げて(累積して)います。このクエリがどのように「今月」のデータだけを抽出しているのか、2つの要素に分けて見ていきましょう。

  1. slo:sli_error:ratio_rate1h - Slothが生成したRecording Ruleメトリクス

    これは既に解説したslo:sli_error:ratio_rate5mと同じ構造で、時間窓が1hになっているものです。YAMLで定義したerror_querytotal_queryから、1時間ごとのエラー率を事前計算しています。

  2. * on() group_left() (month() == bool vector(...)) によるフィルタリング

    この部分は、今月のデータだけを有効にし、それ以外の月のデータをゼロにするためのフィルターです。

    • month() == bool vector(${__to:date:M})
      • month()関数は、各データポイントのタイムスタンプが「何月」であるかを返します。
      • vector(${__to:date:M})は、Grafanaの変数を使ってグラフの表示範囲の終了時刻の月を取得します。
      • この2つを==で比較することで、「データポイントの月」と「現在の月」が一致すれば1、一致しなければ0を返す時系列データが作られます。
    • slo:sli_error:ratio_rate1h * on() group_left() ...
      • このフィルター(1または0)を、元のslo:sli_error:ratio_rate1h(1時間ごとのエラー率)に掛け合わせます。
      • これにより、今月のデータは (エラー率 * 1) で値がそのまま残り、先月以前のデータは (エラー率 * 0) で強制的に0になります。
  3. sum_over_time(...[32d:1h]) による累積

    • [32d]という期間指定は、月初からのデータをすべて含めるためのものです。月は最大でも31日のため、余裕を持たせた32dでデータを遡って取得します。
    • sum_over_timeは、この32日分のデータの中から、前述のフィルター処理によって0にされなかったデータ、つまり「今月のデータ」だけを合計(累積)します。

この2段階の処理により、「月初から現在までのエラー消費の累積量」を計算しています。

分母の解読:(slo:error_budget:ratio * ...)

(
  slo:error_budget:ratio{sloth_service="${service}",sloth_slo="${slo}"}
  * on() group_left() (24 * days_in_month())
)

分母では、slo:error_budget:ratio(エラーバジェット率、例: 0.001)に、24 * days_in_month()(今月の総時間)を掛けて、今月許容されるエラーの総量を算出しています。

  1. slo:error_budget:ratio - Slothが生成したRecording Ruleメトリクス

    このメトリクスは、slo_generated_rules.ymlで以下のように定義されています:

    - record: slo:error_budget:ratio
      expr: vector(1-0.9990000000000001)
      labels:
        sloth_id: my_service-my_slo
        sloth_service: my_service
        sloth_slo: my_slo
    

    このRecording Ruleは、YAMLで定義した objective: 99.9 から、エラーバジェットの割合(1 - 0.999 = 0.001、つまり0.1%)を計算した固定値を保存しています。

  2. * (24 * days_in_month()) - 今月の総時間を掛ける

    • days_in_month()は、今月の日数(28〜31日)を返すPromQLの関数です。
    • これに24を掛けることで、今月の総時間(例:30日の月なら720時間)を算出しています。
    • この総時間を、エラーバジェットの割合(0.001)に掛けています。

    つまり、この式は (エラーバジェットの割合) * (今月の総時間) を計算しています。例えば、バジェットが0.1% (0.001) で、今月が720時間なら、0.001 * 720 = 0.72 となります。これは「今月は合計で0.72時間(約43分)までならエラーレートが100%になってもSLO違反にはならない」という、エラー許容時間の総量を意味します。

全体の組み立て

最終的に(分子 / 分母)でエラーバジェットの「消費率」を算出し、1から引くことで「残量」を求めています。

このように、Slothが生成した計算済みメトリクス(slo:sli_error:ratio_rate1hslo:error_budget:ratio)を活用しながら、PromQLの関数を組み合わせることで、複雑な月次集計のクエリを実現しています。

補足:定義した評価期間(30日間)で可視化する場合

もしYAMLで定義した通りの評価期間(この例では30日間)でのエラーバジェット残量を可視化したい場合は、より簡単です。

Slothが自動生成したslo:period_error_budget_remaining:ratioというRecording RuleメトリクスをGrafanaでクエリするだけで、グラフを描画できます。

slo:period_error_budget_remaining:ratio{sloth_service="${service}", sloth_slo="${slo}"}

この slo:period_error_budget_remaining:ratio は、slo_generated_rules.ymlで以下のように定義されています:

- record: slo:period_error_budget_remaining:ratio
  expr: 1 - slo:period_burn_rate:ratio{sloth_id="my_service-my_slo", sloth_service="my_service", sloth_slo="my_slo"}
  labels:
    sloth_id: my_service-my_slo
    sloth_service: my_service
    sloth_slo: my_slo

このRecording Ruleは、さらに別のRecording Ruleである slo:period_burn_rate:ratio を参照しています。この slo:period_burn_rate:ratio の定義も見てみましょう:

- record: slo:period_burn_rate:ratio
  expr: |
    slo:sli_error:ratio_rate30d{sloth_id="my_service-my_slo", sloth_service="my_service", sloth_slo="my_slo"}
    / on(sloth_id, sloth_slo, sloth_service) group_left
    slo:error_budget:ratio{sloth_id="my_service-my_slo", sloth_service="my_service", sloth_slo="my_slo"}
  labels:
    sloth_id: my_service-my_slo
    sloth_service: my_service
    sloth_slo: my_slo

ここでは、slo:sli_error:ratio_rate30d(30日間の平均エラー率)を slo:error_budget:ratio(許容されるエラーの割合)で割ることで、「エラーバジェットを消費する速度」を計算しています。

このように、Slothは複数のRecording Ruleを階層的に組み合わせることで、複雑な計算を段階的に実行しています。私たちは最終結果である slo:period_error_budget_remaining:ratio を呼び出すだけで、YAMLで定義した評価期間(30日間)でのエラーバジェット残量を簡単に可視化できるのです。

4.3. トラブルシューティング:ゼロ除算でグラフが表示されない問題

実際にSlothを使ってSLI/SLO運用を行う際には、グラフが描画されなくなる(NaNになる)問題に遭遇する可能性があります。

このよくある原因は、エラー率の計算式 (エラー数) / (総数) において、イベントが全くない期間が存在する場合、分母の「総数」が0になるために発生します。分母が0になると、ゼロ除算となり計算結果全体がNaNになるため、グラフが描画されなくなります。

対策はいくつか考えられます。

  • 対策1: PromQLでの対処
    • 分母 > 0 or vector(1) のようにクエリを修正し、分母が0の場合にデフォルトで1を使うようにすることで、ゼロ除算を回避する方法があります。この場合、イベントが存在しない時間帯でもデフォルトで1件はイベントがあったとみなして計算されます。
    • こちらについて議論されているSlothのIssue: https://github.com/slok/sloth/issues/231 , https://github.com/slok/sloth/issues/531
  • 対策2: Slothでの--disable-optimized-rulesオプションの利用
    • Sloth v0.12.0以降では、sloth generateコマンドに--disable-optimized-rulesというオプションが追加されました。デフォルトでは、Slothでは長い期間(例: 30d)のSLIを計算する際に、短い期間(例: 5m)のデータを合計する最適化を行い、prometheusにかかる負荷を軽減しています。このオプションを有効にすると、この最適化が無効になり、長い期間であっても直接計算するようになります。これにより、途中のデータが欠損していてもprometheusの内部処理によって適切に処理され、ゼロ除算が起こらなくなります。ただし、このオプションを有効にすると、その分prometheusへの負荷が高くなるため、注意が必要です。
    • こちらについて議論されているSlothのIssue: https://github.com/slok/sloth/issues/241

ここまで第4章では、GrafanaダッシュボードのパネルごとにPromQLを確認してきました。4.1節では「SLI・SLOパネル」を、4.2節では「エラーバジェット残量パネル」を見てきました。

これらすべてのクエリに共通していたのは、その中核にSlothが生成したRecording Ruleメトリクスが使われていたという点です。そしてこれらのRecording Ruleは、すべて第3章で私たちがYAMLファイルに定義したerror_querytotal_queryobjectiveという3つのシンプルな値から自動生成されたものでした。

もしSlothがなければ、これらの複雑なPromQLや、それを支えるRecording Ruleの定義を、すべて手作業で書く必要があったでしょう。様々な時間窓(5m1h30dなど)ごとのエラー率計算、月次集計のためのmonth()関数を使ったフィルタリング、sum_over_timeによる累積計算、さらには階層的に組み合わされた複数のRecording Ruleなど、正しく動作させるには高度なPromQLの知識と経験が必要です。

Slothの価値は、まさにここにあります。私たちはYAMLで「何を測りたいか」(error_query / total_query)と「どのレベルを目指すか」(objective)を宣言的に定義するだけで、これらすべての複雑な実装をSlothが自動的に生成してくれるのです。

5. まとめ

本記事では、SLI/SLO運用の基本である「SLI」「SLO」「エラーバジェット」という3つの指標を可視化するために、まずGrafanaダッシュボードというゴールを示し、その裏側で動くPromQLと、それを支えるSlothが作ったRecording Ruleの内容を確認してきました。

YAMLによるシンプルな定義から、PrometheusのRecording Ruleが自動生成され、最終的にGrafanaダッシュボードでどのようにPromQLとして利用されるのか、その一連の流れと裏側の仕組みを解読しました。

Slothの価値は、PromQLの実装を抽象化し、SLI/SLO管理の導入と運用を簡素化する点にあります。これにより、私たちエンジニアはクエリの実装詳細に頭を悩ませるのではなく、「何をサービスの信頼性指標とすべきか」といった本質的な議論に集中することができます。

しかし同時に、Slothが生成するPromQLの中身を一度は理解しておくことは、“脱ブラックボックス"に繋がり、予期せぬ問題が発生した際のトラブルシューティング能力を向上させます。また、ツールに過度に依存しないSLI/SLO管理の知識を身につける上でも重要です。

この記事が、読んでいただいた方のSLI/SLO運用の一助となれば幸いです。

最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。

recruit

DeNAでは、失敗を恐れず常に挑戦し続けるエンジニアを募集しています。