rateはなぜ整数をくれず、leはなぜ捨ててはいけないのか
一言でいうと
PromQLで最も多い間違いは2つ、rateをsumのあとに書くことと、分位数の計算でleを捨てることです。どちらもエラーを出さず、もっともらしい数字を返すため、ダッシュボードに何か月も生き残ります。
なぜ必要なのか
カウンターはそのまま描くと右上がりの直線で、何の情報もありません。そのためrateで毎秒の増加率を見ます。ところがrateは単純な割り算ではなく、外挿(extrapolation)を行います。範囲内の最初のサンプルと最後のサンプルの差を求めたうえで、サンプルが範囲の境界にぴったり付いていない分を、比例で引き延ばします。そのためincreaseは3.4のような値を返します。エラーがちょうど3件でも、そうなります。これはバグではなく定義であり、「ちょうどN件」が必要な計算には使いません。
どう動くのか
rateの3つの落とし穴
1つ目に、ウィンドウがスクレイプ間隔に比べて狭すぎてはいけません。rateはウィンドウ内にサンプルが最低2つ必要です。スクレイプが15秒なのにウィンドウが20秒だと、サンプルが1つだけの瞬間が生じて結果が空になり、グラフでは穴として、アラートでは「条件不成立」として現れます。経験則はウィンドウ ≥ スクレイプ間隔 × 4で、アラートには5分以上を使います。
2つ目に、順序です。
# 틀림 — 카운터를 먼저 더하면 파드 재시작(리셋)이 감지되지 않는다
rate(sum(http_requests_total) by (route)[5m:])
# 맞음 — rate 를 먼저, 집계는 그다음
sum(rate(http_requests_total[5m])) by (route)
カウンターのリセット補正は、時系列1つ1つにかけて初めて正しくなります。rateとincreaseは、値が直前のサンプルより小さくなる瞬間をすべてリセットとみなし、減少する直前の値を増加分に加えます。合計を先に出すと、このルールがPodごとのカウンターではなく合計にかかってしまい、結果は場合によって2方向にずれます。
- Pod1つが再起動して合計が減ると、
rateは合計全体がリセットされたとみなして、減少する直前の合計全体を増加分に加えます。再起動のたびにリクエスト率が急上昇する偽のスパイクが生じ、デプロイのたびにトラフィックが急増したように見えます。 - 再起動したPodの累積値が小さく、2つのサンプルの間に他のPodが増やした分に隠れて合計が減らなければ、リセットにまったく気づきません。このときは、再起動したPodが失った累積値の分だけ、リクエスト率が実際より低く出ます。
3つ目に、irateは最後の2つのサンプルだけを見ます。ダッシュボードで瞬間的な反応を見るには使えますが、アラートには絶対に使いません。ノイズ1回で発火します。
lookback deltaとstaleness
インスタントベクトルセレクターは、評価時点Tから過去に最大5分(既定のlookback delta)まで遡って、最新のサンプルを使います。ちょうどT時点にサンプルがなくてもよい理由がこれです。ただし、その間にstale markerがあれば、時系列は結果から外れます。
histogram_quantileの線形補間
le="1" 누적 9812
le="2.5" 누적 9993
목표 = 0.99 × 10000 = 9900
추정 = 1.0 + (9900 - 9812) / (9993 - 9812) × (2.5 - 1.0) = 1.729초
1.729秒という精密に見える数字が出ますが、このバケットの中の181件が実際にどこにあるのかは、誰にもわかりません。すべて1.05秒かもしれず、すべて2.4秒かもしれません。ここから実務のルールが3つ出てきます。SLOのしきい値とちょうど同じバケット境界を入れます。関心のある区間を細かく置きます。最上段の有限のバケットを実際のタイムアウトより大きくします。p99が最後の有限の境界を超えると、関数はその境界値に張り付いてしまい、どれだけ悪いのかがわからなくなります。
そして、集計するときはleを必ず残します。
# 틀림 — le 를 버리면 버킷이 뭉개져 의미 없는 숫자가 나온다
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (route))
# 맞음
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, route))
同じ理由で、avg(histogram_quantile(...))も間違いです。分位数を平均する代わりに、バケットのカウンターを先に合算して、その上で分位数を求めます。
試験が好きな残り
| 要素 | 核心 |
|---|---|
offset / @ |
相対的な移動 / 絶対エポック時点の指定 |
bool |
比較の結果を、フィルタリングの代わりに0・1の値にします |
on / ignoring |
二項演算でマッチさせるラベルの指定 |
group_left / group_right |
多対1・1対多のマッチングを許可します |
unless |
右側とマッチする時系列を、左側から除去します |
| サブクエリ | expr[30m:1m](範囲:解像度) |
predict_linear |
ゲージの線形回帰による外挿、容量アラート用 |
absent |
時系列がないとき1を返します |
現場での姿
ホームラボのダッシュボードで最も長く生き残っていたバグは、by (leの欠落でした。値が出て、グラフも描かれ、もっともらしく動きさえします。見つける方法は1つだけです。histogram_quantileが入ったクエリをすべて検索して、leが集計句にあるかを目で確認することです。ほとんどのチームで、1つくらいは抜けています。
カーディナリティの事故は、たいていデプロイの直後に階段状に現れます。prometheus_tsdb_head_seriesをグラフに表示しておき、デプロイの時刻と重ねて見れば、原因のコミットがすぐに出てきます。どのメトリクスが犯人かは、topk(10, count by (__name__)({__name__=~".+"}))の1行で済みます。
次のラボですること
/root/pca-promql/の下に、8つのクエリをファイルとして書きます。ルートごとのリクエスト率とエラー率、leを守ったp99、カーディナリティの診断、predict_linearによるディスク予測、低トラフィックでの幽霊アラートを防ぐANDゲート、upとabsentを組み合わせたスクレイプ健全性の確認、そして最後に、2つのウィンドウをANDでつないだバーンレートの式までです。採点は文字列のマッチングなので、指示されたメトリクス名とウィンドウを正確に書いてください。