記事の一覧

閾値の決め方:監視アラートとKPI監視で使える5つの方法

監視アラートの閾値の決め方を、固定値、過去の分布、時間帯と曜日、継続時間、変化の検出の 5 つに分けて解説。注文数などの KPI 監視で使える SQL の例、症状ベースと原因ベースの考え方、閾値を見直す運用まで説明します。

監視を始めると、最初に悩むのが閾値 (しきいち: アラートを出す境目の値) です。CPU は 80% でよいのか、注文数は何件を下回ったら異常なのか。根拠なく決めた閾値は、誤報が続くか、本当の異常を見逃すかのどちらかになりがちです。この記事では、閾値の決め方を 5 つの方法に分けて、どんな指標にどれが向くか、実際に値を出す SQL、決めたあとの見直し方までを説明します。

閾値が難しい理由

閾値を下げれば異常を見逃しにくくなりますが、誤報が増えます。上げれば誤報は減りますが、見逃しが増えます。この 2 つは片方を良くするともう片方が悪くなる関係にあり、どこかで折り合いをつける必要があります。

Google の SRE の資料ではアラートの良し悪しを次の 4 つで評価しています。

観点 意味 悪いと起きること
適合率 (precision) 出したアラートのうち、本当に重要だったものの割合 誤報が多く、通知が信用されなくなる
再現率 (recall) 重要な出来事のうち、アラートを出せたものの割合 異常を見逃す
検知までの時間 異常が始まってから通知するまでの時間 気づくのが遅れ、影響が広がる
解除までの時間 異常が収まってからアラートが消えるまでの時間 終わった問題の通知が残り、混乱する

閾値を決めるというのは、この 4 つのどこで折り合うかを、指標ごとに選ぶ作業です。たとえば「ディスクがあふれる」は見逃すと影響が大きいので再現率を優先し、「CPU が一時的に高い」は誤報を嫌って適合率を優先する、という具合です。

閾値を決める前に確かめること

数字を決める前に、そのアラートが届いたら誰が何をするのかを書き出してください。対応する人も行動も決まらないなら、そのアラートは通知ではなく、ダッシュボードで見られれば十分な値かもしれません。SRE の資料にも「呼び出しはすべて、対応が必要なものであるべき」という原則があります。

もう 1 つ、その指標が「症状」か「原因」かを区別します。症状は利用者が感じる問題 (画面が遅い、決済が失敗する、注文が入らない) で、原因はその手前で起きていること (CPU が高い、接続数が多い、レプリカが遅れている) です。

種類 例 アラートとしての扱い
症状 API の応答時間、エラー率、注文数の減少、決済の失敗率 利用者に影響が出ているので、優先して通知する
原因 CPU 使用率、接続数、ロック待ち、レプリケーション遅延 調査の手がかりとして記録する。影響が確実で差し迫っているときだけ通知する

原因の指標はいくらでも増やせますが、CPU が 90% でも利用者に影響がないことはよくあります。SRE の資料では症状を捉えることに力を注ぎ、原因で人を呼ぶのは「確実で差し迫っている」場合に限るよう勧めています。ディスクの空きがあと数時間でなくなる、というのは原因側ですが、確実で差し迫っているので通知する価値がある例です。

方法 1:固定値で決める

最も分かりやすいのは「この値を超えたら通知する」という固定の閾値です。上限が物理的・設定的に決まっている指標に向いています。

ディスクの空き容量、接続数と max_connections、トランザクション ID の消費 (PostgreSQL)、証明書やライセンスの期限などがこれにあたります。これらは「上限に達したら止まる」ことがはっきりしているため、上限からの距離で閾値を決められます。

固定値を決めるときのコツは、「対処にかかる時間」から逆算することです。ディスクの増設に半日かかるなら、残りが半日分を切る前に通知しなければ間に合いません。そのため、容量は「残り何 %」より「この増え方だとあと何時間で埋まるか」で判断すると、対処の時間を確保できます。

