「向こうが落ちてる」の六つの意味
一言でいうと
「連携できません」は少なくとも6つの異なる出来事であり、その6つを切り分けた瞬間に、責任の所在と次の行動が一緒に決まります。
なぜ必要なのか
現場で「向こうが落ちています」と聞いて相手に電話をかけると、相手は自分のダッシュボードは緑色だと答えます。どちらも嘘をついているわけではありません。こちらから見たものと、相手が見ているものが、別の出来事なだけです。
名前が見つからないのは、相手が無事でも起こります。接続が拒否されたのは、相手のホストは生きているのに、そのポートに誰もいないという意味です。接続がタイムアウトしたのは、相手がこちらとのハンドシェイクを終えられていないという意味で、この場合、相手のアプリケーションログには何の記録も残りません。読み取りがタイムアウトしたのは、相手がハンドシェイクは済ませたのに応答を返せていないという意味で、このとき相手側にリクエストがすでに届いて処理中かもしれません。最後の文が決定的です。リトライしてよいかどうかが、ここで分かれます。
どう動くのか
6つの顔を表に整理すると、次のとおりです。
얼굴 무슨 일이 일어났나 책임의 위치 재시도
dns_error 이름을 주소로 못 바꿨다 경로 안전
connection_refused 호스트는 답하는데 포트가 닫혔다 상대 안전
connect_timeout 손잡기(TCP)가 끝나지 않았다 경로 안전
read_timeout 손은 잡았고 답이 안 온다 상대 위험
partial_response 본문이 약속한 길이보다 짧다 상대 위험
slow_response 답은 왔는데 늦었다 상대 안전
このコードブロックの韓国語の表は、症状ごとに、起きたこと、責任の所在、リトライの可否を示しており、dns_errorは名前をアドレスに変換できなかった場合で経路の責任・リトライ安全、connection_refusedはホストは応答するがポートが閉じている場合で相手の責任・リトライ安全、connect_timeoutはTCPのハンドシェイクが終わらない場合で経路の責任・リトライ安全、read_timeoutはハンドシェイクは済んだが応答が来ない場合で相手の責任・リトライ危険、partial_responseは本文が約束した長さより短い場合で相手の責任・リトライ危険、slow_responseは応答は来たが遅かった場合で相手の責任・リトライ安全、という意味です。
リトライの列が、この表の核心です。接続が確立する前に失敗したものは、相手がリクエストを受け取れていないという意味なので、もう一度送っても同じことが2回起きません。しかし、読み取りタイムアウトと部分応答は、リクエストがすでに処理されているかもしれません。そのリクエストが決済や振込なら、リトライは二重請求になります。この場合、冪等キーのような保護なしにリトライしてはいけません。
区別する方法は、幸い難しくありません。Pythonのrequestsは、接続と読み取りにそれぞれ別のタイムアウトを与えられ、2つをタプルで渡すと、失敗もConnectTimeoutとReadTimeoutに分かれて届きます。この2つを1つの値で渡すと、2つの出来事が1つの顔に丸まってしまいます。
タイムアウトをまったく与えないと、さらに悪いことが起きます。デフォルト値がないので、リクエストは相手が応答するまで、またはカーネルが接続をあきらめるまで待ちます。その間、その作業スレッドは戻ってこず、リクエストが溜まると、こちらが先に落ちます。相手の遅さがこちらの障害に広がる最もよくある経路が、これです。
ここにリトライが加わると、状況は倍に悪くなります。終わらない試行を3回すると、3倍長く待ちます。そのため、リトライには全体のデッドラインが一緒にある必要があります。試行回数だけを決めたリトライは、最悪の場合どれだけ待つことになるのか、誰にもわかりません。
現場での姿
1つ目は、インターネットのない場所では再現できないと思い込むことです。実は、ローカルソケットだけで6つの顔がすべて再現できます。接続拒否は誰も待ち受けていないポートで足り、接続タイムアウトは、キューを満杯にしたあとacceptしないソケットで作れます(listen(2)のbacklogがその場所です)。名前解決の失敗は、RFC 2606がこの目的のために予約している.invalidの名前を使えばよいのです。
2つ目は、相手のせいとこちらのせいを混ぜることです。接続タイムアウトを「相手の障害」として報告すると、相手は自分のログで何も見つけられません。ハンドシェイクが終わらなかった接続は、相手のアプリケーションまで行っていないからです。この区別が報告書にあれば、調査はファイアウォールと経路の側から始まります。
3つ目は、遅い応答を成功としてしか記録しないことです。200が返ってきたから成功と書くと、相手がだんだん遅くなっていることを誰も見られません。成功にもしきい値を置き、超えたら別の顔として記録すれば、傾向が見えます。
4つ目は、部分応答をパースエラーとして記録することです。JSONパーサーが落ちたので、応答の形式が変わったのだと思い込みます。実際は本文が途中で切れたのであり、これは相手か中間の機器が接続を切ったという意味です。Content-Lengthと実際に受け取ったバイト数を比べれば、すぐに分かれます。
実務で本当に大切なこと
- 接続と読み取りのタイムアウトを別々に与えます。1つの値で与えると、2つの出来事が丸まります。
- リトライできるかどうかは、顔が決めます。読み取りタイムアウトと部分応答は、すでに処理されているかもしれません。
- リトライには全体のデッドラインを一緒に置きます。回数だけを決めたリトライは、最悪のケースを約束できません。
- こちらの責任、経路の責任、相手の責任を分けて書きます。その1行が、調査の出発点を決めます。
次のラボですること
連携相手の6つの顔をローカルソケットで立てておき、リクエストを1つ投げてその顔を切り分ける判定器を作ります。接続と読み取りのタイムアウトを別々に与えて2つの出来事を分けて見て、タイムアウトがないときに試行が終わらないことを自分で測り、デッドラインを守るリトライを作ります。最後に、6つの対象を一度に点検して、責任の所在を分けて書いた表を1枚で報告します。