ミニマムライフ通信

今の30代、ミニマムライフ世代?が興味ありそうなことについてひたすら書くブログ

タイムアウト・再送設計について整理してみる

当初技術的なことも少しは書こうかなと思っていたことブログですが
ほぼ書かないまま今に至ります。
まぁ基本的に会社のことは守秘義務などで書けないので
汎用的な内容を書くのですが特にそれっぽいネタもなかったのです。
そもそも私がガチのエンジニアではなくPMに近いということもありますが。

と色々言い訳していても始まらないのでたまにはということで
再送の設計について勘所についてまとめておきたいと思ったのでまとめます。

システム開発を行うにあたっては当たり前のことだけど
統合的に見ておかないとシステム全体で見た時にあれ?っていうことになってしまいます。

再送・タイムアウトとは

タイムアウトはあるリクエストに対するレスポンスが返る前にクライアントが待つことを諦めることを言います。
クライアント側でタイムアウトしたと判断されるとプログラム上はこれに応じた次の処理に移ることになります。
再送はリトライなどとも表現されますが、あるリクエストがタイムアウトした時に再度リクエストを投げることです。

再送・タイムアウトはなぜ必要?

アプリケーション間やシステム間の通信においては常にリクエストに対するレスポンスが返ってくることが
保証されているわけではありません。
サーバ、アプリケーションが落ちていたり、処理が輻輳していたりするケースにおいては
当然レスポンスが返ってこないのでこれを検知するための仕組みが必要になります。
これがタイムアウトです。xx秒で返ってこない場合はタイムアウトとして次の処理に移る、といった仕組みです。

またこのレスポンスが返ってこない状態というのは永久的に続くわけではなく
一時的なものである可能性もあります。
NWの調子が悪いとか、輻輳しているとか、アプリケーションが再起動中だとかがこれにあたります。
こういったケースのために一度はタイムアウトしたリクエストを再度送るのが再送またはリトライと言います。

タイムアウト・再送の設計ポイント

タイムアウト(以下TO)と再送においては当然ですがタイマが重要になります。
設計検討のポイントとしては以下のようなタイマがあるかと思います。

タイムアウト値…リクエスト投げてから応答するまでの待ち時間
再送間隔…タイムアウトから再送を行うまでのsleep時間。常に同じ時間だけ待つか倍々で待ち時間が増えていくようなケースが多いです。
再送回数…何回再送を行うかの回数を指定する値です
再送期限…初回のリクエスト送信から再送を含め全体の処理を打ち切る期限を設定する値です

モジュール間連携がある場合、そのモジュール間の再送設計の兼ね合いも注意して見る必要があります。
A->B->外部サーバという形でAとBのモジュールが連携している場合
Bが再送を頑張る時間>Aが再送を頑張る時間とした方が良いように思いますが、
この差を著しく大きくした場合Aは諦めているのにBはひたすら頑張るというおかしなことが起こります。

また、注意が必要なのは設計のポイントでOSレイヤ(L4レイヤ)とAPLレイヤ(L7レイヤ)で
それぞれ設計可能な箇所があり、これらを踏まえて設計しておく必要があるという点です。

OSレイヤのTOはAPLレイヤに通知されるのでこれを踏まえておかないと思った通りに
タイムアウトが動いてくれない、といったことが起こります。

OSのTO値>APLのTO値のケースではAPLのTO値を超えた所でAPLがTOします。
OSのTO値

接続時のタイマ

TCPの接続時のタイマでいわゆる3wayハンドシェイク時のタイマです。
これらはOSレイヤでの設計とAPLレイヤでの設計があります。

OSレイヤ

カーネルパラーメータでの設定になります。
Linux 7でいうと以下のようなパラメータがあります。

・net.ipv4.tcp_syn_retries
synを再送する回数です。確か初期値は1sだかで(今度調べる)リトライ間隔はkernelにハードコードされており、
回数が大きくなるに従い倍々で大きくなっていったと思います。
例えば3であれば1s、2s、4s、8sという形で大きくなります。
なのでconfigが可能なのは回数だけということになります。


・net.ipv4.tcp_synack_retries
synackを再送する回数です。仕組みはsynの再送と同じだと思う。(未確認)

・net.ipv4.tcp_fin_timeout
finを送信した時のack待ちタイマです。これは再送にあまり関係ないかも。

アプリレイヤ

自分で実装してね。(そのうち書く)
ただし、前段で書いたOSレイヤとの兼ね合いを設計することは重要です。
APLのTO値を長くしすぎるとこのTOの前にOSから接続不可が検知されてくる形となり
想定していた通りの値のTOを実現することができません。

接続中のタイマ

3wayハンドシェイクが正常に完了した後にアプリケーションでデータ通信を行う場合に
タイムアウトをさせるためのタイマです。

OSレイヤ

OSレイヤでは接続されたTCP通信を切るかどうかのコントロールがされるわけですが
この時に考慮すべきはTCP keepalive関連のkernelパラメータでしょう。

お前繋がってるよな?という確認のためのkeepaliveパケットを送信して
応答がなかった場合、rstを送信してアプリケーション側に情報を通知するというものです。

デフォルトだと2時間とか長めに設定してあるので、基本的にはAPL側で制御する
というケースが多いように思います。

以下にkernelパラメータでconfig可能な値を記載しています。

・net.ipv4.tcp_keepalive_time
keepaliveパケットを送る間隔

・net.ipv4.tcp_keepalive_intvl
keepaliveパケットに応答がなかった場合に再送を行う間隔

・net.ipv4.tcp_keepalive_probes
keepaliveパケットに応答がなかった場合に再送を行う回数

APLレイヤ

自分で実装してね。(そのうち書く)
HTTP1.1のようなプロトコルの場合keepaliveを有効にするのかどうか
というのも意識する必要があります。
また、こちらも接続時と同じでOSレイヤの接続関連のkernelパラメータとの整合性を
整理しておくことが重要です。

その他の考慮ポイント

クライアントがタイムアウトしてもサーバ側の処理は続く

要求をだすクライアントは応答が返ってこないためにタイムアウトをするわけですが
この場合当然サーバ側でどこまで処理が進んでいるかはわかりません。
要求が受け付けられていない可能性もあるし、受け付けられて処理がされている可能性もあります。
この時にどちらのケースでも処理の整合性が取れる設計にしておく必要があります。

例えばクライアントがサーバ側のデータを削除するという要求を投げて、削除できたらtrueを返却するような
処理があった場合を想定します。
仮にtrueに"削除して成功した"という意味を持たせたとします。(サーバ側でそのように実装する)
この要求をサーバに投げて処理に時間がかかってタイムアウトしたが削除自体には成功したケースを想定すると
クライアントは1回目はタイムアウト、2回目以降は削除すべきデータがない、とうことになり
ずっと削除が成功したことを知ることができません。

こうしたケースを避けるためにtrueの意味は"あなたのリクエスト通りファイルが存在しない状態ですよ"
という意味を持たせておくと1回目はタイムアウト、2回目以降でtrueが返るということになるので
データの整合性が保たれます。
あるいはクライアント、サーバ間でデータの整合性を保つような処理を別途入れる必要があります。

まとめ

タイムアウトの設計のポイントについて自分なりにまとめてみました。
ざっくり言うと
タイムアウトと一口に言ってもタイマは複数あるので注意して整理を
・検討のレイヤは一つではなく多段なケースがほとんどなので注意して整理を
と言う点でしょうか。
また、気づきがあれば書き足したいと思います。