-- 1 日ごとに記録したサイズから、直近 7 日の平均の増え方と残り日数を見積もる例 (PostgreSQL)
-- db_size_daily(day date, bytes bigint) は自分で記録しているテーブルの例
WITH growth AS (
  SELECT (max(bytes) - min(bytes))::numeric / nullif(max(day) - min(day), 0) AS bytes_per_day
  FROM db_size_daily
  WHERE day >= current_date - 7
),
latest AS (
  SELECT bytes FROM db_size_daily ORDER BY day DESC LIMIT 1
)
SELECT round(g.bytes_per_day) AS bytes_per_day,
       round((536870912000 - l.bytes) / nullif(g.bytes_per_day, 0), 1) AS days_left  -- 500 GiB を上限とした例
FROM growth g, latest l;

CPU 使用率のように上限に達しても止まらない指標に、固定値だけでアラートを付けると誤報が増えます。後で説明する継続時間の条件と組み合わせてください。

方法 2:過去の分布から決める

上限がはっきりしない指標は、過去の値の分布から閾値を決めます。たとえば過去 4 週間の応答時間を並べて、99 パーセンタイル (小さい順に並べて 99% の位置にある値) を超えたら異常、のように決めます。

平均ではなくパーセンタイルや中央値を使うのには理由があります。平均は一部の極端な値に引っ張られます。応答時間の平均が 100 ミリ秒でも、1% のリクエストが数秒かかっている、ということはよくあります。異常の判定では「普段の大半はどの範囲に収まっているか」を知りたいので、中央値やパーセンタイルの方が向いています。

PostgreSQL では percentile_cont で分布を出せます。次の例は監視の結果を記録しているテーブルから、指標ごとに過去 28 日の分布を出すものです。

-- metric_samples(metric text, ts timestamptz, value double precision) は監視値を記録しているテーブルの例
SELECT metric,
       count(*) AS samples,
       percentile_cont(0.50) WITHIN GROUP (ORDER BY value) AS p50,
       percentile_cont(0.95) WITHIN GROUP (ORDER BY value) AS p95,
       percentile_cont(0.99) WITHIN GROUP (ORDER BY value) AS p99,
       max(value) AS max
FROM metric_samples
WHERE ts >= now() - interval '28 days'
GROUP BY metric;

この方法の注意点は 2 つあります。1 つめは過去の期間に障害が含まれていると、その異常値も「普段の値」として分布に入ってしまうことです。障害のあった期間は除いて計算してください。2 つめは 99 パーセンタイルを閾値にすると、正常でも 100 回に 1 回は超えるという点です。5 分ごとに判定するなら、単純に計算して 1 日に 3 回ほど超えることになるため、パーセンタイルだけで通知せず、継続時間の条件と組み合わせます。

方法 3:時間帯と曜日で分けて決める

注文数、ログイン数、API のリクエスト数のように、人の活動に合わせて動く指標は、時間帯と曜日で大きく変わります。昼の注文数を基準にすると深夜は毎晩「異常に少ない」ことになり、深夜を基準にすると昼に半減しても気づけません。

こうした指標は「同じ曜日の同じ時間帯」の過去の値と比べます。次の SQL は過去 4 週間の注文数を曜日・時刻ごとに集計し、中央値と最小値を出すものです。

SET TIME ZONE 'Asia/Tokyo';   -- 曜日と時刻を日本時間で数える

WITH hours AS (
  SELECT generate_series(
           date_trunc('hour', now()) - interval '28 days',
           date_trunc('hour', now()) - interval '1 hour',
           interval '1 hour') AS hour
),
hourly AS (
  SELECT h.hour, count(o.id) AS orders
  FROM hours h
  LEFT JOIN orders o
    ON o.created_at >= h.hour
   AND o.created_at <  h.hour + interval '1 hour'
  GROUP BY h.hour
)
SELECT extract(isodow FROM hour) AS dow,   -- 1 = 月曜 ... 7 = 日曜
       extract(hour FROM hour)   AS hh,
       count(*) AS weeks,
       percentile_cont(0.5) WITHIN GROUP (ORDER BY orders) AS median_orders,
       min(orders) AS min_orders
FROM hourly
GROUP BY 1, 2
ORDER BY 1, 2;

この SQL には、よくある落とし穴への対策を 2 つ入れています。

1 つめは注文が 0 件の時間の扱いです。orders を GROUP BY で集計するだけだと、注文が 1 件もなかった時間は結果に現れず、分布が実際より高く出ます。generate_series ですべての時間を並べてから LEFT JOIN しているのはこのためです。

