メトリクスとアラート

このページの目次

メトリクスは大量のシステムイベントを比較可能な時系列に圧縮し、アラートはその一部の変化を人間のアクションに変換する。前者はまず測定の問題であり、統計対象、時間窓、分母は何か。後者はまず意思決定の問題であり、どのような状況を、誰が、どのくらいの時間内に対処すべきか。

本稿では測定と推論の方法について議論する。具体的な Prometheus コマンドについては Prometheus 運用マニュアル を、単一リクエストの因果分析については 分散トレーシング を参照されたい。

ユーザーの課題から逆算して測定口径を定義する

「サービスが正常かどうか」は具体化しなければならない。クエリインターフェースを例にとると、「有効なリクエストのうち、成功し、300ms 以内に完了した割合」と定義できる。ここでは少なくとも4つの選択肢がある。有効なリクエストとは何か、どのレスポンスが成功とみなされるか、遅延の計測開始点はどこか、統計窓はどのくらいか。

アプリケーションのエントリポイントでのインスツルメントでは、アプリケーションに到達する前における DNS、TLS、またはゲートウェイの失敗は観測できない。エンドツーエンドの可用性が目標であれば、エッジの観測または外部プローブと組み合わせる必要がある。アプリケーション内の成功率をそのままユーザーの可用性と呼ぶと、測定範囲外の失敗を見逃すことになる。

4xx をエラーに含めるかどうかにも固定的な答えはない。エラーパラメータがサービスの責任範囲にない場合もあれば、認証設定のエラーにより正常なユーザー全員が拒否される場合はサービス障害である。まずはビジネスの口径を明確にし、その後にクエリ式を記述すべきである。

Counter、Gauge、および分布

種類記録内容典型的な用途一般的な誤解
Counter累積イベント数(再起動時にゼロになる可能性あり)リクエスト数、失敗数、処理バイト数累積値を毎秒のレートと見なす
Gauge現在の状態(増減可能)キューの長さ、処理中のリクエスト数、残り容量任意の Gauge に対して rate を求め、スループットと呼ぶ
Histogram観測値の分布と数遅延、レスポンスサイズ平均値のみを見てロングテールを無視する
Summary数、合計、およびオプションのクライアント側分位数インスタンス単位の分布統計複数のインスタンスの分位数を単純平均する

Counter からレートを算出する際は、まず各シグネチャに対して rate を計算し、その後サービス単位で集約し、再起動によるゼロリセットをインスタンスレベルで処理する。以下の例では、http_requests_total が service、instance、status ラベルを持つと仮定する。

# サービスごとのリクエストレート
sum by (service) (rate(http_requests_total[5m]))

# サービスごとの 5xx 割合;サービスにリクエストがある場合のみ結果を解釈する
sum by (service) (rate(http_requests_total{status=~"5.."}[5m]))
/
sum by (service) (rate(http_requests_total[5m]))

必要なエラーラベルのシグネチャは事前に初期化するか、ルール内で明示的に欠損値を処理するべきである。「エラーシグネチャがない」ことと「エラー率がちょうどゼロ」ことは、同じデータ形態ではない。完全にトラフィックがない場合、分母はゼロとなり、可用性が100%であるかのように偽装すべきではない。

Counter を先に集約すると、1つのインスタンスのゼロリセットが他のインスタンスに拡散する

3回の同時サンプリングを仮定する。A のカウントは 100、110、5、B は 100、120、130 とする。A は後2回のサンプリングの間に再起動し、再起動後の値は 5 である。B は再起動していない。インスタンスごとにゼロリセットを個別に処理すると、観測された増分は A: 10+5=15、B: 20+10=30 で、合計 45 となる。

もし先に足し合わせると、シグネチャは 200、230、135 となる。その後、減少を集約されたカウンターのゼロリセットとみなすと、30+135=165 となり、B がまだ保持している累積値も新しいイベントとしてカウントされてしまう。

def observed_increase(samples):
    return sum(current - previous if current >= previous else current
               for previous, current in zip(samples, samples[1:]))

A, B = [100, 110, 5], [100, 120, 130]
assert observed_increase(A) + observed_increase(B) == 45
assert observed_increase([a + b for a, b in zip(A, B)]) == 165

