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

原因ではなく、返ったもので数える — データ起因の 500 と、200 の縮退応答

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

SLI の値は測りますが、何を数えるかは決めます。今回は分子、つまり何を「良い」と数えるかを、利用者に返ったもので決めます。

まず結論

「データが揃っていない」という同じ事象でも、アプリが 500 を返せば可用性 SLI は 70.00% に下がり、既定値で埋めて 200 を返せば 100.00% のままです。どちらの数字に写るかは、アプリの書き方が決めています。

200 の縮退を nginx のログで数えるには、アプリが縮退を申告し、nginx がそれをログの列に写す必要があります。さらに品質 SLI の分子に 5xx をどう置くかで、同じログから 100.00% と 70.00% が出ます。

? 読む前に予測

100 件の要求のうち 30 件で依存先に届かず、アプリは推薦を空にして 200 を返しました。nginx のログで「5xx 以外 ÷ 全件」の可用性 SLI を出すと何% でしょうか。

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

抽象原理

同じ事象が、どの列に落ちるか

この回で決めるのは分子、つまり何を「良い」と数えるかです。分母(有効な事象)は全件のままにします。今回のログには 4xx が 0 件なので、全件と有効な事象は一致します。変えるのは分子の決め方だけで、それでも数字が 100.00% と 70.00% に割れるのを見ます。

SRE Book 第6章は、ゴールデンシグナルの Errors で失敗を3種に分けます。

The rate of requests that fail, either explicitly (e.g., HTTP 500s), implicitly (for example, an HTTP 200 success response, but coupled with the wrong content), or by policy …

同じ段落は only end-to-end system tests can detect that you’re serving the wrong content と結びます。200 に紛れた失敗は、状態コードでは捕まりません。

The Art of SLOs 参加者ハンドブックは、状態コードで数える前提をこう書きます。

This requires careful, accurate use of status codes within your system so that each code maps distinctly to either success or failure.

コードが成功か失敗のどちらかに一意に写ること。縮退した 200 は、この前提を内側から破ります。どちらに写すかを決めているのは、状態コードを書くアプリです。

「データが揃っていない」事象が、アプリの書き方で別の SLI に落ちる
データが揃っていない行がまだ無い / 依存先に届かない
アプリが返し方を決める
(a) 500 を返す原因はアプリログにだけ残る
(b) 既定値で 200X-Degraded: 1 を付ける
nginx のログの列
500 500 -可用性 SLI が下がる
200 200 1可用性は下がらず、品質 SLI に出る
  • ・nginx は縮退を検出していない。アプリの応答ヘッダを列に写しているだけ
  • ・ヘッダを出さなければ (b) は nginx のログには映らない(アプリが自分のログに書いていればそこには残る)
具体実物

同じ 100 件を、2通りに返させる

環境: Docker 28.4.0(linux/arm64)、nginx:1.31.6-alpine(nginx/1.31.6)の後ろに python:3.14-slim(Python 3.14.7、sqlite 3.46.1 の :memory:)の自前アプリ、さらに依存先(推薦サービスの模型)。乱数なしの決定的な構成で、2回実行して集計は一致しました。

  • (a) 利用者は id 1〜100 あるが、バッチで作る集計行は 70 件まで。id 71〜100 は行が無く、アプリは 500 を返す
  • (b) 集計行は 100 件揃っている。id 70 まで要求したあと依存先を止め、id 71〜100 は推薦を空にして 200 を返す
# app.py(抜粋。do_GET の本体から。インデントを詰めた)
stats = db.execute(
    "select logins, last_seen from profile_stats where user_id=?", (uid,)
).fetchone()
# (a) バッチ未完: stats が None のまま添字参照 → TypeError → 500
logins, last_seen = stats[0], stats[1]
try:
    recs = fetch_recs(uid)
    degraded = "0"
except (urllib.error.URLError, OSError, TimeoutError) as e:
    if MODE == "b":
        # (b) 依存先が落ちた: 既定値で埋めて 200、縮退を1列で申告
        recs = []
        degraded = "1"
        cause = f"deps:{type(e).__name__}"
    else:
        raise
status = 200
self._send(
    200,
    {"id": user[0], "name": user[1], "logins": logins, "last_seen": last_seen, "recs": recs},
    {"X-Degraded": degraded},
)

