Dev Study
← SLI を数える — 分母と分子を決める
レッスン 7 / 10·約 8 分·第28期 SLI を数える

レイテンシも数えた比 — 閾値の下に何件あったか

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口今ここ
  6. アプリ処理今ここ
  7. DB保存

可用性で決めてきた「良い」と「有効」を、レイテンシに当てます。数えるのは速さそのものではなく、線の下に入った件数です。

まず結論

レイテンシの SLI は「p95 が何秒か」ではなく、「閾値以下で返った要求が何件あったか」の比として数えられます。閾値にバケット境界があれば、件数の比は生データの割合と一致しました(1065/1200 = 0.8875)。

同じ1,200件から p95 を4通りに出すと 0.312〜0.383 秒と揺れ、バケットから出した2つは観測値ではなく推定でした。閾値が境界に無ければ、件数の式は結果を返しません。

? 読む前に予測

境界 0.1/0.2/0.3/0.45 の histogram で、0.3 秒以内の割合を `le="0.3"` の件数 ÷ 全件で出します。生データから数えた割合と比べるとどうなるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

速さを1つの値で測るか、線の下を数えるか

この回で決めることは、レイテンシの「良い」です。可用性では状態コードで良い・悪いを分けました。レイテンシでは、閾値を1本引き、その下に入った要求を良いと数えます。数えるのは件数なので、SLI はこれまでと同じ「良い÷有効」の比になります。もう1つ、失敗した要求を分子と分母のどちらに入れるかも決めます。

SRE Workbook 第2章の Table 2-1 は、レイテンシの SLI をこう定義します。

The proportion of requests that were faster than some threshold.

一方、同じ章の Table 2-2 は、レイテンシを histogram_quantile(0.9, ...) と (0.99, ...) で計算しています。割合の定義と百分位の計算が、同じ章に並んでいます。その間の段落が理由を書いています。

Generally speaking, it is better to count the slow requests than to approximate them with a histogram. But, because that information isn’t available, we use the histogram provided by our monitoring system.

数えるほうが良いが、手元に無いので histogram で近似した、までです。「百分位を使うな」とは書いていません。

1件の要求が、2つの読み方に分かれる
要求1件の所要時間例: 0.27 秒
上限がその値以上の全バケットに +1
累積バケットle=0.1 / 0.2 / 0.3 / 0.45 / +Inf の件数
どう読むか
le="0.3" ÷ 全件件数の比。補間なし
histogram_quantileバケット内を一様と仮定した推定
  • ・バケットが保持するのは区間ごとの件数だけ。個々の値は残らない
  • ・閾値が境界に一致するときだけ、件数の比は補間を含まない
具体実物

同じ1,200件を、比と p95 で読む

実演の環境は Docker Desktop 4.47.0(arm64)、prom/prometheus:v3.15.0、python:3.14-slim(Python 3.14.7)です。自前アプリを3レプリカ置き、app-a に600件、app-b と app-c に300件ずつ流しました。app-c は4件に1件を 500 で返し、その 500 は約 10 ms で返ります(run1 の最大 0.0156 秒)。アプリは境界 0.1/0.2/0.3/0.45 の classic histogram を出し、別に全観測を生ログに書きます。カウンタは0から始まるので、生カウンタの比は生データと突き合わせられます。

run1 のクエリ結果(JSON から値だけ抜き、小数4桁に丸めた。生データ列は truth.txt)表
量 Prometheus 生データ
le="0.3" の件数 ÷ 全件(生カウンタ) 0.8875 0.8875 (1065/1200)
同じ比を rate()[2m] で 0.8842 -
p95 レプリカごとに推定して平均 0.3627 -
p95 バケットを足してから推定 0.3833 -
p95 全件を並べて1140番目 - 0.3362
p95 レプリカごとに並べて平均 - 0.3119

件数の比は、生データの割合と一致しました。3回実行して3回とも一致しています(3回目は境界付近の5件が移り、両方とも 0.8917 = 1070/1200)。一方、Prometheus 文書と同じ形の式(窓は 2m)で rate() を割った比は 0.880〜0.884 と揺れました。負荷が窓の中で始まって終わる条件だったためと考えられますが、原因は切り分けていません。正確だったのは件数の比であって、窓で割った比ではありません。

p95 は4通りで 0.312〜0.383 秒です。合算後の 0.3833 は補間で作られます。累積件数 [137, 688, 1065, 1200] で1140番目は 0.3〜0.45 のバケットにあり、0.3 + 0.15×(1140−1065)/(1200−1065) = 0.3833。バケットを足してから推定するのは正しい合算の手順ですが、値は生データの 0.3362 から 0.047 秒離れました。

第13期で「パーセンタイルは必ず実在の1回」と見たのは、生データを並べて選ぶときの話です。0.3362 は生ログに実在しますが、バケットから出した値は観測値ではありません。

✓ ここまでの確認

同じ1,200件から出した4つの「p95」のうち、実在する1回の観測値はどれですか(今回の実演)。

抽象原理

