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

要求の無い仕組みは、何を1件と数えるかから決める — 鮮度・網羅・正しさ

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

これまでは要求の行を数えてきました。今回は要求の無い仕組みで、何を1件とするかを決めます。

まず結論

バッチやパイプラインには「要求」という決まった1件がありません。鮮度も網羅も、行・読み出し・実行のどれを1件とするかで、同じ表の状態から違う値が出ます。

網羅の分母を処理系自身が数えると、処理系が知らない記録は分母ごと消えます。正しさは、出力を作ったのと同じ方法で照合すると、誤りを見落としやすくなります。

? 読む前に予測

出力表100行のうち、1分以内に更新された行は20行です。同じ時点の読み出し1000件のうち980件はその20行を読んでいました。鮮度 SLI の値はいくつですか。

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

抽象原理

要求の無い仕組みには、決まった1件が無い

ここまでの SLI は、要求を1件として数えれば済みました。夜間に走るバッチや、表を更新し続けるパイプラインには、その「要求」がありません。

第15期で、SLI は測って出てきた値だと見ました。値は測ります。ただし何を1件と数えるかは、測る前に決めます。 この回で決めるのは事象の単位です。

ここで言う鮮度は、データが最後に更新されてからの経過です。第3期の HTTP キャッシュの鮮度とは別の量です。Art of SLOs 参加者ハンドブック(p.11)が挙げる鮮度の仕様はこうです。

The proportion of valid data updated more recently than a threshold.

data の1件が行なのか、読まれた回数なのかは、この1文では決まりません。Workbook 第2章 Table 2-1 は方向だけ示します。

Ideally this metric counts how many times a user accessed the data, so that it most accurately reflects the user experience.

同じ表の状態から、1件の単位を替えて数える
出力表 100行新しい20行・古い80行
読み出し 1000件980件は新しい20行へ
何を1件とするかを決める
行を1件20/100 = 20%
読み出しを1件980/1000 = 98%
  • ・どちらも同じ時点の同じ表から出た値
  • ・誰も読まない古い行60行は、行単位の値だけを下げる
具体実物

鮮度を、行と読み出しで数える

Workbook 第2章の例では、パイプラインが出力表を更新するたびに更新時刻(watermark)を記録します。実装例は2つです。

Run a periodic query across the league table, counting the total number of fresh records and the total number of records. This will treat each stale record as equally important, regardless of how many users saw the data.

Make all clients of the league table check the watermark when they request fresh data and increment a metric counter saying that data was requested. Increment another counter if the data was fresher than a predefined threshold.

これを PostgreSQL 18.6(postgres:18、aarch64)と Python 3.14.7 + psycopg 3.3.6(python:3.14-slim、Docker 28.4.0)で組みました。乱数は使わず、「今」は固定時刻 T0 です。実運用の定期クエリなら now() になります。閾値は60秒です。値は決定的な模擬の構成から出たもので、一般則ではありません。

run1.log の (a) 節(抜粋。env 行と他の節は省略。2回の実行で全行一致)コマンド出力
[a-1 periodic query] fresh rows (updated within 60s of T0) = 20, total rows = 100 -> freshness(row unit) = 20/100 = 20.00%
[a-2 client counters] data_requests = 1000, fresh (<=60s) = 980 -> freshness(read unit) = 980/1000 = 98.00%
[a-2 check via read_log] 980/1000 = 98.00%
[a-note] stale rows in table = 80; stale rows that were actually read = 20
[a-3 control: all rows refreshed] row unit = 100/100 = 100.00% ; read unit would also be 1000/1000 = 100.00% for the same 1000 reads

行単位は20%、読み出し単位は98%です。古い行80行のうち読まれたのは20行で、読まれない60行が行単位だけを下げています。読み出し単位も、古い行を読んだ20件の分だけ下がっています。全行を新しくすると行単位は100/100になりました。読み出し単位も、どの読み出しも新しい行に当たるので理屈の上で100%です(こちらは測っていません)。

Workbook は例として後者を採り、理由を1文で書きます。

From these two options, our example uses the client-side implementation, as it gives SLIs that are much more closely correlated with user experience and are straightforward to add.

Art of SLOs(p.11)も同じ向きです。if no user accesses the stale data, no expectations around the freshness of that data can be missed. ただし Workbook は行単位を誤りとは書いていません。

✓ ここまでの確認

Workbook 第2章は、表を定期的に走査して「新しい行÷全行」を出す実装について何と書いていますか。

抽象原理

分母を誰が数え、正しさをどの方法で照合するか

網羅でも単位は割れます。Table 2-1 の Coverage は1つの型の中で、バッチなら the proportion of jobs、ストリーミングなら the proportion of incoming records です。

単位の次は、分母を誰が数えるかです。

Workbook 第2章の「Pipeline freshness, coverage, and correctness」節は、網羅の分母をパイプライン自身に出させる例を挙げています。

To calculate our coverage SLI, our pipeline exports the number of records that it should have processed and the number of records that it successfully processed. This metric may miss records that our pipeline did not know about due to misconfiguration.

This metric は、パイプラインが自分で出すこの2つの件数を指します。

Art of SLOs(p.12)は、処理系の外で数える手を挙げます。some way of determining the number of valid records that resides outside of the data processing system itself, perhaps by running the equivalent of COUNT(*) on the data source.

