その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

ネットワーク越しリトライ考

ここ最近では何らかのインターネットサービスを構築・運用するにあたって、ネットワーク越しのリトライを考えることは避けられなくなりつつあります。
micro services のようなアーキテクチャを採用している場合はサービス間のメッセージのやり取りはまず失敗する前提 (つまりリトライをする前提) で組む必要がありますし、たくさんのクライアントがいてそのクライアントが定期的に何かを処理してセントラルにデータを送ってくる IoT のようなシステムを構築する時もその処理のリトライをよく考える必要があります。

というわけで「ネットワーク越しのリトライ」についてここ最近考えていることをざっくりと書き留めるものであります。

前提
  • リトライをする側をクライアント、リトライを試みられる側をサーバと呼称します
  • リトライにおいて、サーバおよびネットワークはクライアントよりも弱者です
    • クライアントはリトライをコントロール出来る側にいますが、サーバとネットワークはそれをまったくコントロールできないためです
  • クライアントはリトライ時、サーバに迷惑をかけてはいけません
    • クライアントが1つポシャるのはそのクライアントだけで不具合が完結しますが、サーバがポシャると自分も含めた多数のクライアントが被害を受けるためです
  • クライアントはリトライ時、ネットワークに迷惑をかけてはいけません
    • クライアントが1つポシャるのはそのクライアントだけで不具合が完結しますが、ネットワークがポシャるとクライアントどころではなくネットワークに所属しているホストすべてが被害を受けるためです
リトライタイミングについて
  • リトライをする際にはインターバルを設けましょう
    • インターバルなしでリトライしてリクエストを殺到させてはいけません
    • 最悪いつまで経ってもサービスが復活しなくなる可能性があります
      • サーバが高負荷でリクエストを捌けなくなる
      • ネットワークが輻輳してすべてが終わる
  • リトライをする際のインターバルには backoff を設けましょう
    • 一定周期のインターバルは無いよりはマシですが、一定よりもリトライ回数の増加に応じて間隔を伸ばしていく方が好ましいでしょう
    • インターバルを設けた上で複数回失敗するということは、サーバにパフォーマンス等の深刻な問題が生じているか、クライアントに「そもそものリクエストがおかしい (サーバ側で受け入れられない)」などの致命的な問題が生じている可能性が高いためです
      • サーバに問題が生じているときは時間を置いたほうが良いので backoff を設けたほうが良いでしょう
      • クライアントの問題である場合はそのゴミリクエストを頻繁に投げるのは迷惑なのでリクエストの間隔を広げて頻繁にリクエストを投げないようにしたほうが良いでしょう
    • Exponential Backoff などがよく使われる方法だと思います (たまに Fibonacci Backoff を見ることがある)
  • インターバルにはジッターを設けましょう
    • サーバあるいはネットワーク起因で問題が発生した場合、問題が起きるタイミングは複数のクライアントでほぼ同一です
    • その複数のクライアントが同時にリトライを試みた場合、リクエストが殺到するのでサーバ・ネットワークが負荷に耐えきれなくなる場合があります
    • リトライ間隔にブレを持たせることで、リトライタイミングがある程度分散してくれると期待できるようになります
  • キリがよい時間のリトライを避けましょう
    • 例えば定期的に実行するようなクライアントの場合、「定期実行に失敗したので次の0:00 GMTちょうどに再度実行してデータを送る」というふうな「キリの良い時間」の再実行はできるだけ避けましょう
    • これはリトライの話題というよりバッチ処理のタイミングの話題ですが、「毎時ゼロ分」のようなキリの良い時間はリクエストが殺到する傾向があります
    • まあこれは民間療法に近い……
  • ジッターと同等の話題ですが、クライアントのハードウェアの電源投入時に即リトライを試みるのはやめましょう
    • IoT 的な話題ですが、メンテ等でハードウェアを一斉に再起動させることがあると思います
    • この際に全デバイスが一斉にリトライを試みるとサーバ・ネットワークが負荷に耐えきれなくなる可能性があります
    • ジッターを入れましょう
    • ハードウェアプロダクトの場合、一度アプリケーションをハードに焼き込むとリプレースが大変な場合が多いので注意しましょう。リプレースができないということは、ずっとその問題と付き合っていく必要が出てきます