2 つめはタイムゾーンです。date_trunc や extract は timestamptz の値をセッションのタイムゾーンで区切ります。サーバーの既定が UTC のままだと、「月曜 9 時」が日本時間の月曜 18 時を指してしまいます。

この集計で出せるのは各曜日・時刻ごとに 4 つの値だけなので、分布としては心もとない数です。そこで、判定には次のような、幅を持たせたルールを使います。

  • 直近 1 時間の注文数が、同じ曜日・時刻の中央値の半分を下回る
  • ただし、その時間帯の中央値が 20 件以上のときだけ判定する

2 つめの条件は、件数が少ない時間帯の誤報を防ぐためのものです。普段 2 件の時間帯に 0 件でも偶然の範囲ですが、普段 200 件の時間帯に 50 件なら明らかにおかしい、という感覚をルールにしています。「半分」「20 件」は例なので、過去のデータに当てはめて、何回発火したかを確かめてから決めてください。

祝日、セール、テレビ放映のように曜日の周期から外れる日は、事前に分かっていれば判定を止めるか基準を変えます。予定表のような形で例外の日を管理しておくと、毎年の大型連休で誤報が続くのを防げます。

方法 4:継続時間と回数の条件を付ける

ここまでの方法で決めた閾値に、「どのくらい続いたら」という条件を加えると、誤報を大きく減らせます。CPU 使用率が 1 分だけ 95% になることは日常的にありますが、15 分続くなら何かが起きています。

多くの監視ツールに、この条件を指定する設定があります。

ツール 設定 意味
Amazon CloudWatch --evaluation-periods と --datapoints-to-alarm 直近 N 回の判定のうち M 回超えたら発火
Prometheus アラートルールの for 条件がこの時間続いたら発火。それまでは保留 (pending)
Prometheus アラートルールの keep_firing_for 条件が外れても、この時間は発火を続ける
# Prometheus のアラートルールの例: エラー率が 5 分続けて 10% を超えたら発火
groups:
- name: example
  rules:
  - alert: HighErrorRate
    expr: job:http_errors:rate5m{job="api"} > 0.1
    for: 5m
    labels:
      severity: page

継続時間を長くするほど誤報は減りますが、検知は遅れます。決済の失敗のように 1 分でも影響が大きいものは短く、CPU のように一時的な山が多いものは長く、と指標ごとに変えます。

もう 1 つ、閾値の付近で値が上下すると、発火と解除が何度も繰り返されます。これを防ぐには、発火の閾値と回復の閾値を分けます (ヒステリシス)。たとえば「80% を超えたら発火、70% を下回ったら解除」とすれば、78% と 82% を行き来しても通知は 1 回で済みます。Prometheus の keep_firing_for も同じ目的で使えます。

方法 5:変化の大きさや異常検出で決める

値そのものより変化に注目する方法もあります。「前の 1 時間から 3 倍に増えた」「先週の同じ時刻から半分に減った」のような比率の閾値は、指標の水準が少しずつ変わっていく場合にも使えます。データが増え続けるサービスでは、半年前に決めた固定値がいつの間にか合わなくなりますが、比率なら水準の変化に追従します。

監視ツールの異常検出の機能を使う方法もあります。Amazon CloudWatch の異常検出 (anomaly detection) は、最大 2 週間分の過去の値から、時間・日・週の周期と傾向を学習して「予想される範囲」を作り、その範囲を外れたら発火するアラームを作れます。デプロイや障害のように学習から外したい期間は、除外することもできます。ただし、異常検出のアラームには追加の料金がかかります。

異常検出は閾値を考える手間を減らしてくれますが、「統計的に珍しい」ことと「対応が必要」なことは同じではありません。珍しいが問題のない変化 (キャンペーンで注文が急増した、など) でも発火します。上か下か、どちらの外れを通知するかを指標ごとに選び、発火したときに誰が何をするかを決めておく点は、固定値の場合と変わりません。

KPI 監視の閾値で気をつけること

注文数、決済の成功率、会員登録数のような KPI (重要業績評価指標) を監視するときは、技術的な指標とは別の注意が要ります。

まず、比率の指標は分母も見ます。「決済の失敗率が 10% を超えたら異常」というルールは、深夜に決済が 3 件しかなく、そのうち 1 件が失敗しただけで発火します。次のように、件数が一定以上あるときだけ判定する条件を加えます。

