向こうが落ちてる — 六つの顔を分けて責任の場所を決める
目標
接続拒否・名前解決の失敗・接続タイムアウト・読み取りタイムアウト・遅い応答・部分応答をそれぞれ区別する判定器と分類表を作ります。タイムアウトがないときに試行が終わらないことを自分で測り、デッドラインを守るリトライを作って、6つの対象を一度に点検します。
なぜ重要なのか
「連携できません」は、少なくとも6つの異なる出来事です。こちら側では障害なのに、相手のダッシュボードが緑色である理由もここにあります。ハンドシェイクが終わらなかった接続は相手のアプリケーションまで行かないので、相手のログには何の記録もありません。 切り分けが重要な理由は、次の行動が変わるからです。特にリトライできるかどうかが分かれます。接続前に失敗したものは、相手がリクエストを受け取れていないという意味なので、もう一度送ってよいのですが、読み取りタイムアウトと部分応答は、リクエストがすでに処理されているかもしれません。そのリクエストが振込なら、リトライは二重請求になります。 タイムアウトをまったく与えないと、さらに悪くなります。リクエストは相手が応答するまで待ち、その間、作業スレッドは戻ってきません。相手の遅さがこちらの障害に広がる最もよくある経路がこれで、そこにリトライが加わると、待ち時間が掛け算になります。 採点ツールは、あなたの説明を信じません。採点ツールが自分のポートに6つの顔を立てて、あなたの判定器を実際に接続し、分類を照合します。ポートは実行のたびに変わるので、値を暗記して入れることはできません。
ステップ
- /root/dep/gen_dep.pyを作成して実行し、/root/dep/servers.pyを作ってください。
- /root/dep/classify.pyを作成して、ok・connection_refused・dns_errorを区別させてください。
- 接続と読み取りのタイムアウトを別々に与えて、connect_timeoutとread_timeoutを分けてください。
- 部分応答と遅い応答を区別し、/root/dep/timeout.jsonに、タイムアウトがないときを測って書いてください。
- /root/dep/classes.jsonに、6つの顔の責任の所在とリトライの可否を書いてください。
- /root/dep/retry.pyでデッドラインを守るリトライを作り、/root/dep/retry.jsonに書いてください。
- 6つの対象を一度に点検して、/root/dep/verdict.jsonに表として残してください。
- /root/dep/summary.jsonを作り、/root/dep/dep_report.mdに4つの節で報告してください。
参考
- サーバーの契約:
python3 /root/dep/servers.py --role <ok|slow|hang|partial|backlog> --port P [--delay-ms N] [--ready-file F]は、その役割のとおりにだけ動きます。readyファイルができたら準備完了で、終わらせるにはプロセスをkillします。 - 判定の契約:
python3 /root/dep/classify.py --url <주소> [--connect-timeout S] [--read-timeout S] [--slow-ms N] [--out <json>](プレースホルダーはアドレスです)は、url・class・elapsed_ms・evidence・connect_timeout・read_timeoutを含むJSONを1つ出力します。 - 6つのclass名は
dns_error・connection_refused・connect_timeout・read_timeout・partial_response・slow_responseで、正常はokです。 - 遅い応答の基準は
--slow-msです。成功したものの、その時間を超えていたらslow_responseと書きます。200が返ってきたからといってすべて成功と書くと、相手が遅くなっていく傾向が見えません。 - タイムアウト: requestsは
timeout=(연결, 읽기)(プレースホルダーは接続と読み取りです)のように、2つを別々に受け取ります。1つの値で与えると、2つの出来事が1つの顔に丸まってしまいます。--connect-timeout 0以下を渡されたら、タイムアウトなしで待つようにしてください。 - リトライの契約:
python3 /root/dep/retry.py --url <주소> --attempts N --deadline-s S [--connect-timeout S] [--read-timeout S] [--out <json>](プレースホルダーはアドレスです)は、attempts_allowed・attempts_made・deadline_s・elapsed_ms・final_class・tries・gave_upを出力します。デッドラインを超えたら、試行回数が残っていても止まる必要があります。 - 責任の所在は、
우리・상대・경로(韓国語の値は、順に「こちら」「相手」「経路」という意味です)の3つのうち1つで書きます。リトライできるかどうかは、リクエストがすでに処理されているかもしれないかで決めます。読み取りタイムアウトと部分応答は危険です。 - 再現の材料: 名前解決の失敗はRFC 2606が予約した
.invalidの名前で、接続拒否は誰も待ち受けていないポートで、接続タイムアウトはキューを満杯にしたあとacceptしないソケット(backlogの役割)で作ります。 - よくあるミスは、タイムアウトを1つの値で与えること、200ならすべて成功と書くこと、部分応答をパースエラーと書くこと、冪等でないリクエストを読み取りタイムアウトのあとにリトライすることの4つです。
- 負荷テストを作らないでください。採点1回の予算は60秒です。サーバーは使い終わったら必ず止めてください。
連携相手の6つの顔を立てる
/root/dep/gen_dep.pyを作成して実行し、/root/dep/servers.pyを作ってください。ok・slow・hang・partial・backlogの5つの役割が、それぞれ自分のやり方どおりにだけ動く必要があります。
インターネットがなくても、6つの顔はすべてローカルソケットで再現できます。このスクリプトをそのまま保存して実行し、okの役割を1つ立てて、curlで一度呼び出してみてください。使い終わったら、プロセスを止めるのを忘れないでください。
顔を切り分ける判定器を作る
/root/dep/classify.pyを作成して、正常はok、誰も待ち受けていないポートはconnection_refused、.invalidの名前はdns_errorとして区別させてください。出力には、class・elapsed_ms・evidenceが必要です。
requestsは、失敗を例外の種類で知らせてくれます。ただし、接続拒否と名前解決の失敗は、どちらもConnectionErrorで来るので、メッセージを見て初めて分かれます。evidenceには、判定の根拠になったメッセージの末尾をそのまま残してください。あとで報告書に貼るのはそれです。
接続と読み取りを分けて見る
connect_timeoutとread_timeoutを分けて出してください。backlogの役割はハンドシェイクが終わらないのでconnect_timeout、hangの役割はハンドシェイクは済んで応答がないのでread_timeoutです。2つのタイムアウトは、別々に与える必要があります。
ステップ2の判定器は、2つのタイムアウトをtimeoutという1つの顔にまとめています。requestsは、接続段階の失敗をConnectTimeout、読み取り段階の失敗をReadTimeoutとして別々に投げるので、2つの例外を別々に捕まえれば分かれます。1つの顔のままだと、ファイアウォールの問題と相手の遅延を区別できません。
部分応答と遅い応答、そしてタイムアウトなし
partialの役割はpartial_response、slowの役割は--slow-msを超えたときにslow_responseとして区別してください。/root/dep/timeout.jsonには、with_timeoutとwithout_timeout(外部から5秒後に切ったときの終了コード)とnoteを書いてください。
部分応答は、本文を最後まで受け取って初めて表に出ます。ヘッダーだけを見て成功と書くと、見逃します。タイムアウトのないリクエストは自分では終わらないので、外から切る必要があります。timeout 5 python3 ...の終了コードが124だという事実そのものが証拠です。
6つの顔の責任とリトライを表にする
/root/dep/classes.jsonに、6つの顔(dns_error・connection_refused・connect_timeout・read_timeout・partial_response・slow_response)ごとに、whose(우리/상대/경로のいずれか。韓国語の値は、順に「こちら」「相手」「経路」という意味です)、retry_safe(trueかfalse)、next_step(10文字以上)を書いてください。リトライが危険な2つは、read_timeoutとpartial_responseです。
リトライできるかどうかは、「リクエストがすでに処理されているかもしれないか」で決めます。接続が確立する前に失敗したものは、相手がリクエストを受け取れていないという意味なので安全で、ハンドシェイクのあとの失敗は、すでに処理されているかもしれないので危険です。next_stepには、その顔に出会ったときに最初に見る場所を書いてください。
デッドラインを守るリトライを作る
/root/dep/retry.pyを作成して、--attemptsと--deadline-sの両方を守らせてください。hangの役割で実行した結果は、/root/dep/retry.jsonに残してください。デッドラインを超えたら、試行回数が残っていても止まる必要があります。
試行回数だけを決めたリトライは、最悪の場合どれだけ待つことになるのか約束できません。毎回の試行の前に残り時間を見て、残り時間がなければ止まってください。各試行にもタイムアウトがあって初めて、デッドラインが意味を持ちます。終わらない試行は、デッドラインでも救えません。
6つの対象を一度に点検する
6つの顔をすべて立てて1回ずつ判定し、/root/dep/verdict.jsonに、targets(name・role・class・whose・retry_safe)と、ours・theirs・pathの件数を書いてください。class6種類が、すべて1回ずつ出る必要があります。
点検表は、1行ずつ手で書くのではなく、判定器を実行して埋めるものです。whoseとretry_safeは、ステップ5の表から持ってきてください。表と点検表が食い違っていたら、どちらかが古くなっています。
責任の所在を分けて報告する
/root/dep/summary.jsonにclasses_covered・retry_elapsed_ms・retry_deadline_s・retry_attempts・theirs・path・no_timeout_exit_codeを書き、/root/dep/dep_report.mdに## 무엇이 안 됐나、## 우리인가 그들인가、## 재시도는 어떻게 했나、## 남은 위험(韓国語の見出しは、順に「何が動かなかったか」「こちらか相手か」「リトライはどうしたか」「残るリスク」という意味です)の4つの節で報告してください。
相手に電話する前にこの表を作れば、通話が短くなります。接続タイムアウトを「相手の障害」と書くと、相手は自分のログで何も見つけられません。その区別を報告書に残してください。リトライのデッドラインと実際にかかった時間も、数字で書きます。