はじめに:静的しきい値がサーバ監視で壊れる理由
Prometheusでサーバを監視していると、必ず一度は「CPU使用率90%超えたらアラート」のような静的しきい値を書いた経験があるはずです。しかし本番環境のメトリクスは、
- トラフィックの日内変動でベースライン自体が動く(深夜は閑散、昼はピーク)
- スクレイプ間隔に由来する自己相関ノイズが乗る(1点のノイズが次のスクレイプにも残る)
- 緩やかなドリフト(メモリリークによるレイテンシの徐々の悪化など)と突発的なスパイク(一時的な負荷集中)の両方が起こる
という性質を持つため、固定のしきい値では「昼は誤検知だらけ、深夜は異常を見逃す」という典型的な失敗に陥ります。
この問題は本質的にフィルタリングの問題です。本ブログでは指数移動平均(EMA)フィルタやカルマンフィルタの理論を扱ってきましたが、本記事ではこれらをそのままPrometheusメトリクスの異常検知に応用し、実際に合成メトリクスへ異常を注入してシミュレーションし、検知性能を定量比較します。時系列異常検知の手法体系そのものは時系列データの異常検知:統計的手法からカルマンフィルタまでで解説済みのため、本記事はサーバ監視という具体的なユースケースに絞り、パラメータチューニングが検知性能に与える影響まで踏み込みます。
2つのアプローチ
アプローチ1:EWMA + 適応的しきい値(ボリンジャーバンド型)
EMAフィルタでメトリクスのレベルを平滑化し、同時に残差の分散もEWMAで追跡することで、ボリンジャーバンドのように平均 ± kσ の適応的なバンドを作ります。
\[ \hat{\mu}_t = (1 - \alpha) \hat{\mu}_{t-1} + \alpha x_t \tag{1} \] \[ \hat{\sigma}^2_t = (1 - \alpha) \hat{\sigma}^2_{t-1} + \alpha (x_t - \hat{\mu}_{t-1})^2 \tag{2} \] \[ \text{alarm}_t = \left| x_t - \hat{\mu}_{t-1} \right| > k \hat{\sigma}_{t-1} \tag{3} \]ここで \(\hat{\mu}_{t-1}\) は「1ステップ前のレベル」で、判定時点ではまだ最新の観測値 \(x_t\) を取り込んでいない点が重要です(取り込んでから判定すると、異常値自身がバンドを押し広げてしまい検知が鈍る)。\(\alpha\) は平滑化係数、\(k\) はしきい値の厳しさを制御します。
アプローチ2:スカラーカルマンフィルタ(ローカルレベルモデル)
カルマンフィルタを1次元のローカルレベルモデル(状態遷移 \(A=1\) のランダムウォーク+観測ノイズ)として適用します。
\[ x_k = x_{k-1} + w_{k-1}, \quad w_{k-1} \sim \mathcal{N}(0, Q) \tag{4} \] \[ z_k = x_k + v_k, \quad v_k \sim \mathcal{N}(0, R) \tag{5} \]予測共分散 \(P_{k|k-1} = P_{k-1|k-1} + Q\) 、イノベーション \(y_k = z_k - \hat{x}_{k|k-1}\) 、イノベーション共分散 \(S_k = P_{k|k-1} + R\) とすると、異常判定は次のように行います。
\[ \text{alarm}_k = |y_k| > k \sqrt{S_k} \tag{6} \]EWMAが単一のパラメータ \(\alpha\) でレベルと分散の両方を平滑化するのに対し、カルマンフィルタはプロセスノイズ \(Q\) (メトリクス自体がどれだけ変動しうるか)と観測ノイズ \(R\) (スクレイプのノイズの大きさ)を分離してモデル化できる点が理論上の利点です。
シミュレーション設計
numpyで擬似的なPrometheusメトリクス(リクエストレイテンシ、単位ms)を生成します。実際のスクレイプ間隔15秒を想定し、\(N=2000\) 点(約8.3時間分)を生成しました。
import numpy as np
np.random.seed(42)
N = 2000
t = np.arange(N)
# ベースライン: 緩やかな日内変動(トラフィック増減)
baseline = 50 + 8 * np.sin(2 * np.pi * t / N)
# AR(1)自己相関ノイズ(スクレイプ間隔の相関を模擬)
phi, sigma_e = 0.6, 3.0
noise = np.zeros(N)
for i in range(1, N):
noise[i] = phi * noise[i - 1] + np.random.randn() * sigma_e
x = baseline + noise
ここに4種類の異常を注入します。
| 異常 | 区間(スクレイプ番号) | 継続長 | シナリオ |
|---|---|---|---|
| スパイク | [400, 406) | 6点 | 一時的な負荷急増(+90ms) |
| 緩やかな悪化(creep) | [900, 1150) | 250点 | メモリリーク様の劣化(50点かけて+60msにクリープし持続) |
| ステップ変化 | [1500, 1550) | 50点 | デプロイ起因の劣化(+40ms、突発) |
| 微小ブリップ | [1750, 1754) | 4点 | ノイズに埋もれがちな軽微な異常(+22ms) |
いずれも「ありそうな」障害パターンを意図的に選んでいます。スパイクとステップは検知しやすい急峻な変化、creepは検知が難しい緩やかな変化、ブリップはノイズフロアに近い微小な変化です。
検知結果
両手法とも、EWMA \(\alpha=0.05\) 、カルマンフィルタ \(Q=0.5,\ R=9.0\) (\(R=\sigma_e^2\) )、しきい値 \(k=3\sigma\) で実行しました。