これは離散サンプルのゼロリセットモデルであり、Prometheus の rate が行う時間外挿は実装されていない。サンプリング間隔で再起動前に発生したが観測されなかったイベントは、この3つのサンプルからは完全に復元できない。この例の目的は、正確な全量帳簿をサンプルから作り出すことではなく、各 Counter のライフサイクルの境界を保持した上で、レートを集約することにある。

なぜ P99 を平均化してはいけないか

1つのインスタンスが 100 件のリクエストを処理し、もう1つが 10000 件のリクエストを処理していると仮定する。両者の P99 が同じであっても、すべてのリクエストを混合した後の正確な P99 を教えてくれるわけではない。分位数は分布内部の情報を失う。単純な平均はさらにサンプルの重みを無視するという欠点もある。

従来の Histogram は累積バケットカウントを保存しており、バケットを先に集約してから全体分位数を推定できる。バケットの境界は集約中に保持しなければならない。

histogram_quantile(
  0.99,
  sum by (service, le) (
    rate(http_request_duration_seconds_bucket[5m])
  )
)

ここでは各インスタンスが互換性のある従来のバケット境界を使用していると仮定する。推定誤差はバケットの幅に依存する。目標閾値が 300ms であれば、0.3秒のバケットを設定することで、P99 を推定する前に閾値内の割合を直接統計できる。Native Histogram の表現とクエリ形式は異なるため、実際のメトリクスタイプに応じて選択する必要がある。Prometheus Histograms and Summaries を参照。

平均値にも価値がある。総処理時間をリクエスト数で割ったものは平均的な作業負荷を表し、分位数はテール側の体験を表し、目標閾値を超える割合はサービス目標に直接結びつく。これらは異なる質問に答えるものである。

リクエスト数で重み付けしても、局所的な分位数は結合できない

最近接順位法で説明する。P99 はソート後の第 ceil(0.99*N) 番目のサンプルを取る。インスタンス A の 10000 件のリクエストがすべて 1ms、インスタンス B の 100 件のリクエストがすべて 1000ms であるとすると、局所的な P99 はそれぞれ 1 と 1000 だが、結合後の第 9999 番目のサンプルはまだ A の高速リクエストの中にあるため、全体としての P99 は 1ms となる。

from fractions import Fraction
from math import ceil

def p99(values):
    return sorted(values)[ceil(Fraction(99, 100) * len(values)) - 1]

A, B = [1] * 10000, [1000] * 100
assert p99(A) == 1 and p99(B) == 1000
assert p99(A + B) == 1
simple_average = Fraction(p99(A) + p99(B), 2)
weighted_average = Fraction(p99(A) * len(A) + p99(B) * len(B), len(A) + len(B))
assert simple_average == Fraction(1001, 2)
assert weighted_average == Fraction(1100, 101)
assert simple_average != p99(A + B) and weighted_average != p99(A + B)

これは 100 件の低速リクエストが無関係であることを意味するわけではない。局所的なインスタンスの劣化を特定することが目標であれば、B の分布は有用である。全体としてのユーザーリクエストの分位数が目標であれば、全体分布に基づいて計算しなければならない。異なる統計ソフトウェアでは分位数の補間規約が異なるが、局所的な分位数を平均することで全量分位数を普遍回復できるものはない。

共通の 300ms バケットを採用する場合、この例では閾値内のリクエスト割合を2つのインスタンスの累積バケットを足すことで直接 10000/10100≈99.01% と得られる。これは約 0.99% のリクエストが閾値を超えていることを明確に露呈し、全体 P99 は依然として 1ms である。閾値適合率と分位数はそれぞれ異なる情報を保持している。

全体の P99 はソート位置であり、2台のマシンの P99 を平均化することはできない インスタンス A 10000 回 × 1 ms;P99=1 インスタンス B 100 回 × 1000 ms;P99=1000 全体 10100 回 第 9999 番目もなお 1 ms 単純平均 500.5 ms、加重平均約 10.89 ms いずれも全体の P99 ではない。 低速リクエストは依然として存在する。閾値適合率、分位数、およびインスタンス次元を組み合わせて解釈する。

RED と USE:サービス結果とリソースの解釈

RED は Rate(レート)、Errors(エラー)、Duration(所要時間)を観察し、つまりサービスがどれだけの量を処理し、どれだけの失敗があり、どれだけの時間を要したかを示す。USE は Utilization(使用率)、Saturation(飽和度)、Errors(エラー)を観察し、つまりリソースの占有、待機圧力、およびエラーを示す。前者はユーザー側の症状を発見するのに役立ち、後者はリソースのボトルネックを説明するのに役立つ。