リトライリクエストについて
  • リトライを前提とするリクエストについては冪等性 (何度実行しても結果に一貫性があるという性質) を担保しましょう
    • リクエストが冪等でない場合、最悪システムが矛盾した状態に陥ります
  • 破棄して良いリトライリクエストなのか、破棄してはならないものなのかをしっかり区別しましょう
  • ゴミリクエストは破棄しましょう
    • サーバは内容由来で処理不能なリクエストを受け取った場合は処理不能の旨 (例: HTTPの4xxレスポンス) をクライアントに通達しましょう
    • クライアントは処理不能なリクエストを受け取った場合はそのリクエストを破棄しましょう
    • 一定回数リトライを試みても成功しない場合はそれ以上送っても受け入れられない可能性が高いので破棄することを検討しましょう
  • リクエストを破棄する際は、そのリクエストをリプレイ可能な形でログかなにかに残しましょう
    • トラブルシューティングやマニュアルオペレーションでのリクエストの再実行に用いることができます
    • 破棄時にアラートを発報するなども良いでしょう
  • クライアントとサーバの間にバッファ (例: ジョブキュー) を挟むことができる状況の場合はバッファを挟むことを検討しましょう
    • クライアントの責任を「バッファにリトライリクエストを詰める」というところに限定できる
    • サーバはバッファから「自分のタイミング」でリクエストを取り出して処理することに集中できるようになるので、コントロールをある程度サーバ側に引き寄せられるようになるでしょう
    • 破棄してはならないリトライリクエストについてはバッファを入れた方が堅牢にしやすくなると思います
    • とはいえ
      • バッファに詰める時にポシャったらどうするかと言うと、ここにもリトライを考える必要が出てくる……
      • バッファから取ってきたデータの処理に失敗した時にどうするかと言うと、ここにも場合によってはリトライを考える必要が出てくる……
        • バッファ環境で、サーバサイドのリトライをやるにあたっては冪等性が必須となるでしょう
  • リトライリクエストの内容をメモリに蓄積している場合はデータロストの可能性を考えましょう
    • メモリに内容を保っているプロセスがダウンするとリトライすべき内容が失われます
    • それが致命的な場合はなんらかのストレージに保存しておくか、リプレイ可能なログを残すべきです
  • 失敗した部分だけをリトライする「ソフトリトライ」と、失敗した部分を含む一連の処理ごとやり直す「ハードリトライ」の両方の方法を用意すると便利です
    • 基本的にはソフトリトライを実行して、そのソフトリトライで解決しない (例: リトライが一向に成功しない) 場合はハードリトライにフォールバックして結果整合を保てるようになっているとなにかと良いです
    • ここを自律的に行うのは少々大変ですが……
  • リトライのメトリクスが取れるのであれば取りましょう
    • 常にリトライされている状況はおそらく何かがおかしいのでそれは検出したほうが良いでしょう
    • 場合によってはアラート等を上げるのも良いでしょう
    • とはいえどうメトリクスを取るのか、サーバで取るのかクライアントで取るのか、など考えることはあります
強いリアルタイムが求められる時はどうするか

とはいえ強いリアルタイム性を求められる際には「インターバルをたくさん入れる」とか「backoff を入れる」とかが難しい場合があり、そういうときはどうすれば良いんでしょうね……正直明確な答えはありませんが、考えられるのは

  • そもそもネットワーク越しでのリトライをやめる
  • サーキットブレイカ
    • リクエストに失敗してもその時点でのリクエストは通るようにしておく
    • バックグラウンドでリトライ処理を走らせておいて結果整合を図る

などでしょうか……まあ他にも色々あると思いますが。
しかし例に示した「サーキットブレイカーを入れて結果整合を図る」というのは複雑度がバリ上がりそうで大変そうな雰囲気がありますね。大変です。そもそも結果整合で良いのか? (結果整合で良いのであれば強リアルタイム性いらなくない?) というところもあるでしょう。

まあこのへんは歯を食いしばって頑張るしか無いのでしょうね……

まとめ

結局これがキングです:

  • クライアントからサーバにリトライリクエストを送る時にリクエストを殺到させるのはやめましょう
    • サーバが死にます
    • ネットワークが死にます (特にこちらは死んでしまうとどうにもならなくなる)

気をつけましょう。気をつけます。

追記

追記2

そういえば TCP 等の再送処理の話を一切していなかったことに気づきました……まあ本記事のスコープ外とさせて下さい。