def evaluate(alarms, anomaly_windows, buffered_mask, n):
tp_events, latencies = 0, []
for name, s, e in anomaly_windows:
window_alarms = np.where(alarms[s:e])[0]
if len(window_alarms) > 0:
tp_events += 1
latencies.append(int(window_alarms[0]))
else:
latencies.append(None)
fp_count = int(np.sum(alarms & ~buffered_mask))
return tp_events, latencies, fp_count, int(np.sum(alarms))
実行結果は以下の通りです。
| 手法 | 検知イベント数 | 誤検知(FP)点数 | 総アラーム点数 |
|---|---|---|---|
| EWMA + 適応的しきい値 | 4/4 | 4 | 13 |
| スカラーカルマンフィルタ | 4/4 | 11 | 45 |
両手法とも4つの異常すべてを検知できましたが、カルマンフィルタの誤検知点数(11点)はEWMA(4点)の2.75倍でした。検知遅延(イベント開始からアラームまでのスクレイプ点数)は両手法で完全に一致し、スパイク・ステップ・ブリップは即座(遅延0点)、creepは緩やかな立ち上がりのため10点の遅延で検知されました。
平滑化性能(真のベースラインに対するRMSE)も比較すると、raw(生データ)21.94に対し、EWMAレベル20.35、KF推定21.28と、AR(1)自己相関ノイズの下では両手法の平滑化性能に大差はありませんでした。これは、EWMAとローカルレベルモデルのカルマンフィルタが数学的に近い関係にある(定常状態でのカルマンゲインは固定のEWMA係数に収束する)ためで、この実験のようにパラメータを個別にチューニングした場合、性能差の大部分は「モデルの違い」ではなく「しきい値の効き方の違い」から生じることを示しています。
パラメータ感度:Qを上げすぎると異常を"追いかけて"見逃す
上記の結果だけを見ると「EWMAの方が優秀」に見えますが、これはパラメータの選び方に強く依存します。EWMAの \(\alpha\) とカルマンフィルタの \(Q\) (\(R=9.0\) 固定)を振って、検知率と誤検知点数のトレードオフを調べました。