オブジェクト使用率飽和/待機エラー
CPUバジー時間の割合実行可能タスクのキュー、CPU pressure関連するハードウェアまたはスケジューリングの異常は別途観測
メモリワーキングセットと空き容量ガベージコレクション、ページング、memory pressureOOM イベント、割り当て失敗
ストレージデバイスのビジー時間などの利用指標キュー深度、待機時間I/O エラー
スレッドプールアクティブスレッド数待機実行タスクキュータスク拒否、実行失敗

OOM score はプロセス選択に関する情報であり、メモリ飽和度を直接代替するものではない。CPU 使用率が高いからといって必ずしも障害とは限らない。スループットが高く、キューが安定し、遅延が目標を満たしている場合、リソースを有効に利用しているだけかもしれない。逆に、シングルスレッドのボトルネックにより、マシン全体の平均 CPU は低くても、リクエストが深刻にキューイングされている可能性がある。関連する手法については USE Method および Google SRE のモニタリングシグナル を参照。

SLI、SLO、SLA およびエラーバジェット

SLI は確立された口径で測定された指標である。SLO は一定の期間内で達成したい目標である。SLA は外部との合意および関連する結果であり、これらは互いに代替できない。

30日間のリクエスト成功率の SLO が 99.9% であれば、許容される失敗割合は 0.1% となる。有効なリクエストが100万件の場合、バジェットは1000回の失敗となる。これはリクエスト数ベースのバジェットであり、「43.2分のダウンタイムを許容」と直接換算できるものではない。リクエストトラフィックは時間とともに変化する可能性がある。時間ベースの可用性口径を使用して初めて、分の換算に対応する意味を持つ。

エラーバジェット:消費速度が対応の緊急性を決定する 目標許容エラー率 0.1% 現在の観測エラー率 1.0% 燃焼率 = 1% ÷ 0.1% = 10 倍 同じ測定口径において、10倍の燃焼率は目標許容速度の10倍でバジェットを消費する。

燃焼率 B = 現在の不良イベント割合 / (1 - SLO) と定義する。SLO が 99.9% で、現在の不良イベントが 1% を占める場合、B=10 となる。測定口径とトラフィック分布がほぼ安定している場合、この速度が続けば、30日間のバジェットは約3日で枯渇する。窓の変化やトラフィックの急変はこの予測に影響を与える。

短窓は敏感だがスパイクの影響を受けやすく、長窓は頑健だが回復が遅い。マルチウィンドウ戦略は、同じ燃焼率条件を長窓と短窓の両方に同時に適用し、問題が持続的に存在し、かつ回復していないことを確認する。具体的な閾値はバジェットの消費と対応時間から導出すべきであり、Google SRE:SLO によるアラート を参照。

14.4 倍の閾値はバジェット消費から逆算できる

バジェット窓が 30 日(720時間)であり、安定したトラフィックで消費速度を近似して見積もると仮定する。「1時間で総バジェットの 2% を消費」した場合に高優先度対応をトリガーしたい場合、閾値は B×1/720=0.02 を満たす必要がある。したがって B=14.4 となる。99.9% の目標に対して、対応する観測エラー割合は 1.44% である。

from fractions import Fraction
window_hours = 30 * 24
burn = Fraction(2, 100) * window_hours / 1
bad_fraction = burn * Fraction(1, 1000)
assert burn == Fraction(72, 5)  # 14.4
assert bad_fraction == Fraction(9, 625)  # 1.44%

def page(long_burn, short_burn):
    return long_burn > burn and short_burn > burn

assert page(20, 20)
assert not page(20, 0)  # 長窓はまだ高いが、直近は回復済み
assert not page(2, 20)  # 短時間のスパイクはまだ同じ長期消費を形成していない

短窓は長窓の一部を選択し、現在も急速に消費されていることを確認するために使用できる。ここでは組み合わせロジックのみを示しており、すべてのサービスが同じ閾値を使用すべきであるとは規定していない。顕著な日次サイクルを持つトラフィックや、非常に低いリクエスト量の場合、短窓のエラー割合は非常に不安定になり、時間の割合がリクエストバジェットの割合を正確に表さない可能性がある。実際のイベント数に基づいてアラート戦略を調整する必要がある。Google SRE バジェットアラート手法

