foundations · Stage 1
ネットワーク遅延と部分失敗を層別に診断する
DNS、TCP、TLS、HTTPの時系列を分け、期限内に失敗を扱う予算を設計する。
到達目標
DNS問い合わせからHTTP応答完了までを少なくとも六イベントの時系列として図示できる
- DNSからHTTPまでの時刻、期限、再試行条件、未観測区間を含むトレース
- 輸送成功とアプリケーション成功を区別する5分説明
利用者期限から接続、TLS、応答、再試行へ時間を配分し、最悪時の合計を計算できる
- DNSからHTTPまでの時刻、期限、再試行条件、未観測区間を含むトレース
- 損失、名前解決、接続、依存遅延の仮説を反証する診断
パケット損失と依存サービス遅延の仮説を層別の時刻と再送観測で反証し、未知経路へ診断を移せる
- 損失、名前解決、接続、依存遅延の仮説を反証する診断
- 未知の多段依存で損失と依存遅延を切り分けた調査計画
能力の進行
recognize
DNS、TCP、TLS、HTTPの開始・完了イベントと各層の失敗を時系列上で識別できる
証拠: DNSからHTTPまでの時刻、期限、再試行条件、未観測区間を含むトレース
explain
TCP到達、TLS認証、HTTP応答、業務処理成功が別の契約である理由を説明できる
証拠: 輸送成功とアプリケーション成功を区別する5分説明
apply
総期限から各段の予算と再試行回数を逆算し、合計が期限を超えないトレースを作成できる
証拠: DNSからHTTPまでの時刻、期限、再試行条件、未観測区間を含むトレース
diagnose
DNS時間、接続時間、再送、server timingを比較し、損失と依存遅延を反証できる
証拠: 損失、名前解決、接続、依存遅延の仮説を反証する診断
lead
冪等性、deadline伝播、retry stormを含む障害レビューを未知の依存グラフで主導できる
証拠: 未知の多段依存で損失と依存遅延を切り分けた調査計画
なぜ重要か
「APIが遅い」という一つの観測には、名前解決、接続、暗号handshake、request送信、server処理、response転送という異なる区間が含まれる。総時間だけを見てnetwork担当またはserver担当へ責任を移すと、原因を反証できず、timeoutとretryで負荷を増幅し得る。
networkでは、相手が処理して応答だけ失われた状態と、相手へ届かなかった状態をclientから完全には区別できないことがある。特に状態を変えるPOSTを無条件にretryすると二重処理を生む。deadline、冪等key、結果照会を一つの契約として設計する必要がある。
メンタルモデル
一要求を層ではなくイベント列として観測する。各イベントに単調時計の開始・完了、残りdeadline、request IDを付ける。層の境界を越えるたびに「何が完了したと言えるか」を更新する。
timeoutは各層へ同じ値を配るのではなく、利用者の総deadlineから逆算する。再試行を許すなら、一回目のtimeout、backoff、二回目、後処理の合計が総deadline以下でなければならない。下流にも残りdeadlineを伝えないと、clientが諦めた後もserverが仕事を続ける。
説明用の総deadline 300msで、どのeventが最初の超過点となり、残りbudgetはどう変化したか。
- 接続準備 budget 120ms
説明用の総deadline 300msのうちDNS 30ms、TCP 40ms、TLS 50msを割り当てる。observed合計105ms。普遍値ではない。
- 名前解決(説明用budget)
DNS queryを送り、候補addressとTTLを得る。budget 30ms、observed 25ms、累積25ms・残り275ms。
順序: 0
- 輸送接続(説明用budget)
TCP SYN、SYN-ACK、ACKで接続状態を確立する。budget 40ms、observed 35ms、累積60ms・残り240ms。
順序: 1
- 暗号接続(説明用budget)
TLSでversion、鍵、相手のidentityを検証する。budget 50ms、observed 45ms、累積105ms・残り195ms。
順序: 2
- 名前解決(説明用budget)
- HTTP交換 budget 160ms
request 10ms、server 100ms、first-byte 20ms、body 30ms。first-byte observedで総deadlineを初めて超える。
- request送信(説明用budget)
HTTP method、target、header、bodyを送る。budget 10ms、observed 10ms、累積115ms・残り185ms。
順序: 3
- server処理(説明用budget)
handlerと依存serviceが状態を読み書きする。budget 100ms、observed 120ms、累積235ms・残り65ms。
順序: 4
- 最初のbyte(説明用budget)
statusとheaderを受け始める。budget 20ms、observed 75ms、累積310ms。ここが最初のdeadline超過点(10ms超過)。
順序: 5
- 本文完了(説明用budget)
responseを読み終える。ただし業務結果はstatusとbodyの契約で判定する。budget 30ms、observed 25ms、累積335ms・残り-35ms。
順序: 6
- request送信(説明用budget)
- 業務結果 budget 20ms
transport完了後の結果永続化へ20msを割り当てる。数値はtraceの読み方を示す説明用。
- 結果確定(説明用budget)
clientが結果を永続化し、必要なら冪等keyで後から照会できる。budget 20ms、observed 10ms、累積345ms・残り-45ms。
順序: 7
- 結果確定(説明用budget)
各eventのbudgetと累積observedを追い、first-byteの310msを最初の超過点として説明できる。これは普遍的なlatency値ではない。
動く例で考える
2000msの利用者期限へ一回のretryを収める
- 前提
- 総deadlineは2000ms。UI更新と結果保存に200ms、retry間のjitter付きbackoffに100msを確保する。状態変更操作は冪等key対応済み。
- 入力
- 一要求についてDNS、connect、TLS、time to first byte、body完了を単調時計で記録する。
- 操作
- 一回目へ800ms、backoffへ100ms、二回目へ700ms、後処理へ200ms、cancel伝播へ100msを配り、残り100ms未満では新しい試行を開始しない。
- 観測
- localhost simulatorでcold/warmを各7回測り、制御したDNS/TLS delayを含む単調時刻を保存する。最悪時は800 + 100 + 700 + 200 + 100 = 1900msで期限内に収まる。
- 結論
- 各段へ2000msを設定すると、二試行で4000ms以上になり得る。試行ごとに残りdeadlineを再計算し、未開始区間を明示する。
次のtrace-fixture.csv相当のlocalhost simulatorは、外部network、秘密情報、curlを必要としない。DNS cacheとTLS handshakeは制御可能な待ち時間として模擬し、実protocol測定ではないことを出力へ明記する。
python3.13 - <<'PY'
import json
import re
import socket
import statistics
import threading
import time
HARNESS = "network_lab_v2"
MAX_LINE_BYTES = 256
SOCKET_DEADLINE_SECONDS = 0.25
MAX_REQUEST_ID_BYTES = 64
MAX_DELAY_MS = 1000
REQUEST_ID_PATTERN = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}")
class ProtocolError(Exception):
def __init__(self, code):
super().__init__(code)
self.code = code
def receive_line(
connection,
timeline=None,
deadline_seconds=SOCKET_DEADLINE_SECONDS,
):
connection.settimeout(deadline_seconds)
payload = bytearray()
while True:
try:
part = connection.recv(min(64, MAX_LINE_BYTES + 1))
except TimeoutError as error:
raise ProtocolError("receive_timeout") from error
if not part:
raise ProtocolError("newline_required")
newline = part.find(b"\n")
fragment = part if newline == -1 else part[:newline]
payload.extend(fragment)
if len(payload) > MAX_LINE_BYTES:
raise ProtocolError("line_too_long")
if timeline is not None and "first_byte" not in timeline:
timeline["first_byte"] = time.monotonic_ns()
if newline != -1:
if part[newline + 1:]:
raise ProtocolError("multiple_lines")
break
if timeline is not None:
timeline["body_finish"] = time.monotonic_ns()
try:
return payload.decode("ascii")
except UnicodeDecodeError as error:
raise ProtocolError("non_ascii_line") from error
def validate_request_id(request_id):
if (
len(request_id.encode("ascii", errors="ignore"))
> MAX_REQUEST_ID_BYTES
or REQUEST_ID_PATTERN.fullmatch(request_id) is None
):
raise ProtocolError("invalid_request_id")
return request_id
def parse_command(line):
fields = line.split()
if not fields:
raise ProtocolError("empty_command")
command = fields[0]
if command == "APPLY":
if len(fields) != 4:
raise ProtocolError("invalid_arity")
request_id = validate_request_id(fields[1])
if not fields[2].isascii() or not fields[2].isdecimal():
raise ProtocolError("invalid_delay_ms")
delay_ms = int(fields[2])
if not 0 <= delay_ms <= MAX_DELAY_MS:
raise ProtocolError("invalid_delay_ms")
if fields[3] not in {"reply", "drop"}:
raise ProtocolError("invalid_mode")
return command, request_id, delay_ms, fields[3]
if command == "LOOKUP":
if len(fields) != 2:
raise ProtocolError("invalid_arity")
return command, validate_request_id(fields[1])
if command == "STOP":
if len(fields) != 1:
raise ProtocolError("invalid_arity")
return (command,)
raise ProtocolError("unknown_command")
class LocalFixture:
def __init__(self, injected_failure_id):
self.results = {}
self.injected_failure_id = injected_failure_id
self.injected_exceptions = []
self.stopped = threading.Event()
self.terminated = threading.Event()
self.listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
self.listener.setsockopt(
socket.SOL_SOCKET,
socket.SO_REUSEADDR,
1,
)
self.listener.bind(("127.0.0.1", 0))
self.listener.listen()
self.listener.settimeout(0.05)
except Exception:
self.listener.close()
raise
self.port = self.listener.getsockname()[1]
self.started = False
# A non-daemon thread makes cleanup failures visible instead of hiding
# a leaked server at interpreter shutdown.
self.server = threading.Thread(
target=self.serve,
name="localhost-fixture",
daemon=False,
)
def start(self):
try:
self.server.start()
self.started = True
except Exception:
self.listener.close()
raise
def handle(self, connection, command):
if command[0] == "APPLY":
_, request_id, delay_ms, mode = command
if request_id == self.injected_failure_id:
raise RuntimeError("controlled handler failure")
self.results.setdefault(request_id, "applied")
time.sleep(delay_ms / 1000)
if mode == "reply":
connection.sendall(
f"RESULT {request_id} applied\n".encode("ascii")
)
elif command[0] == "LOOKUP":
_, request_id = command
value = self.results.get(request_id, "missing")
connection.sendall(
f"RESULT {request_id} {value}\n".encode("ascii")
)
else:
self.stopped.set()
connection.sendall(b"STOPPED\n")
def serve(self):
try:
while not self.stopped.is_set():
try:
connection, _ = self.listener.accept()
except TimeoutError:
continue
except OSError:
if self.stopped.is_set():
break
raise
# The accepted socket receives its own deadline immediately;
# a slow client therefore cannot retain the sole worker.
connection.settimeout(SOCKET_DEADLINE_SECONDS)
with connection:
try:
self.handle(
connection,
parse_command(receive_line(connection)),
)
except ProtocolError as error:
connection.sendall(
f"ERROR {error.code}\n".encode("ascii")
)
except Exception as error:
self.injected_exceptions.append(type(error).__name__)
connection.sendall(b"ERROR internal\n")
finally:
self.terminated.set()
def close(self):
self.stopped.set()
try:
self.listener.close()
finally:
if self.started:
self.server.join(timeout=1)
if self.started and self.server.is_alive():
raise RuntimeError("localhost fixture did not stop")
if self.started and not self.terminated.is_set():
raise RuntimeError("localhost fixture termination not observed")
def open_connection(port, connector=socket.create_connection):
return connector(("127.0.0.1", port), timeout=1)
def send_line(port, payload, *, shutdown_write=False, send=True):
with open_connection(port) as connection:
if send:
connection.sendall(payload)
if shutdown_write:
connection.shutdown(socket.SHUT_WR)
return receive_line(connection, deadline_seconds=1)
def rejection_code(port, payload=b"", **options):
response = send_line(port, payload, **options)
prefix, separator, code = response.partition(" ")
assert prefix == "ERROR" and separator
return code
def request(port, request_id, resolver_mode, delay_ms=0, drop=False,
connector=socket.create_connection):
timeline = {}
timeline["dns_start"] = time.monotonic_ns()
socket.getaddrinfo("localhost", port, type=socket.SOCK_STREAM)
# These injected waits make the fixture deterministic enough to compare;
# they do not claim to flush or observe the operating-system DNS cache.
time.sleep(0.002 if resolver_mode == "cold" else 0.0001)
timeline["dns_end"] = time.monotonic_ns()
timeline["connect_start"] = time.monotonic_ns()
with open_connection(port, connector) as connection:
timeline["connect_end"] = time.monotonic_ns()
timeline["tls_start"] = time.monotonic_ns()
time.sleep(0.0002)
timeline["tls_end"] = time.monotonic_ns()
mode = "drop" if drop else "reply"
connection.sendall(
f"APPLY {request_id} {delay_ms} {mode}\n".encode("ascii")
)
try:
body = receive_line(connection, timeline)
except ProtocolError as error:
if not drop or error.code != "newline_required":
raise
now = time.monotonic_ns()
timeline.setdefault("first_byte", now)
timeline["body_finish"] = now
body = ""
return {
"request_id": request_id,
"resolver_mode": resolver_mode,
"monotonic_ns": timeline,
"body": body,
"duration_ms": (
timeline["body_finish"] - timeline["dns_start"]
) / 1_000_000,
}
def main():
fixture = LocalFixture(injected_failure_id="inject-error")
baseline = {"cold": [], "warm": []}
protocol_rejections = {}
connection_failure = "not_injected"
real_connector_calls = [0]
real_connector_calls_at_refusal = None
injector_calls = [0]
ambiguous = None
lookup_result = None
delayed = None
baseline_median_ms = None
try:
fixture.start()
for mode in baseline:
for repeat in range(7):
baseline[mode].append(
request(fixture.port, f"{mode}-{repeat}", mode)
)
baseline_values = [
sample["duration_ms"]
for samples in baseline.values()
for sample in samples
]
baseline_median_ms = statistics.median(baseline_values)
delayed = request(
fixture.port,
"delay-fixture",
"warm",
delay_ms=25,
)
protocol_rejections = {
"unterminated": rejection_code(
fixture.port,
b"LOOKUP never-finished",
shutdown_write=True,
),
"oversized": rejection_code(
fixture.port,
b"X" * (MAX_LINE_BYTES + 1) + b"\n",
),
"timeout": rejection_code(fixture.port, send=False),
"invalid_id": rejection_code(
fixture.port,
b"LOOKUP bad/id\n",
),
"excessive_delay": rejection_code(
fixture.port,
b"APPLY too-slow 1001 reply\n",
),
"invalid_arity": rejection_code(
fixture.port,
b"APPLY missing-mode 0\n",
),
"invalid_mode": rejection_code(
fixture.port,
b"APPLY wrong-mode 0 maybe\n",
),
}
assert send_line(
fixture.port,
b"APPLY inject-error 0 reply\n",
) == "ERROR internal"
# The fault injector raises before the real connector can create a
# socket. The counters prove the refusal did not call the real path.
def real_connector(address, timeout):
real_connector_calls[0] += 1
return socket.create_connection(address, timeout=timeout)
def refused_connector(address, timeout):
del address, timeout
injector_calls[0] += 1
raise ConnectionRefusedError(
"controlled refusal before socket creation"
)
try:
request(
fixture.port,
"refused-fixture",
"warm",
connector=refused_connector,
)
except ConnectionRefusedError:
connection_failure = "injected_before_socket_creation"
real_connector_calls_at_refusal = real_connector_calls[0]
assert real_connector_calls_at_refusal == 0
assert injector_calls[0] == 1
ambiguous = request(
fixture.port,
"order-001",
"warm",
drop=True,
connector=real_connector,
)
lookup_result = send_line(
fixture.port,
b"LOOKUP order-001\n",
).split()[-1]
assert send_line(fixture.port, b"STOP\n") == "STOPPED"
finally:
fixture.close()
budget = {
"deadline_ms": 2000,
"first_attempt_ms": 800,
"backoff_ms": 100,
"second_attempt_ms": 700,
"post_processing_ms": 200,
"cancel_propagation_ms": 100,
}
budget["worst_case_ms"] = sum(
value
for name, value in budget.items()
if name != "deadline_ms"
)
assert budget["worst_case_ms"] <= budget["deadline_ms"]
assert ambiguous is not None
assert delayed is not None
assert baseline_median_ms is not None
return {
"harness": HARNESS,
"fixture": "trace-fixture.csv",
"bind_address": "127.0.0.1",
"external_network_used": False,
"tls_simulated": True,
"dns_cache_simulated": True,
"baseline": baseline,
"baseline_median_ms": baseline_median_ms,
"budget": budget,
"retry_stop_conditions": [
"remaining deadline is below 100ms",
"non-idempotent APPLY has an unknown result",
],
"faults": {
"dependency_delay_ms": delayed["duration_ms"],
"connection_failure": connection_failure,
"real_connector_calls": real_connector_calls_at_refusal,
"injector_calls": injector_calls[0],
},
"non_idempotent": {
"request_id": "order-001",
"first_response": ambiguous["body"] or "response-lost",
"automatic_retry": "stopped",
"lookup_result": lookup_result,
},
"protocol_security": {
"max_line_bytes": MAX_LINE_BYTES,
"socket_deadline_ms": int(
SOCKET_DEADLINE_SECONDS * 1000
),
"request_id_ascii_allowlist": REQUEST_ID_PATTERN.pattern,
"max_request_id_bytes": MAX_REQUEST_ID_BYTES,
"max_delay_ms": MAX_DELAY_MS,
"rejections": protocol_rejections,
},
"cleanup": {
"injected_exception": (
"observed"
if fixture.injected_exceptions == ["RuntimeError"]
else "not_observed"
),
"server_stopped": fixture.terminated.is_set(),
"thread_alive": fixture.server.is_alive(),
"listener_closed": fixture.listener.fileno() == -1,
},
}
if __name__ == "__main__":
print(json.dumps(main(), ensure_ascii=False, indent=2))
PY
cold/warmは各7回だが、このfixtureのcoldは注入delay、warmは短い注入delayであり、OS resolver cacheのflushを証明しない。TLS区間も暗号処理のsimulationである。実環境へ移すときだけ管理下の検証先とcurl等を用い、同じrequest IDでclient/serverを相関する。
同じrequest IDでclientとserverを相関する。clientのconnect区間と再送が増え、serverにrequestが届いていなければ損失説を支持する。connectは平常でserver内部のDB spanだけが伸びれば依存遅延説を支持する。どちらも観測できなければ原因確定ではなく観測不足である。
ラボ成果物: 「DNSからHTTPまでの時系列トレースとタイムアウト予算」には、coldとwarmの両経路、単位、単調時計、request ID、最悪時の足し算、retry停止条件を含める。
注記
図を読む際の補足情報です。
- retry budget fixture。primaryの300ms phase breakdown fixtureとは別。実packet captureや普遍的latencyではない。
DNS、TCP、TLS、request、response喪失を分けると、再試行前に何を観測すべきか。
| 項目 | 完了した段階failureまでに完了したDNS/TCP/TLS/request段階。 | 観測clientとserverのtraceで確認する事実。 | retry判断残りdeadlineと冪等性を確認してから再試行する。 |
|---|---|---|---|
| failureなし全phaseを完了。 | DNS→TCP→TLS→request→response | statusとbodyまで観測 | retry不要 |
| DNS failureDNSで停止。 | DNS→failure observation→retry decision/recovery | resolver結果とTTL | 残りdeadline内で再解決 |
| TCP failureTCPで停止。 | DNS→TCP→failure observation→retry decision/recovery | connect timeoutとaddress | 別addressとbudgetを確認 |
| TLS failureTLS設定不整合。 | DNS→TCP→TLS failure→retry blocked/config repair | version、証明書、identity | 設定不整合なら盲目的retry禁止 |
| request failurerequest timeout。 | DNS→TCP→TLS→request timeout→retry decision/recovery | 送信量とserver trace | 冪等性と残りdeadlineを確認 |
| response lostresponse未観測。 | DNS→TCP→TLS→request sent→response unobserved→side-effect inquiry→retry decision | side effectとresponseを別々に照会 | 同じ冪等keyで結果照会またはretry |
接続準備とHTTP交換を混同せず、failure点、観測結果、retry条件を有限scenarioごとに説明する。
| パラメータ | 選択肢 | 既定値 |
|---|---|---|
| failure注入点 | なし、DNS、TCP、TLS、request、response喪失 | ok |
| latency profile | normal-budget: elapsed=1900ms, remaining=100ms、tight-budget(+1ms overhead): elapsed=1901ms, remaining=99ms | n |
- DNS: DNS→TCP→TLS→request。total=2000ms; first=800ms+backoff=100ms+second=700ms+post=200ms+cancel=100ms=planned=1900ms。retry開始条件 remaining>=100ms。; 条件 常時; node
completed-path; edge なし - TCP: DNS完了、TCP接続を確立。; 条件
fault=ok; nodehealthy; edgehealthy-path - TLS: TCP完了、TLS identityを検証。; 条件
fault=ok; nodehealthy; edgehealthy-path - request: TLS完了、request送信。; 条件
fault=ok; nodehealthy; edgehealthy-path - response: elapsed=1900ms; remaining=100ms; budget-gate=allowed。response observed。; 条件
budget=n、fault=ok; nodehealthy; edgehealthy-path、healthy-observation - no deadline: elapsed=1901ms; remaining=99ms; budget-gate=blocked。response待ち。; 条件
budget=t、fault=ok; nodehealthy; edgehealthy-path、healthy-retry - DNS failure: DNS failure観測。TCP以降は未開始。; 条件
fault=dns; nodedns-point; edgedns-path、dns-observation - DNS retry: elapsed=1900ms; remaining=100ms; budget-gate=allowed。DNS recovery。; 条件
budget=n、fault=dns; nodedns-point; edgedns-path、dns-retry - DNS blocked: elapsed=1901ms; remaining=99ms; budget-gate=blocked。DNS retry禁止。; 条件
budget=t、fault=dns; nodedns-point; edgedns-path、dns-retry - TCP failure: DNS後にTCP failure観測。TLS以降は未開始。; 条件
fault=tcp; nodetcp-point; edgetcp-path、tcp-observation - TCP retry: elapsed=1900ms; remaining=100ms; budget-gate=allowed。TCP recovery。; 条件
budget=n、fault=tcp; nodetcp-point; edgetcp-path、tcp-retry - TCP blocked: elapsed=1901ms; remaining=99ms; budget-gate=blocked。TCP retry禁止。; 条件
budget=t、fault=tcp; nodetcp-point; edgetcp-path、tcp-retry - TCP before TLS: DNS後にTCP確立。; 条件
fault=tls; nodetls-point; edgetls-path - TLS failure: TLS設定不整合を観測。; 条件
fault=tls; nodetls-point; edgetls-path、tls-observation - TLS repair: elapsed=1900ms; remaining=100ms; budget-gate=allowed。設定不整合なのでretry禁止、config repair。; 条件
budget=n、fault=tls; nodetls-point; edgetls-path、tls-retry - TLS blocked: elapsed=1901ms; remaining=99ms; budget-gate=blocked。TLS retry禁止。; 条件
budget=t、fault=tls; nodetls-point; edgetls-path、tls-retry - request TCP: DNS後にTCP確立。; 条件
fault=req; noderequest-point; edgerequest-path-cell - request TLS: TCP後にTLS完了。; 条件
fault=req; noderequest-point; edgerequest-path-cell - request timeout: timeout観測。statusとside effectを照会。; 条件
fault=req; noderequest-point; edgerequest-path-cell、request-observation - request retry: elapsed=1900ms; remaining=100ms; budget-gate=allowed。status確認後retry decision。; 条件
budget=n、fault=req; noderequest-point; edgerequest-path-cell、request-retry - request blocked: elapsed=1901ms; remaining=99ms; budget-gate=blocked。status照会、retry禁止。; 条件
budget=t、fault=req; noderequest-point; edgerequest-path-cell、request-retry - response TCP: DNS後にTCP確立。; 条件
fault=resp; noderesponse-point; edgeresponse-path - response TLS: TCP後にTLS完了。; 条件
fault=resp; noderesponse-point; edgeresponse-path - request sent: TLS後にrequest送信。; 条件
fault=resp; noderesponse-point; edgeresponse-path - unobserved: response未観測。side effectは不明。; 条件
fault=resp; noderesponse-point; edgeresponse-path、response-observation - inquiry: 冪等keyでside effect/statusを照会。; 条件
fault=resp; noderesponse-point; edgeresponse-observation - response retry: elapsed=1900ms; remaining=100ms; budget-gate=allowed。side-effect照会後retry。; 条件
budget=n、fault=resp; noderesponse-point; edgeresponse-path、response-retry - response blocked: elapsed=1901ms; remaining=99ms; budget-gate=blocked。side-effect照会、retry禁止。; 条件
budget=t、fault=resp; noderesponse-point; edgeresponse-path、response-retry
| イベント | 開始 | 終了 | 条件 |
|---|---|---|---|
| parameter-change | dns-lookup | dns-lookup | fault=ok |
| parameter-change | dns-lookup | dns-lookup | fault=dns |
| parameter-change | dns-lookup | dns-lookup | fault=tcp |
| parameter-change | dns-lookup | dns-lookup | fault=tls |
| parameter-change | dns-lookup | dns-lookup | fault=req |
| parameter-change | dns-lookup | dns-lookup | fault=resp |
| next | dns-lookup | h-tcp | fault=ok |
| timer | dns-lookup | h-tcp | fault=ok |
| previous | h-tcp | dns-lookup | fault=ok |
| reset | h-tcp | dns-lookup | fault=ok |
| next | h-tcp | tls-ready | fault=ok |
| timer | h-tcp | tls-ready | fault=ok |
| previous | tls-ready | h-tcp | fault=ok |
| reset | tls-ready | dns-lookup | fault=ok |
| next | tls-ready | h-req | fault=ok |
| timer | tls-ready | h-req | fault=ok |
| previous | h-req | tls-ready | fault=ok |
| reset | h-req | dns-lookup | fault=ok |
| next | h-req | h-ok | budget=n、fault=ok |
| timer | h-req | h-ok | budget=n、fault=ok |
| previous | h-ok | h-req | budget=n、fault=ok |
| reset | h-ok | dns-lookup | budget=n、fault=ok |
| next | h-req | h-retry-blocked | budget=t、fault=ok |
| timer | h-req | h-retry-blocked | budget=t、fault=ok |
| previous | h-retry-blocked | h-req | budget=t、fault=ok |
| reset | h-retry-blocked | dns-lookup | budget=t、fault=ok |
| next | dns-lookup | d-fail | fault=dns |
| timer | dns-lookup | d-fail | fault=dns |
| previous | d-fail | dns-lookup | fault=dns |
| reset | d-fail | dns-lookup | fault=dns |
| next | d-fail | d-retry | budget=n、fault=dns |
| timer | d-fail | d-retry | budget=n、fault=dns |
| previous | d-retry | d-fail | budget=n、fault=dns |
| reset | d-retry | dns-lookup | budget=n、fault=dns |
| next | d-fail | d-retry-blocked | budget=t、fault=dns |
| timer | d-fail | d-retry-blocked | budget=t、fault=dns |
| previous | d-retry-blocked | d-fail | budget=t、fault=dns |
| reset | d-retry-blocked | dns-lookup | budget=t、fault=dns |
| next | dns-lookup | t-fail | fault=tcp |
| timer | dns-lookup | t-fail | fault=tcp |
| previous | t-fail | dns-lookup | fault=tcp |
| reset | t-fail | dns-lookup | fault=tcp |
| next | t-fail | t-retry | budget=n、fault=tcp |
| timer | t-fail | t-retry | budget=n、fault=tcp |
| previous | t-retry | t-fail | budget=n、fault=tcp |
| reset | t-retry | dns-lookup | budget=n、fault=tcp |
| next | t-fail | t-retry-blocked | budget=t、fault=tcp |
| timer | t-fail | t-retry-blocked | budget=t、fault=tcp |
| previous | t-retry-blocked | t-fail | budget=t、fault=tcp |
| reset | t-retry-blocked | dns-lookup | budget=t、fault=tcp |
| next | dns-lookup | l-tcp | fault=tls |
| timer | dns-lookup | l-tcp | fault=tls |
| previous | l-tcp | dns-lookup | fault=tls |
| reset | l-tcp | dns-lookup | fault=tls |
| next | l-tcp | l-fail | fault=tls |
| timer | l-tcp | l-fail | fault=tls |
| previous | l-fail | l-tcp | fault=tls |
| reset | l-fail | dns-lookup | fault=tls |
| next | l-fail | l-retry-blocked | budget=n、fault=tls |
| timer | l-fail | l-retry-blocked | budget=n、fault=tls |
| previous | l-retry-blocked | l-fail | budget=n、fault=tls |
| reset | l-retry-blocked | dns-lookup | budget=n、fault=tls |
| next | l-fail | l-retry-blocked-tight | budget=t、fault=tls |
| timer | l-fail | l-retry-blocked-tight | budget=t、fault=tls |
| previous | l-retry-blocked-tight | l-fail | budget=t、fault=tls |
| reset | l-retry-blocked-tight | dns-lookup | budget=t、fault=tls |
| next | dns-lookup | q-tcp | fault=req |
| timer | dns-lookup | q-tcp | fault=req |
| previous | q-tcp | dns-lookup | fault=req |
| reset | q-tcp | dns-lookup | fault=req |
| next | q-tcp | q-tls | fault=req |
| timer | q-tcp | q-tls | fault=req |
| previous | q-tls | q-tcp | fault=req |
| reset | q-tls | dns-lookup | fault=req |
| next | q-tls | q-fail | fault=req |
| timer | q-tls | q-fail | fault=req |
| previous | q-fail | q-tls | fault=req |
| reset | q-fail | dns-lookup | fault=req |
| next | q-fail | q-retry | budget=n、fault=req |
| timer | q-fail | q-retry | budget=n、fault=req |
| previous | q-retry | q-fail | budget=n、fault=req |
| reset | q-retry | dns-lookup | budget=n、fault=req |
| next | q-fail | q-retry-blocked | budget=t、fault=req |
| timer | q-fail | q-retry-blocked | budget=t、fault=req |
| previous | q-retry-blocked | q-fail | budget=t、fault=req |
| reset | q-retry-blocked | dns-lookup | budget=t、fault=req |
| next | dns-lookup | s-tcp | fault=resp |
| timer | dns-lookup | s-tcp | fault=resp |
| previous | s-tcp | dns-lookup | fault=resp |
| reset | s-tcp | dns-lookup | fault=resp |
| next | s-tcp | s-tls | fault=resp |
| timer | s-tcp | s-tls | fault=resp |
| previous | s-tls | s-tcp | fault=resp |
| reset | s-tls | dns-lookup | fault=resp |
| next | s-tls | s-req | fault=resp |
| timer | s-tls | s-req | fault=resp |
| previous | s-req | s-tls | fault=resp |
| reset | s-req | dns-lookup | fault=resp |
| next | s-req | deadline-exceeded | fault=resp |
| timer | s-req | deadline-exceeded | fault=resp |
| previous | deadline-exceeded | s-req | fault=resp |
| reset | deadline-exceeded | dns-lookup | fault=resp |
| next | deadline-exceeded | s-inquiry | fault=resp |
| timer | deadline-exceeded | s-inquiry | fault=resp |
| previous | s-inquiry | deadline-exceeded | fault=resp |
| reset | s-inquiry | dns-lookup | fault=resp |
| next | s-inquiry | s-retry | budget=n、fault=resp |
| timer | s-inquiry | s-retry | budget=n、fault=resp |
| previous | s-retry | s-inquiry | budget=n、fault=resp |
| reset | s-retry | dns-lookup | budget=n、fault=resp |
| next | s-inquiry | s-retry-blocked | budget=t、fault=resp |
| timer | s-inquiry | s-retry-blocked | budget=t、fault=resp |
| previous | s-retry-blocked | s-inquiry | budget=t、fault=resp |
| reset | s-retry-blocked | dns-lookup | budget=t、fault=resp |
| 結果 | 状態 |
|---|---|
| elapsed=1900ms; remaining=100ms; budget-gate=allowed | h-ok |
| elapsed=1901ms; remaining=99ms; budget-gate=blocked | h-retry-blocked |
| elapsed=1900ms; remaining=100ms; budget-gate=allowed | d-retry |
| elapsed=1901ms; remaining=99ms; budget-gate=blocked | d-retry-blocked |
| elapsed=1900ms; remaining=100ms; budget-gate=allowed | t-retry |
| elapsed=1901ms; remaining=99ms; budget-gate=blocked | t-retry-blocked |
| elapsed=1900ms; remaining=100ms; budget-gate=allowed | l-retry-blocked |
| elapsed=1901ms; remaining=99ms; budget-gate=blocked | l-retry-blocked-tight |
| elapsed=1900ms; remaining=100ms; budget-gate=allowed | q-retry |
| elapsed=1901ms; remaining=99ms; budget-gate=blocked | q-retry-blocked |
| elapsed=1900ms; remaining=100ms; budget-gate=allowed | s-retry |
| elapsed=1901ms; remaining=99ms; budget-gate=blocked | s-retry-blocked |
現在の状態: DNS — DNS→TCP→TLS→request。total=2000ms; first=800ms+backoff=100ms+second=700ms+post=200ms+cancel=100ms=planned=1900ms。retry開始条件 remaining>=100ms。
このモデルは例示的かつ決定的であり、実システムの完全な再現ではありません。
トレードオフと失敗モード
| 観測と操作 | action | 必要な契約 | 避ける失敗 |
|---|---|---|---|
| 接続前失敗、read-only | 予算内で別addressへretry | 残りdeadlineとbackoff | 同時retryによる集中 |
| 送信後timeout、状態変更 | まず結果照会 | 冪等keyと結果endpoint | 処理済みrequestの二重実行 |
| 依存serviceの持続的遅延 | 縮退またはfail fast | 機能別の許容損失 | queue滞留とdeadline超過の連鎖 |
| 名前解決結果が疑わしい | TTLとresolver経路を確認 | cache更新規則 | stale addressへの接続継続 |
もっともらしい誤診と反証
- 誤診: 「HTTP p99悪化はpacket loss」。反証: TCP再送、connect時間、複数地点の同時刻を確認し、serverの依存spanと比較する。再送が平常でDB区間だけ増えていれば損失説を退ける。
- 誤診: 「DNS時間が0msなのでDNSに問題はない」。反証: warm cacheとcold queryを分け、TTL、負のcache、複数addressを確認する。測定がresolver callを含んでいない可能性も検証する。
retryは成功率を上げる可能性と、障害中の負荷を増やす可能性を同時に持つ。全clientが同時に再試行しないようjitterを入れ、総試行数を制限し、server側のcapacityと期限を共有する。TLS 0-RTTを使う場合はreplay可能性に適した操作かを確認する。
知識チェック
- TCP ACKを受けたら注文は確定済みか。分からない。byteの輸送確認とapplicationの状態遷移は別である。
- HTTP 200なら利用者操作は成功か。契約次第である。body内の業務結果、後続処理、client保存まで別の失敗点がある。
- DNS cacheは常に遅延を減らすか。通常はqueryを省けるが、stale addressや負のcacheにより回復を遅らせることがある。
- timeoutを長くすれば成功率は上がるか。待てば成功する原因には効くが、queueとresource占有を増やし、利用者deadlineを破り得る。
5分teach-back
八イベントの時系列を使い、各段で保証されたことと未確定なことを説明する。2000ms予算の足し算と、POST timeout後に直ちにretryしない理由を一分ずつ含める。
未知へのtransfer
パケット損失と依存遅延を区別する診断を、CDN、API gateway、二つの内部service、databaseからなる未知経路で行う。観測点が欠ける区間を明示し、責任を断定する前に追加する時刻とrequest IDを設計する。
出典と次の学習
RFC 1035はDNS、RFC 9293はTCP、RFC 6298はTCP再送timeout、RFC 8446はTLS 1.3、RFC 9110はHTTP semanticsの正準として使う。各層の仕様を横断して要約し、長い引用や原図の転載はしていない。正確なURLはlesson metadataのsourcesに集約した。
次の学習では、要求とdomain modelへ進み、networkの結果不明状態をapplication契約として表現する。1日後は時系列、7日後は予算計算、30日後は実trace、90日後はretryによる障害増幅を再レビューする。
実践ラボ
名前解決から応答完了までを期限へ収める
提出成果物: DNSからHTTPまでの時系列トレースとタイムアウト予算
- cold DNSとwarm DNSで、名前解決、接続、TLS、最初のbyte、本文完了の単調時計時刻を記録する
- 利用者期限2000msから各段の上限、後処理余白、キャンセル伝播時間を配分する
- 一回の再試行を含む最悪時合計を計算し、期限を超える場合は試行予算を修正する
- 制御可能な遅延依存と接続失敗を別々に注入し、時系列上の差を観測する
- 非冪等操作では再試行を止める条件と、結果不明を照会するための識別子を記録する
説明して理解を確かめる
5分で、DNS、TCP、TLS、HTTPの役割、TCP成功が業務成功を保証しない理由、exactly-onceを安易に主張できない理由を説明する。
アセスメント
問い: HTTPのp99が800msへ悪化した。『パケット損失だ』という診断と『依存DBが遅い』という診断をどう区別するか。
期待する証拠: TCP再送または接続時間、server-side区間、依存span、同時刻比較による反証
問い: POSTの応答がtimeoutした。即時retryが常に安全でない理由と必要な契約を示す。
期待する証拠: 処理済みだが応答喪失の状態、冪等key、結果照会、期限とbackoffを含む説明
別問題へ転用する
パケット損失と依存遅延を区別する診断
復習スケジュール
- 1日後
総期限2000msへ一回の再試行を入れるなら、各試行へ何ms配分するか
- 7日後
TCP成功から業務成功までに残る結果不明状態は何か
- 30日後
パケット損失説を反証するclient側とserver側の観測を一つずつ示す
- 90日後
総期限2000msへ一回の再試行を入れるなら、各試行へ何ms配分するか
評価ルーブリック
| 観点 | 未達 | 発展途上 | 熟達 | 卓越 |
|---|---|---|---|---|
| technical-correctness | TCP接続、TLS、HTTP成功、業務処理完了を同じ成功として扱う | 層は区別するが、再送またはtimeout後の結果不明状態を説明できない | 各層のイベント、期限、失敗状態、retryの影響を正しい順序で説明する | DNS cache、TLS 0-RTT、TCP再送、冪等性の境界と反例まで扱う |
| judgment | 各段へ総期限と同じtimeoutを設定し、無条件に再試行する | 予算を分けるが、後処理余白、deadline伝播、操作の冪等性が抜ける | 利用者期限から逆算し、操作種別に応じて試行回数と停止条件を選ぶ | retry stormと部分成功を考慮し、適応的縮退と結果照会を設計する |
| evidence | 総所要時間だけでネットワーク層の原因を断定する | client時刻はあるが、serverまたは再送の観測と時刻基準が揃わない | 単調時計の層別時刻、再送、server区間を同じ要求IDで相関する | coldとwarm経路、複数地点、欠測区間を比較し、観測限界を結論へ反映する |
| communication | ネットワークが遅いという表現だけで、層と時間帯が分からない | 時系列はあるが、期限超過への寄与と次の反証操作が不明である | 要求ID、層別区間、予算、仮説、反証、次の操作を一枚で追跡できる | client、network、service各担当へ責任断定を避けつつ同じ証拠を提示する |
出典
以下の外部資料は利用者が選択したときだけ開きます。