-- 直近 30 分の決済の失敗率。件数が 30 件未満のときは判定しない (PostgreSQL)
SELECT count(*) AS total,
       count(*) FILTER (WHERE status = 'failed') AS failed,
       CASE WHEN count(*) >= 30
            THEN round(count(*) FILTER (WHERE status = 'failed') * 100.0 / count(*), 1)
       END AS failed_pct
FROM payments
WHERE created_at >= now() - interval '30 minutes';

次に、0 件を見逃さない仕組みにします。「注文数が少ない」を判定するクエリが、データの取り込みの停止やアプリの障害で 0 件になったとき、監視の仕組みによっては「値がない」として判定を飛ばしてしまいます。CloudWatch なら --treat-missing-data breaching のように、値が来ないことを異常として扱う設定を確かめてください。

最後に、KPI の閾値は業務の担当者と一緒に決めます。「1 時間注文が半分になったら知りたいか」「決済の失敗率が何 % なら夜中でも起こしてほしいか」は、エンジニアだけでは決められません。

重大度を分けて通知先を変える

同じ指標でも、程度によって対応の急ぎ方は違います。閾値を 2 段階にして、重大度ごとに通知先を変えると、夜間に起こされる回数を減らせます。

重大度 例 通知先 期待する対応
緊急 決済の失敗率が 30 分続けて 20% 超、ディスクの残りが 6 時間分 電話やオンコールの呼び出し すぐに対応する
警告 注文数が中央値の半分を下回った、ディスクの残りが 3 日分 チャットの監視チャンネル 営業時間内に確認する
記録 CPU 使用率が 1 時間 80% 超 ダッシュボードのみ 定期的な見直しで傾向を見る

SLO (サービスレベル目標: 「月の 99.9% の時間は正常に応答する」のような目標) を決めているなら、エラーの量そのものではなく、許容できるエラーの量 (エラーバジェット) を使い切る速さで重大度を分ける方法もあります。Google の SRE の資料では 1 時間でエラーバジェットの 2% を使う速さ、6 時間で 5% を使う速さなら呼び出し、3 日で 10% ならチケットにする、という組み合わせを出発点として紹介しています。

閾値を見直す運用

閾値は一度決めて終わりではありません。サービスの規模も使われ方も変わるので、定期的に見直す前提で運用します。

見直しのために、アラートが発火するたびに次の 3 点を記録しておきます。

  • 対応が必要だったか (必要だった、不要だった、すでに知っていた)
  • 対応した場合、何をしたか
  • 発火までに異常が始まってからどれくらい経っていたか

あわせて、アラートが出なかったのに後から見つかった異常 (見逃し) も記録します。月に 1 回これを見返し、指標ごとに次のように判断します。

記録の傾向 見直しの例
対応不要の発火が多い 閾値を上げる、継続時間を延ばす、件数の条件を加える、通知をやめて記録だけにする
毎回同じ対応をしている 対応を自動化する。人を呼ぶ必要がないか考える
見逃しがあった 閾値を下げる、症状側の指標を足す
発火が遅く、影響が広がっていた 継続時間を短くする、比率や変化の指標を足す
何か月も一度も発火していない 本当に必要か確かめる。不要なら消す

対応不要の発火が多いアラートを放置すると、通知そのものが読まれなくなります。この問題は アラート疲れ の記事で詳しく扱っています。監視する項目そのものについては データベース監視とは を、統計的に異常を見つける方法については 異常検知 の記事を参照してください。

閾値の管理を続けるには

ここで紹介した SQL を cron で定期的に実行して閾値を更新したり、監視ツールの継続時間の条件や異常検出の機能を使ったりすれば、閾値の管理を少しずつ自動にできます。それでも、指標が増えるほど、曜日ごとの基準や例外の日、誤報の記録と見直しを人が続けるのは大変になります。

AI に任せる方法もあります。Scandock は、AI がデータベースを監視するサービスで、固定の閾値ではなく、値の推移、過去の発火とその結末、プロジェクトに蓄積した知識を踏まえて、異常かどうかを AI が判断します。異常なしと判断した理由も残るため、なぜ通知しなかったのかを後から確かめられます。注文数や決済の成否のような KPI も、AI がテーブルの構成と実データを読んで監視の対象に含めます。

参考