監視を始めると、最初に悩むのが閾値 (しきいち: アラートを出す境目の値) です。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 がテーブルの構成と実データを読んで監視の対象に含めます。
参考
- Google SRE Book: Monitoring Distributed Systems
- Google SRE Workbook: Alerting on SLOs
- Prometheus: Alerting rules
- AWS CLI: put-metric-alarm
- Amazon CloudWatch: Using CloudWatch anomaly detection
- PostgreSQL: Aggregate Functions
- PostgreSQL: Set Returning Functions
- PostgreSQL: Date/Time Functions and Operators