正しさでは、どの方法で照合するかが問題になります(p.13)。

For a correctness SLI to be useful, the method of determining correctness needs to be independent of the methods used to generate the output data.

同じページは、正しさはまず good software engineering and testing practice で確保するものだと書きます。SLI は保証の手段ではありません。

具体実物

網羅と正しさを、決め方を替えて数える

元表は1000行(east 500・west 400・north 100)です。パイプラインの設定は east と west だけで、north を知りません。east の5行は処理に失敗する値にしてあります。

run1.log の (b) 節(見出し行を除き省略なし)コマンド出力
[b-source] rows per region in games: [('east', 500), ('north', 100), ('west', 400)]
[b-1 no failures] pipeline says should=900, processed=900 -> self-reported coverage = 900/900 = 100.00%
[b-1 external] COUNT(*) on games = 1000 -> coverage vs source = 900/1000 = 90.00%
[b-2 with 5 failures] should=900, processed=895 -> self-reported = 895/900 = 99.44% ; vs source = 895/1000 = 89.50%
[b-3 per-run unit] runs: [(1, '900/900'), (2, '900/900'), (3, '900/900'), (4, '900/900'), (5, '895/900')]
[b-3 per-run unit] runs with 100% (by pipeline's own count) = 4/5 = 80.00% ; record unit over the same 5 runs = 4495/4500 = 99.89%
[b-3 per-run unit, external denominator] runs that processed all 1000 source rows = 0/5 = 0.00%

自己申告の分母では100%、元表の COUNT(*) を分母にすると90%です。差の100行が設定漏れの north です。同じ5回の実行を、実行を1件として数えると 4/5 = 80%、記録を1件とすると 4495/4500 = 99.89% です。

正しさには、200人×3試合の黄金データを入れました。10人の3試合目は −3 点です。パイプラインの SQL には正の点だけを足す誤りを置き、期待値は Python で入力を単純に合計して別に作りました。

run1.log の (c) 節(抜粋。誤った10人の一覧の行を省略)コマンド出力
[c-1 independent golden check] correct = 190/200 = 95.00%
[c-1 example] golden-001: inputs=[2, 3, -3] expected=2 pipeline_output=5
[c-2 NON-independent check (same SQL as pipeline)] agree = 200/200 = 100.00% <- hides the bug
[c-3 Appendix A arithmetic] 200 tests/s * 4 weeks = 483,840,000 records; * 1e-7 = 48.384 -> budget 48 errors/4 weeks

独立に作った期待値では95%、パイプラインと同じ SQL で作ると100%で、誤りが消えます。付録A の approximately 200 tests, which are injected every 1s を4週(2,419,200秒)と 1−99.99999% で掛けると48.384件で、原文の予算 48 errors every four weeks と合います。

✓ ここまでの確認

パイプラインが自分で出す「処理すべき件数」を分母にした網羅が100%でした。Workbook 第2章がこの数え方に付けている注意はどれですか。

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

行を1件と数える鮮度(定期クエリ)

表を走査して新しい行を数えます。読まれない古い行も同じ重さで入ります。実演では20%でした。

run.py から抜粋(%s は固定した T0)設定ファイル
select count(*) filter (where updated_at >= %s - interval '60 seconds') as fresh_rows,
count(*) as total_rows from league_table
具体例 B

読み出しを1件と数える鮮度(2つのカウンタ)

読むたびに watermark を閾値と比べ、2つのカウンタを増やします。読まれない行は入りません。実演では98%でした。

run.py から抜粋設定ファイル
requested += 1 # counter: data was requested
fresh = age <= 60
if fresh:
fresh_reads += 1 # counter: data was fresher than threshold

同じ表の状態で、この2つの値が離れる条件は何でしょうか。

古い行のうち、読まれている行と読まれていない行の数を比べてください。

この章のまとめ

  • 要求の無い仕組みでは、何を1件と数えるかを先に決める。鮮度は行単位20%・読み出し単位98%。
  • Workbook 第2章は両方の実装を挙げ、利用者体験との相関で後者を例に採った。行単位を誤りとは書いていない。
  • 網羅の分母を処理系が数えると、設定漏れは見えない(100% と COUNT(*) での90%)。実行単位80%・記録単位99.89%。
  • 正しさは生成と独立に照合する(95%)。同じ SQL では100%に見える。
  • 付録A は Completeness が hours と runs、Correctness の式だけ data_requests。第2章の Coverage とは別の語。
  • 値は測る。何を数えるかは決める。決めた単位と分母の出どころを SLI 仕様に書く。

自分の現場で確かめる

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

理解度チェック

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

問題 1 / 3正解: 0

Workbook 付録A の Correctness の記述について正確なのはどれですか。

用語集(4語)
鮮度(データの)
データが最後に更新されてからの経過で決める性質。第3期の HTTP キャッシュの鮮度とは別の量。
watermark
Workbook 第2章の例で、パイプラインが出力表を更新した時刻の記録。読み手はこれを閾値と比べる。
網羅(Coverage)
Workbook 第2章 Table 2-1 のパイプラインの SLI の型。第15期の SLI 実装の性質としての coverage とは別。付録A の Completeness とも語と単位が違う。
黄金データ
正しい出力が分かっている入力データ。実データを代表し、処理の経路を広く通る必要がある。