この 500 は、本実装が TypeError を捕まえて明示的に返したものです。nginx のログ形式は '$status $upstream_status $upstream_http_x_degraded $request_method $request_uri $request_time' です。2列目は nginx が上流(アプリ)から受け取った状態コード(第5回で扱う)、3列目はアプリの応答ヘッダ X-Degraded の値です。

nginx のログ(run1 の抜粋。上4行 (a) 行69-72、下4行 (b) 行69-72。見出し行は説明用に追加)コマンド出力
# (a) a-nginx-sli.log
200 200 0 GET /profile/69 0.000
200 200 0 GET /profile/70 0.001
500 500 - GET /profile/71 0.000
500 500 - GET /profile/72 0.001
# (b) b-nginx-sli.log
200 200 0 GET /profile/69 0.001
200 200 0 GET /profile/70 0.001
200 200 1 GET /profile/71 0.016
200 200 1 GET /profile/72 0.003

(a) の 500 行に原因はありません。原因はアプリの標準出力にだけ残っていました。

アプリのログ(run1 の抜粋。上2行 a-app.log 行73-74、下2行 b-app.log 行73-74)コマンド出力
app mode=a path=/profile/71 status=500 degraded=- cause=TypeError:'NoneType' object is not subscriptable dt=0.0001
app mode=a path=/profile/72 status=500 degraded=- cause=TypeError:'NoneType' object is not subscriptable dt=0.0000
app mode=b path=/profile/71 status=200 degraded=1 cause=deps:URLError dt=0.0156
app mode=b path=/profile/72 status=200 degraded=1 cause=deps:URLError dt=0.0022

(b) の URLError は、urllib が名前解決の失敗も接続拒否も包む例外で、このログからはどちらかを区別できません。

可用性 SLI = (全件 − 5xx) ÷ 全件 で数えると、(a) は (100 − 30) ÷ 100 = 70.00%、(b) は (100 − 0) ÷ 100 = 100.00% です。SLO 99% なら、100 件で許される悪い事象は (1 − 0.99) × 100 = 1 件。(a) の 30 件はその 30 倍で、(b) は 0 件と数えられます。

✓ ここまでの確認

実演 (a) で、参照先の行が無いため 500 になった要求の原因(TypeError)は、どこに記録されていましたか。

抽象原理

品質 SLI の分子に、何を入れるか

(b) を数えるには別の比が要ります。SRE Workbook 第2章 Table 2-1 の Quality はこう書きます。

If the service degrades gracefully when overloaded or when backends are unavailable, you need to measure the proportion of responses that were served in an undegraded state.

本回は、これを品質 SLI(縮退していない応答の割合)と呼びます。同じ章の good/total の例は total number of search results, including those that degraded gracefully と、縮退した応答を分母に含めています。Art of SLOs の仕様は The proportion of valid requests served without degrading quality. で、分母は valid requests です。

実装について、Art of SLOs は縮退の側から数えるよう勧めます。

It is therefore much easier to express this SLI in terms of "bad events" rather than "good events".

ただし「悪い = 縮退を申告した行」だけにすると、縮退を申告しない 500 は悪いに入りません。 500 の行では縮退の列が - だからです。5xx を品質 SLI の悪い側に置くか、可用性 SLI だけに任せるかは、Workbook の Table 2-1 の Quality にも Art of SLOs の Quality 節にも書かれていません(確認範囲はこの2か所)。どちらを採るかは SLI 仕様に書きます。

具体実物

同じログに、2つの品質 SLI を当てる

分母は同じ全件のまま、分子だけを2通りにしました。品質(1) = (全件 − 縮退の申告) ÷ 全件、品質(2) = (200 かつ縮退していない) ÷ 全件 です。