線が境界に無いとき、線の下に何を入れるか

件数の比が正確だったのは、閾値 0.3 がバケット境界だったからです。Prometheus 公式文書は、境界が無い場合をこう書きます。

Instead of an estimation, no result is returned at all.

さらに、境界が一部の histogram にしか無いと an incomplete result is returned, but without any warning とも書きます。どちらもエラーにならないのが厄介です。

閾値そのものは SLI 仕様の決め事です。Workbook では第2章が 450/900 ms、付録A が 400/850 ms ですが、付録A は測定期間(2018-01-01〜01-28)と 50 ms 切り上げを明記しています。第2章の実測 432/891 ms を切り上げても 400/850 にはならないので、食い違いではなく別の測定から出た例です。不等号は両文書とも散文が <、実装が「以下」(less than or equal to)で、文書間の差ではありません。

もう1つの決め事が失敗の扱いです。付録A の式は count of all "api" http_requests で割り、分子にも分母にも状態コードの条件がありません。速い 500 は良い側に入ります。ただし同じ付録に可用性 SLI が別にあり、5xx はそこで数えます。SRE Book 第6章は、失敗を混ぜると misleading calculations になりうると書いたうえで、a slow error is even worse than a fast error! と続け、除外ではなく別に追えと言います。

具体実物

OTel 既定バケットと、速い 500

同じアプリは、OpenTelemetry semconv(main コミット 094950f。既定バケット列は v1.44.0 と同一)が http.server.request.duration に勧める境界 [ 0.005, ..., 0.1, 0.25, 0.5, ... 10 ](一部省略)の histogram も出しています。0.3 はこの列にありません。

run1、同じ1,200件・閾値 0.3 秒(JSON から値だけ抜き、0.802 は丸めた値)表
式 値
SLO 用 le="0.3" ÷ 全件(速い 500 も良い) 0.8875
同上、分子から 5xx を除く・分母は全件 0.825
同上、分子・分母とも 5xx を除く 0.88
OTel 既定 le="0.3" ÷ 全件 結果なし
OTel 既定 histogram_fraction(0, 0.3) 0.802
OTel 既定 le="0.25" ÷ 全件 0.7525

境界の無い le="0.3" は "result":[] で、エラーは出ません。histogram_fraction は 0.25〜0.5 を一様と仮定し、0.7525 + 0.2×(1−0.7525) = 0.802 と推定しました。真値 0.8875 より 8.55 ポイント低い値です。

le の書き方でも空になります。Prometheus v3 は取り込み時に le を float 表記に正規化するので、{le="1"} や {le="0.30"} は結果なし、{le="1.0"} と {le="0.3"} で当たりました。

速い 500 は、分子に入れるかで 0.8875 と 0.825、分母からも外せば 0.88。同じ要求群から3つの数字が出ます。

✓ ここまでの確認

app-c は4件に1件を 0.1 秒以内で 500 にします。サービス全体で「0.3 秒以内÷全件」を出すと 0.8875 でした。これについて正しいのはどれですか。

⇄ 2つを見比べる
具体例 A

閾値 0.3 に境界を置いた histogram

le="0.3" の件数を全件で割るだけで 0.8875。生データの 1065/1200 と一致した。histogram_fraction(0, 0.3) も同じ 0.8875 を返した。

具体例 B

OTel 既定境界の histogram(0.25 の次が 0.5)

le="0.3" は結果なし。histogram_fraction は 0.802 と推定し、真値より 8.55 ポイント低い。0.25 なら境界があり、0.7525 は生データと一致した。

閾値を 0.3 から 0.25 に変えると、どちらの histogram で何が起きるでしょうか。

境界の一覧に 0.25 があるのはどちらかを見てください。

この章のまとめ

  • レイテンシの SLI も 良い÷有効の件数比 にできる。良いは「閾値以下」。
  • 閾値がバケット境界にあれば、生カウンタの比は生データと一致した(0.8875 = 1065/1200、3回とも)。rate()[2m] の比は 0.880〜0.884 と揺れた。
  • 同じ1,200件の p95 は数え方4通りで 0.312〜0.383 秒。バケットから出した値は推定で、実在の1回ではない。
  • 境界が無い、または le の表記が違うと、式は エラーでなく結果なし を返す。
  • 速い失敗を分子に入れるかで 0.8875 / 0.825 / 0.88。どれを採るかは SLI 仕様に書く。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

SRE Workbook の第2章 Table 2-3(450/900 ms)と付録A(400/850 ms)でレイテンシの閾値が違います。適切な読み方はどれですか。

用語集(3語)
累積ヒストグラムと le
境界を固定した classic histogram では、各バケットが「その値以下の件数」を数えるカウンタ。`le` は less or equal で、境界を含む上限。
histogram_quantile
バケットの件数から分位数を出す Prometheus の関数。classic histogram ではバケット内を一様と仮定して補間するので、値は推定。
histogram_fraction
指定した区間に入る割合を出す関数。区間の端がバケット境界に一致すれば件数の比、しなければ補間の推定。