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

試行を数えるか、操作を数えるか — リトライで分母が膨らむ

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

値は測りますが、何を1件と数えるかは決めます。今回決めるのは単位です。

まず結論

クライアントが最大3回まで試す100操作の実験で、同じ nginx ログが、試行で数えると 69.23%、操作で数えると 99.00%、初回の試行だけなら 66.00% になりました。

リトライで成功率が良くなるかどうかは、1件を試行と操作のどちらに取るかを言わないと決まりません。nginx の1行は、利用者に返した応答(`$status`)と upstream への試行(`$upstream_status`)という2つの単位を並べて持っています。

? 読む前に予測

100操作を、失敗したら最大3回まで試すクライアントで流しました。最終的に成功を受け取ったのは99操作です。同じ実験の nginx アクセスログを「1行=1件」で数えた成功率は、どれに近いでしょうか。

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

抽象原理

この回で決めること — 1件は試行か、操作か

この回で決めるのは単位です。 1件を何と取るかには少なくとも3通りあります。利用者に返した1応答(nginx の $status)、upstream への1試行($upstream_status の各値)、クライアントの1操作(再送をまとめたもの)。リトライが無ければ3つは一致します。リトライがあると、同じログから3つの違う数字が出ます。

原典も数え分けています。SRE Book 第22章は 3 retries (4 attempts) と書き、第21章は図の説明に corresponding to 1,000 initial requests, not counting retries と断っています。

第3回「原因ではなく、返ったもので数える」でログの列として出した $upstream_status を、ここで定義します。

keeps status code of the response obtained from the upstream server. Status codes of several responses are separated by commas and colons like addresses in the $upstream_addr variable.

nginx が次の server に回すと、1行の中に試行が複数並びます。 どの失敗で回すかは proxy_next_upstream が決め、既定は error timeout です。502 を回すには http_502 を書きます。

利用者の1要求が、nginx の中で2試行になる(8082 の偶数番の要求)
利用者の1要求GET /a-8082
proxy_next_upstream error timeout http_502
1回目 → 9002502
2回目 → 9001200
ログは1行
status=200応答で数えると成功1
upstream_status="502, 200"試行で数えると失敗1・成功1
  • ・利用者は 200 だけを受け取る。502 は $upstream_status にしか残らない
  • ・$upstream_status は第3回でログの列として出した値。定義はこの回で扱う
具体実物

同じ6行が、6/6 にも 6/9 にもなる

1つの nginx コンテナの中に、常に 200 を返す 9001 と、常に 502 を返す 9002 を置きました(環境: Docker 28.4.0 arm64 / nginx:1.30.4-alpine / 2026-09-28)。

lab の nginx.conf(抜粋。前段3つの server ブロックは1行ずつに要約)設定ファイル
log_format sli '$request_method $request_uri status=$status upstream_status="$upstream_status" upstream_addr="$upstream_addr" op=$http_x_op_id att=$http_x_attempt';
server { listen 9001; access_log off; location / { return 200 "ok from 9001\n"; } }
server { listen 9002; access_log off; location / { return 502 "bad from 9002\n"; } }
upstream pair_default {
server 127.0.0.1:9001;
server 127.0.0.1:9002;
}
upstream pair_nofails {
server 127.0.0.1:9001 max_fails=0;
server 127.0.0.1:9002 max_fails=0;
}
# 8080: proxy_pass http://pair_default; proxy_next_upstream error timeout http_502;
# 8081: proxy_pass http://pair_nofails;
# 8082: proxy_pass http://pair_nofails; proxy_next_upstream error timeout http_502;

前段の3つに GET を6回ずつ、8082 の設定に POST を4回送り、nginx のログを $status と $upstream_status で数えました。

count.py の出力((a) の部分、run1.log L304-315。2回目の実行も同一)コマンド出力
== (a) a-8080: 6 lines
by $status : 200=6 non200=0 -> 6/6 = 1.0000
by $upstream_status : 200=6 non200=1 -> 6/7 = 0.8571
== (a) a-8081: 6 lines
by $status : 200=3 non200=3 -> 3/6 = 0.5000
by $upstream_status : 200=3 non200=3 -> 3/6 = 0.5000
== (a) a-8082: 6 lines
by $status : 200=6 non200=0 -> 6/6 = 1.0000
by $upstream_status : 200=6 non200=3 -> 6/9 = 0.6667
== (a) post-8082: 4 lines
by $status : 200=2 non200=2 -> 2/4 = 0.5000
by $upstream_status : 200=2 non200=2 -> 2/4 = 0.5000

8082 は偶数番の3行が upstream_status="502, 200" で、$status で 6/6、$upstream_status で 6/9 です。8081 は http_502 を書いていないので 502 がそのまま利用者に返り、どちらで数えても 3/6。POST は non_idempotent を書いていないので次へ回らず、2/4 です。

✓ ここまでの確認

8082(`proxy_next_upstream` に `http_502`、`max_fails=0`)へ GET を6回送り、利用者は6回とも 200 を受け取りました。`$upstream_status` の各値を1件と数えた成功率はどれですか。

抽象原理

クライアントの再送は、サーバには1行ずつ届く

nginx の中の再試行は1行に畳まれました。クライアントの再送は違います。1試行が1つの要求として届き、1行ずつ書かれます。

SRE Book 第21章は From the point of view of our load balancing policies, retries of requests are indistinguishable from new requests. と書きます。主語は Google の負荷分散の方針で、サーバ一般の話ではありません。同じ章は区別させる方式も書いています。