同じ燃焼率なのに、なぜバジェット消費が異なるのか?

燃焼率 B は割合の比であり、すでに消費されたバジェットのパーセントではない。固定窓の総リクエスト数 N、観察セグメントのリクエスト数 n、そのセグメントの不良イベント割合 e とする。バジェットへの寄与は ne/[N(1−SLO)]=B·n/N となる。トラフィックがほぼ安定している場合に限り、時間比率で n/N を近似できる。以下では、エラー割合を変えずにリクエスト量を変化させることで、B は変わらず、バジェット消費が変化することを直接確認できる。

図を準備しています
同じ燃焼率なのに、なぜバジェット消費が異なるのか?

燃焼率は不良イベント割合と許容割合を比較する。実際に消費されるリクエストバジェットは、その期間にどれだけのリクエストを処理したかにも依存する。時間比率を無条件にリクエスト比率に置き換えることはできない。

可視化から実行可能なアラートへ

高優先度の通知は、エラー、遅延、データの整合性、容量枯渇など、即時の介入が必要な影響や差し迫ったリスクに対応すべきである。「4xx/5xx のみ通知」と規定してはならない。サービスが 200 を返しても、遅延が数十秒に及べば、目標を大きく逸脱する可能性がある。

アラートルールは、サービスと責任者、ユーザーへの影響、観測窓、優先度、最初のチェックポイントおよび回復判断を明記すべきである。短時間の揺らぎは for などの継続条件でフィルタリングできるが、for を延長すると検出遅延も増加するため、無限に延期してレポートを静かにさせることはできない。

欠損データのセマンティクスを明確にする必要がある。サンプリングの失敗によりビジネスシグネチャが消える可能性がある。シグネチャが消えたからといって、自動的にビジネスが回復したとみなしてはならない。ターゲットのサンプリング可能性、ルール評価の正常性、通知チェーンの円滑さをそれぞれ監視すべきである。

ラベルのカーディナリティ:1つの分類は1組の時系列

あるメトリクスに 20 のルート、5 のステータスカテゴリ、50 のインスタンスがあると仮定すると、完全な組み合わせですでに 5000 本のシグネチャに達する。さらに 10 万のユーザー ID をラベルとして追加すると、潜在的な組み合わせは急速に拡大する。すべての組み合わせが出現するわけではないとしても、増え続けるラベルはストレージとクエリの負荷を増加させる。

完全な URL ではなく、安定したルートテンプレートを使用する。リクエスト ID、注文 ID などはログやトレーシングに含めるのが適切であり、無制限のメトリクスラベルとして使用するのは避けるべきである。単位、命名、およびラベル設計については Prometheus Metric and Label Naming を参照。

Prometheus の高可用性:各インスタンスには状態がある

Prometheus 二重化:独立した収集、クエリと通知はそれぞれ重複排除 モニタリングターゲット Prometheus A 独立した TSDB / WAL Prometheus B 独立した TSDB / WAL クエリ層 レプリカラベルで重複排除 Alertmanager クラスタ アラート集約と通知の重複排除 各レプリカはローカル状態を持つ。収集の高可用性は、共有ストレージや自動的なエンドツーエンドの重複排除を意味しない。

Prometheus はローカル時系列、WAL、およびルール関連の実行状態を保存するため、ステートレスサービスと呼ぶことはできない。二重インスタンスは通常、同じターゲットを独立して収集し、クエリ層は設定に基づいてレプリカを識別し、重複計算を避ける。アラート層は集約と通知の重複排除を個別に処理する。クエリの高可用性と通知の高可用性は同一のものではない。

リモート書き込み、長期ストレージ、およびローカル保持ポリシーは、それぞれ独立した設計次元である。二重化はすべての履歴データを自動的にバックアップするものではなく、リモート書き込みの遅延が永不発生することも保証しない。ストレージモデルについては Prometheus Storage を参照。具体的なクエリバックエンドのレプリカラベル、重複排除アルゴリズム、およびフェイルオーバー動作は、それぞれのドキュメントに従って設定する必要がある。

参考

キーワード:Counter、Gauge、Histogram、P99、RED、USE、SLI、SLO、エラーバジェット、燃焼率、ラベルのカーディナリティ、Prometheus HA