alphas = [0.01, 0.02, 0.05, 0.1, 0.2, 0.3]
for alpha in alphas:
lvl, var, alarms = ewma_detector(x, alpha=alpha, k=3.0)
tp, lat, fp, total = evaluate(alarms, anomaly_windows, buffered_mask, N)
# alpha=0.01 -> 3/4検知(blipを見逃す), FP=2
# alpha=0.05 -> 4/4検知, FP=4
# alpha=0.30 -> 4/4検知, FP=33(感度が高すぎて誤検知急増)
Qs = [0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0]
for Q in Qs:
est, istd, alarms = kalman_detector(x, Q=Q, R=9.0, k=3.0)
tp, lat, fp, total = evaluate(alarms, anomaly_windows, buffered_mask, N)
# Q=0.01 -> 4/4検知だがFP=118(バンドが狭すぎてノイズに反応)
# Q=1.0 -> 4/4検知, FP=4(EWMAの最良ケースに匹敵)
# Q=2.0, 5.0 -> 3/4検知(creepを見逃す)
結果は次の通りでした。
EWMA(\(\alpha\) 感度):
| \(\alpha\) | 検知数 | FP点数 | 見逃したイベント |
|---|---|---|---|
| 0.01 | 3/4 | 2 | blip |
| 0.02 | 4/4 | 2 | なし |
| 0.05 | 4/4 | 4 | なし |
| 0.10 | 4/4 | 8 | なし |
| 0.20 | 4/4 | 21 | なし |
| 0.30 | 4/4 | 33 | なし |
カルマンフィルタ(\(Q\) 感度、\(R=9.0\) 固定):
| \(Q\) | 検知数 | FP点数 | 見逃したイベント |
|---|---|---|---|
| 0.01 | 4/4 | 118 | なし |
| 0.05 | 4/4 | 63 | なし |
| 0.10 | 4/4 | 38 | なし |
| 0.50 | 4/4 | 11 | なし |
| 1.00 | 4/4 | 4 | なし |
| 2.00 | 3/4 | 1 | creep |
| 5.00 | 3/4 | 0 | creep |
この結果には実務上重要な示唆が2つあります。
- \(Q\) を大きく(\(\alpha\) を大きく)すると誤検知は減るが、緩やかな異常(creep)を見逃し始める。\(Q=2.0\) 以上でメモリリーク様のcreepイベントが検知できなくなりました。これは、\(Q\) を大きくするとフィルタが観測値に強く追従するようになり、緩やかな悪化を「正当なレベル変化」として吸収してしまうためです。最も検知したい種類の異常(じわじわ悪化する障害)が、最も誤検知が少なく見える設定で見逃されるという逆説が起きています。
- カルマンフィルタは \(Q=1.0\) まで調整すればEWMAの最良ケース(FP=4)に匹敵する性能を出せますが、そこまで到達するのに\(Q\) を100倍以上(0.01→1.0)動かす必要がありました。EWMAは\(\alpha=0.02\) 付近で早々に好バランスに達しており、パラメータ探索のコストという観点ではEWMAの方が扱いやすいという結果になりました。
このように、「EWMAとカルマンフィルタのどちらが優れているか」という問いは、モデルそのものよりもチューニングの容易さとパラメータの落とし穴に還元されることが、今回のシミュレーションから定量的に確認できました。
Prometheusへの組み込み方針(概要)
本記事の主眼はPythonでの検証ですが、実際にPrometheusへ組み込む場合の大まかな方針を触れておきます。
- recording rule で
deriv()やavg_over_time()を使い、EWMA相当の平滑化系列を別メトリクスとして事前計算しておく - しきい値判定は Alertmanager 側のルールで
abs(raw - smoothed) > k * stddev_over_time(...)のような形で表現する - カルマンフィルタは逐次更新が必要なため、PromQLだけで完結させるのは難しく、実運用では sidecar やexporterでフィルタ計算を行い、結果(推定値・信頼区間)を別メトリクスとしてPrometheusにexportする構成が現実的
いずれの方式でも、しきい値のk(何σでアラートするか)とモデルパラメータ(\(\alpha\) または\(Q\) )は障害の性質(急峻か緩やかか)に応じてチューニングが必要という結論は変わりません。
まとめ
- 静的しきい値は日内変動のあるPrometheusメトリクスと相性が悪く、EWMA適応的しきい値かカルマンフィルタのイノベーションベースの判定が有効
- 今回のシミュレーションでは両手法とも4種の異常すべてを検知できたが、パラメータを個別最適化した比較ではカルマンフィルタの誤検知点数がEWMAの2.75倍だった(各手法をデフォルトに近いパラメータで比較した場合の結果であり、優劣を一般化するものではない)
- カルマンフィルタの\(Q\) を大きくしすぎると、緩やかな異常(メモリリーク様のcreep)をモデルが追従して吸収し、見逃すようになる — これは誤検知の少なさだけを見てパラメータを選ぶと踏みやすい落とし穴
- EWMAは少ないパラメータ探索で好バランスに到達しやすく、実運用での扱いやすさで優位
サーバ監視のようにノイズと非定常性を伴うデータに対しては、フィルタリング理論の基礎(EMA、カルマンフィルタ)を理解した上で、実データでパラメータ感度を検証する姿勢が欠かせないことを、今回のシミュレーションはあらためて示しています。
関連記事
- 指数移動平均(EMA)フィルタの周波数特性 - 本記事のEWMA適応的しきい値の基礎となるEMAフィルタの理論と周波数特性を解説しています。
- カルマンフィルタの理論とPython実装 - 本記事で使ったスカラーカルマンフィルタの一般形(状態空間モデル、予測・更新ステップ)を解説しています。
- 時系列データの異常検知:統計的手法からカルマンフィルタまで - 異常検知手法の全体像(点異常・区間異常の分類含む)を体系的に整理しています。本記事はその応用としてPrometheus監視データに特化しています。
- 信号処理におけるフィルタリング手法の基礎 - カルマンフィルタ、EKF、UKF、粒子フィルタの概要を体系的に解説しています。
参考
- Roberts, S.W. (1959). “Control Chart Tests Based on Geometric Moving Averages.” Technometrics.
- Welch, G., & Bishop, G. (2006). “An Introduction to the Kalman Filter.” UNC Chapel Hill TR 95-041.
- Prometheus公式ドキュメント:Recording Rules
関連ツール
- DevToolBox - 開発者向け無料ツール集 - JSON整形、正規表現テスターなど85種類以上の開発者向けツール
- CalcBox - 暮らしの計算ツール - 統計計算、複利計算など61種類以上の計算ツール