sli.py の出力(run1 の a-sli.txt と b-sli.txt。run2 も同一。区切り行は説明用に追加)コマンド出力
--- (a) ---
全件 N=100 status別={'200': 70, '500': 30} x_degraded別={'-': 30, '0': 70}
5xx=30 4xx=0 degraded(=1)=0 200かつdegraded=0 -> 70
可用性 SLI = (全件 - 5xx) / 全件 = (100 - 30) / 100 = 0.7000 = 70.00%
品質 SLI(1) = (全件 - degraded=1) / 全件 = (100 - 0) / 100 = 1.0000 = 100.00%
品質 SLI(2) = (200 かつ degraded=0) / 全件 = 70 / 100 = 0.7000 = 70.00%
--- (b) ---
全件 N=100 status別={'200': 100} x_degraded別={'0': 70, '1': 30}
5xx=0 4xx=0 degraded(=1)=30 200かつdegraded=0 -> 70
可用性 SLI = (全件 - 5xx) / 全件 = (100 - 0) / 100 = 1.0000 = 100.00%
品質 SLI(1) = (全件 - degraded=1) / 全件 = (100 - 30) / 100 = 0.7000 = 70.00%
品質 SLI(2) = (200 かつ degraded=0) / 全件 = 70 / 100 = 0.7000 = 70.00%

(a) では、品質(1) が 100.00% と出ます。可用性が 70.00% に落ちている同じログで、です。品質(2) は (a) も (b) も 70.00% です。品質(1) だけを「品質 SLI」として掲げると、500 の多い日ほど品質がよく見えます。

✓ ここまでの確認

実演 (a)(200 が 70 件、500 が 30 件、縮退の申告は 0 件)に、品質 SLI を「縮退を申告した行を引いた数 ÷ 全件」で当てると何% ですか。

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

(a) 行が無く 500 を返した

nginx の行は 500 500 - で、原因はアプリログにだけ残ります。可用性 SLI は 70.00% に下がります。縮退の申告だけを悪いと数える品質 SLI では、この 30 件は悪いに入りません。

(a) の集計(run1)表
式 値
可用性 (全件-5xx)/全件 70.00%
品質(1) (全件-縮退の申告)/全件 100.00%
品質(2) (200かつ縮退なし)/全件 70.00%
具体例 B

(b) 依存先に届かず 200 を返した

nginx の行は 200 200 1 で、状態コードはすべて成功です。可用性 SLI は 100.00% のままです。アプリが X-Degraded を出したので、品質 SLI にだけ 70.00% として現れます。

(b) の集計(run1)表
式 値
可用性 (全件-5xx)/全件 100.00%
品質(1) (全件-縮退の申告)/全件 70.00%
品質(2) (200かつ縮退なし)/全件 70.00%

利用者が受け取れなかったものは (a) も (b) も 30 件です。数字が入れ替わった理由は、ログのどこにありますか。

nginx のログの1列目と3列目を、(a) と (b) で見比べてください。

この章のまとめ

  • この回で決めたのは分子。同じ全件の分母でも、分子の決め方で 100.00% と 70.00% が出る。
  • データが揃っていないとき、(a) 500 なら可用性 SLI は 70.00%、(b) 既定値で埋めた 200 なら 100.00%。どちらに写るかはアプリの書き方が決める。
  • 原因(TypeError・URLError)はアプリログにだけ残る。SLI は返ったものを数え、原因は内訳で追う。
  • 200 に紛れた失敗は SRE Book 第6章の implicitly。数えるにはアプリが申告し、nginx が $upstream_http_x_degraded で列に写す。
  • 品質 SLI を「縮退の申告を引いた数 ÷ 全件」だけで書くと、(a) で 100.00%。「200 かつ縮退なし ÷ 全件」なら 70.00%。5xx をどこに置くかは SLI 仕様の決め事。
  • 依存先起因の未達について Workbook 第2章が書くのはエラーバジェット方針の2派で、SLI から外す話ではない。

自分の現場で確かめる

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

理解度チェック

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

問題 1 / 3正解: 0

SRE Book 第6章は失敗を3種に分けます。実演 (b) の「推薦を空にして返した 200」はどれに当たりますか。

用語集(3語)
品質 SLI
縮退していない応答の割合。第16期の「品質」(ソフトウェア品質)とは別。Workbook 第2章 Table 2-1 は分母を応答全体とし、Art of SLOs は分母を valid requests とする。5xx をどこに置くかは SLI 仕様で決める。
グレースフルデグラデーション(縮退)
依存先の障害などで一部を諦め、既定値などで埋めて応答を返し続けること。状態コードは成功のままになりうる。
$upstream_http_<name>
nginx の変数。上流サーバの応答ヘッダの値を保持する。値が無いとアクセスログには - が記録される。