A third approach has clients include a counter of how many times the request has already been tried in the request metadata. For instance, the counter starts at 0 in the first attempt and is incremented on every retry until it reaches 2

試行番号と操作IDが要求に載っていれば、サーバ側のログからも操作単位を組み立て直せます。 載っていなければ、ログで数えられるのは試行だけです。

具体実物

同じ143行が、69.23% にも 99.00% にもなる

nginx の 8090 の先に自前アプリ(python:3.12-slim、Python 3.12.14)を置きました。アプリは sha256("l5:op:attempt") を [0,1) に写し、0.3 未満なら 500 を返します。クライアントは100操作を送り、500 なら最大3試行まで再送し、X-Op-Id と X-Attempt: 0,1,2 を付けます。

クライアントの要約(run1.log L116-120)と count.py の (b)(L316-321)コマンド出力
---- client summary ----
operations : total=100 ok=99 fail=1 availability(op)=99/100=0.9900
attempts : total=143 200=99 500=44 availability(attempt)=99/143=0.6923
attempts per op : 1x66 2x25 3x9
first attempt only (= no retry): 200=66/100=0.6600
== (b) /work nginx log, 1 line = 1 client attempt: 143 lines
by $status : 200=99 non200=44 -> 99/143 = 0.6923
by $upstream_status : 200=99 non200=44 -> 99/143 = 0.6923
grouped by op header: ops=100 ok=99 fail=1 -> 99/100 = 0.9900
ops with all attempts failed: ['86'] (attempt-unit lines they contribute: 3, op-unit: 1)
500 lines total=44; of which from all-failed ops=3, from ops that later succeeded=41
  • 試行単位: 良い=200 の行 99、全件=143 行 → 99÷143 = 69.23%
  • 操作単位: 良い=成功した操作 99、全件=100 操作 → 99÷100 = 99.00%
  • 初回の試行だけ: 66÷100 = 66.00%

分母の143は 66×1 + 25×2 + 9×3。500 の44行のうち41行は、利用者が最後には成功を受け取った操作の行です。 逆に、3回とも失敗した op=86 は、試行単位では3件、操作単位では1件です(run1.nginx.log L147-149)。

✓ ここまでの確認

実演 (b) で、nginx のログだけから操作単位の 99/100 を出せたのはなぜですか。

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

nginx の次サーバ転送(8082)

利用者が受け取るのは 200 だけです。$status で数えると 6/6 で、502 は $upstream_status の中にしか残りません。応答の単位に比べて、分母は6から9になりました。

8082 の2行(run1.log L144-145)コマンド出力
GET /a-8082 status=200 upstream_status="200" upstream_addr="127.0.0.1:9001" op=- att=-
GET /a-8082 status=200 upstream_status="502, 200" upstream_addr="127.0.0.1:9002, 127.0.0.1:9001" op=- att=-
具体例 B

クライアントの再送(8090)

試行ごとに別の行が書かれます。行で数えると 99/143 で、操作の単位に比べて分母は100から143になりました。ヘッダで束ねれば 99/100 です。

全試行が失敗した op=86(run1.nginx.log L147-149)コマンド出力
GET /work status=500 upstream_status="500" upstream_addr="172.21.0.2:9000" op=86 att=0
GET /work status=500 upstream_status="500" upstream_addr="172.21.0.2:9000" op=86 att=1
GET /work status=500 upstream_status="500" upstream_addr="172.21.0.2:9000" op=86 att=2

片方では再試行が成功率を100%に見せ、もう片方では69%に見せました。共通する構造は何でしょうか。

それぞれで「1行」「1値」「1操作」がいくつあるかを数えてみてください。

この章のまとめ

  • この回で決めるのは単位。1応答($status)、1試行($upstream_status の各値)、1操作(ヘッダで束ねる)。
  • 8082 の同じ6行で、$status なら 6/6、$upstream_status なら 6/9。"502, 200" が 9002 に当たるたびに出たのは max_fails=0 の 8082(6行中3行)。既定の max_fails=1 の 8080 では最初の1回だけだった(6/7)。
  • 既定の proxy_next_upstream は error timeout。502 は http_502 を書いたときだけ次へ回る。POST は non_idempotent が無ければ回らない。
  • 同じ143行が、試行単位で 69.23%、操作単位で 99.00%、初回だけで 66.00%。500 の44行のうち41行は、最後に成功した操作のもの。
  • 試行番号と操作IDを要求に載せれば、サーバ側のログからも操作単位を組み立て直せる。載せなければ数えられるのは試行だけ。
  • 原典も数え分けている。3 retries (4 attempts)、3層なら 4^3 = 64 試行。

自分の現場で確かめる

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

理解度チェック

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

問題 1 / 3正解: 0

SRE Book 第22章の例で、JavaScript・フロントエンド・バックエンドの3層がそれぞれ `3 retries (4 attempts)` をすると、利用者の1操作はデータベースに最大何試行を生みますか。

用語集(3語)
試行(attempt)と操作(operation)
操作は利用者の1回の意図、試行はそのために送った1回の要求。1操作は1〜複数試行になる。SRE Book 第22章は `3 retries (4 attempts)` と数え分ける。
$upstream_status
nginx が upstream から得た応答の状態コード。複数の server に当たった試行はカンマ区切りで並ぶ。`$status` は利用者に返した1応答の状態コード。
proxy_next_upstream
どの失敗で次の server に回すかを決める nginx の指令。既定は `error timeout`。`http_502` などは書いたときだけ対象になる。