ネットワーク越しリトライ考
ここ最近では何らかのインターネットサービスを構築・運用するにあたって、ネットワーク越しのリトライを考えることは避けられなくなりつつあります。
micro services のようなアーキテクチャを採用している場合はサービス間のメッセージのやり取りはまず失敗する前提 (つまりリトライをする前提) で組む必要がありますし、たくさんのクライアントがいてそのクライアントが定期的に何かを処理してセントラルにデータを送ってくる IoT のようなシステムを構築する時もその処理のリトライをよく考える必要があります。
というわけで「ネットワーク越しのリトライ」についてここ最近考えていることをざっくりと書き留めるものであります。
前提
- リトライをする側をクライアント、リトライを試みられる側をサーバと呼称します
- リトライにおいて、サーバおよびネットワークはクライアントよりも弱者です
- クライアントはリトライ時、サーバに迷惑をかけてはいけません
- クライアントが1つポシャるのはそのクライアントだけで不具合が完結しますが、サーバがポシャると自分も含めた多数のクライアントが被害を受けるためです
- クライアントはリトライ時、ネットワークに迷惑をかけてはいけません
- クライアントが1つポシャるのはそのクライアントだけで不具合が完結しますが、ネットワークがポシャるとクライアントどころではなくネットワークに所属しているホストすべてが被害を受けるためです
リトライタイミングについて
- リトライをする際にはインターバルを設けましょう
- リトライをする際のインターバルには backoff を設けましょう
- 一定周期のインターバルは無いよりはマシですが、一定よりもリトライ回数の増加に応じて間隔を伸ばしていく方が好ましいでしょう
- インターバルを設けた上で複数回失敗するということは、サーバにパフォーマンス等の深刻な問題が生じているか、クライアントに「そもそものリクエストがおかしい (サーバ側で受け入れられない)」などの致命的な問題が生じている可能性が高いためです
- Exponential Backoff などがよく使われる方法だと思います (たまに Fibonacci Backoff を見ることがある)
- インターバルにはジッターを設けましょう
- サーバあるいはネットワーク起因で問題が発生した場合、問題が起きるタイミングは複数のクライアントでほぼ同一です
- その複数のクライアントが同時にリトライを試みた場合、リクエストが殺到するのでサーバ・ネットワークが負荷に耐えきれなくなる場合があります
- リトライ間隔にブレを持たせることで、リトライタイミングがある程度分散してくれると期待できるようになります
- キリがよい時間のリトライを避けましょう
- ジッターと同等の話題ですが、クライアントのハードウェアの電源投入時に即リトライを試みるのはやめましょう
- IoT 的な話題ですが、メンテ等でハードウェアを一斉に再起動させることがあると思います
- この際に全デバイスが一斉にリトライを試みるとサーバ・ネットワークが負荷に耐えきれなくなる可能性があります
- ジッターを入れましょう
- ハードウェアプロダクトの場合、一度アプリケーションをハードに焼き込むとリプレースが大変な場合が多いので注意しましょう。リプレースができないということは、ずっとその問題と付き合っていく必要が出てきます
リトライリクエストについて
- リトライを前提とするリクエストについては冪等性 (何度実行しても結果に一貫性があるという性質) を担保しましょう
- リクエストが冪等でない場合、最悪システムが矛盾した状態に陥ります
- 破棄して良いリトライリクエストなのか、破棄してはならないものなのかをしっかり区別しましょう
- ゴミリクエストは破棄しましょう
- リクエストを破棄する際は、そのリクエストをリプレイ可能な形でログかなにかに残しましょう
- トラブルシューティングやマニュアルオペレーションでのリクエストの再実行に用いることができます
- 破棄時にアラートを発報するなども良いでしょう
- クライアントとサーバの間にバッファ (例: ジョブキュー) を挟むことができる状況の場合はバッファを挟むことを検討しましょう
- クライアントの責任を「バッファにリトライリクエストを詰める」というところに限定できる
- サーバはバッファから「自分のタイミング」でリクエストを取り出して処理することに集中できるようになるので、コントロールをある程度サーバ側に引き寄せられるようになるでしょう
- 破棄してはならないリトライリクエストについてはバッファを入れた方が堅牢にしやすくなると思います
- とはいえ
- バッファに詰める時にポシャったらどうするかと言うと、ここにもリトライを考える必要が出てくる……
- バッファから取ってきたデータの処理に失敗した時にどうするかと言うと、ここにも場合によってはリトライを考える必要が出てくる……
- バッファ環境で、サーバサイドのリトライをやるにあたっては冪等性が必須となるでしょう
- リトライリクエストの内容をメモリに蓄積している場合はデータロストの可能性を考えましょう
- メモリに内容を保っているプロセスがダウンするとリトライすべき内容が失われます
- それが致命的な場合はなんらかのストレージに保存しておくか、リプレイ可能なログを残すべきです
- 失敗した部分だけをリトライする「ソフトリトライ」と、失敗した部分を含む一連の処理ごとやり直す「ハードリトライ」の両方の方法を用意すると便利です
- 基本的にはソフトリトライを実行して、そのソフトリトライで解決しない (例: リトライが一向に成功しない) 場合はハードリトライにフォールバックして結果整合を保てるようになっているとなにかと良いです
- ここを自律的に行うのは少々大変ですが……
- リトライのメトリクスが取れるのであれば取りましょう
- 常にリトライされている状況はおそらく何かがおかしいのでそれは検出したほうが良いでしょう
- 場合によってはアラート等を上げるのも良いでしょう
- とはいえどうメトリクスを取るのか、サーバで取るのかクライアントで取るのか、など考えることはあります
強いリアルタイムが求められる時はどうするか
とはいえ強いリアルタイム性を求められる際には「インターバルをたくさん入れる」とか「backoff を入れる」とかが難しい場合があり、そういうときはどうすれば良いんでしょうね……正直明確な答えはありませんが、考えられるのは
などでしょうか……まあ他にも色々あると思いますが。
しかし例に示した「サーキットブレイカーを入れて結果整合を図る」というのは複雑度がバリ上がりそうで大変そうな雰囲気がありますね。大変です。そもそも結果整合で良いのか? (結果整合で良いのであれば強リアルタイム性いらなくない?) というところもあるでしょう。
まあこのへんは歯を食いしばって頑張るしか無いのでしょうね……
まとめ
結局これがキングです:
気をつけましょう。気をつけます。
追記
しょうもない話だけれど、意図してフィボナッチバックオフにすることってあるのか知りたい。 | ネットワーク越しリトライ考 - その手の平は尻もつかめるさ https://t.co/3DQSIrSIVf
— Yuichiro SAITO (@koemu) 2020年11月17日
ぶっちゃけ利点としては「実装が極めて簡易」くらいしか無いような気はするんですよね……僕も真の理由があるなら知りたいところです
— moznion (@moznion) 2020年11月17日
追記2
そういえば TCP 等の再送処理の話を一切していなかったことに気づきました……まあ本記事のスコープ外とさせて下さい。