ABL 「ジッターを伴うタイムアウト、再試行、およびバックオフ」を読んだ
(追記)輪読会資料を追加。
輪読会の発表に向けて、Amazon Builder's Library(以下、ABL) を読んだので、簡単にまとめてみました。
テーマは「ジッターを伴うタイムアウト、再試行、およびバックオフ」です。
ざっくり理解のためのメモ
・サーバーへのアクセスが失敗したとき、いかに負荷を上げすぎず再アクセスをするか、のAmazonがとってる仕組みが学べる。
・ジッター サーバーへのアクセスを散らすために、各システムからのアクセスをずらす仕組み。本来の意味はは『タイミングのゆらぎ』。
・再試行 システムの応答がない時に、もう一回リクエストかけること。 解決することも多いが、下手すると再試行が失敗し続け負荷を上げてしまいサーバーが落ちるかも。
・バックオフ 間隔をあけること
(参考)輪読会資料
なお記事のほうは、Amazon Web Service のシニアプリンシパルエンジニアの方が書いておりまして、項目は、以下の5つでした。
はじめに 障害の発生
タイムアウト
再試行とバックオフ
ジッター
まとめ
それでは内容について触れていきたいと思います。
はじめに 障害の発生
障害の発生要因は様々
サーバー、ネットワーク、ロードバランサー、ソフトウェア、オペレーティングシステム、システムのオペレーターの操作ミス 等
故障しないシステムは構築不可能
Amazonでは、障害の発生は許容しつつ、障害によりサーバーが停止しない仕組みとして以下の3つを採用している。
① タイムアウト
リクエストが長期化すると、リソース切れになる恐れがあるので、クライアントはタイムアウトを設定する。
② 再試行
多くの場合は再試行は成功する。
リクエストの一部失敗や、短期間の失敗に耐えることが可能。
すべてのクライアントが同時に再試行すると、再試行が無駄になる可能性がある。
この問題を回避するために、ジッター(タイミングを意図的にずらす仕組み)を使用する。
ジッタ(jitter)とは、デジタル信号の「タイミングの揺らぎ」のことです。
③ バックオフ
再試行はリソースが少ないときに、負荷を増加させてしまうため、バックオフ(再試行の間隔を長くすること)を実装する。
べき等(同一のパラメーターで 1 回以上リクエストしても、リソースの状態が同じであること)がベストプラクティス。
参考
HTTP リクエストメソッドにおける「冪等」とは、「同一のパラメーターで 1 回以上リクエストしても、リソースの状態が同じであること」をいいます。
HTTP において「冪等」を定義する目的は、もしクライアントがサーバーからのリクエスト成功の応答を受信する前に通信障害が発生した場合、クライアントはリクエストをリトライしてそのリクエストの目的を達成する必要があり、サーバーはそのリトライによるリソースの不整合から保護しなければなりません。
さらに、クライアントは、同じリクエストをリトライするとリソースの整合性が破壊されるとなれば、リトライはできなくなるでしょうし、そのリトライによりリソースの整合性が保護されるのか、破壊されるのかを知らなければ無闇にリトライもできなくなるでしょう。
したがって、サーバーはこのような堅牢性を確保するとともに、クライアントにリトライ可否の区別を知らせなければなりません。
これが本来の「冪等」の目的であり、「リソースの状態の変化」に着目する理由はここにあります。
負荷が増加してエラーが発生した場合に、同時に再試行が発生するとさらに負荷が上がる。
タイムアウト
リモートの呼び出しにタイムアウトを設定するのがAmazonのベストプラクティス。
タイムアウトの設定値を決めるのが最も難しい問題
値が大きすぎると、タイムアウトされるまでの間消費されるリソースが増加する
値が小さすぎると、
再試行のリクエスト数が増加して、トラフィック量が増加して遅延が増加
リクエストの再試行のタイミングが被って、小さなバックエンドの遅延が増加し、サーバーが停止する
AWS リージョンにおいて、タイムアウトの基準は、下流のサービスの遅延状況を指標にするのがベストプラクティス。
なので、Amazon では、とあるサービスに別のサービスを呼び出しさせるとき、許容されるタイムアウトの相違の割合 (たとえば0.1%) を選択する。
次に、対応する下流のサービス遅延のパーセンタイルを調べる (さきほどのパターンだと99.9パーセンタイル)。
→おそらくリクエストの99.9%は再試行してうまく行った場合含めうまく行き、0.1%は遅延してタイムアウトすることを許容、という意味?
(参考)グーグル翻訳 stackoverflow.com
P99レイテンシは何を表していますか?私はこれについてアプリケーションのパフォーマンスに関する議論で耳にし続けていますが、これについて話すリソースをオンラインで見つけることができませんでした。
それはだ99パーセンタイル。つまり、リクエストの99%は、指定されたレイテンシよりも高速である必要があります。言い換えると、リクエストの1%だけが遅くなることを許可されています。
パーセントは、率をあらわしています。50パーセントは半数が、という意味です。
パーセンタイルは、データを大きさ順でならべて100個に区切り、小さいほうからのどの位置にあるかを見るものです。50パーセンタイルは、小さいほうから50/100のところにあるデータです。
このアプローチはおおむねうまくいきますが、いくつか落とし穴がある。
● このアプローチは、外部ネット経由のように、クライアントにかなりのネットワークの遅延がある場合には機能しない。
このような場合は、クライアントが世界中に広がることを懸念しつつ、合理的かつ最悪のネットワーク遅延のケースを計算に入れておく。
● このアプローチは、p99.9 も p50 も大きな違いがないくらい、遅延の基準値が小さいサービスでもうまくいかない。
このような場合、パディングを追加することで、小さな遅延の増加が、大量のタイムアウトを招くのを防ぐことができる。
パディングとは、詰め物、水増し、埋草などの意味を持つ英単語。ITの分野では、表示要素の内側に設けられた余白や、データを一定の長さに整形するため前後に挿入される無意味なデータなどをこのように呼ぶ。
● タイムアウトを実装するときに、もっともハマりがちな落とし穴は次のようなものである。
Linux の SO_RCVTIMEO は強力だが、エンドツーエンドのソケット(クライアント側)のタイムアウトとしては不向きな欠点があります。
SO_RCVTIMEO と SO_SNDTIMEO
送信・受信のタイムアウトを指定する。これを越えるとエラーを報告する。
Javaのようなプログラミング言語は、このコントロールを直接オープンにする。
他のGoのようなプログラミング言語は、より強固なタイムアウトの仕組みを構築できる。
● また、DNSやTLSハンドシェイクのように、タイムアウトがすべてのリモートの呼び出しをカバーしていない実装もある。
一般的には、ちゃんとテストされたクライアントに組み込まれているタイムアウトを好んで使っている。
独自のタイムアウトを実装する場合、タイムアウトソケットオプションの正確な意味と、どのような作業が行われているかに注意を払っています。
Amazonが取ったタイムアウト問題の回避方法
とあるシステムで、タイムアウト値を約20ミリ秒(約0.02秒)に設定していた
デプロイ以外ではタイムアウトが定期的に発生することはなかったが、デプロイ直後でタイムアウトが見られた
これは、デプロイ時の接続の確率に0.02秒以上かかっていたため、デプロイするとリクエストの一部がタイムアウトしてしまっていたためである
これを解決するために、まずデプロイの場合、接続確率時のタイムアウト値を増やす改善を行い、のちに(どういう意味?)プロセスを開始後、トラフィック受信前に接続の確率を行うよう改善を行った。
再試行とバックオフ
再試行は一時的な障害には有効だが、過負荷が原因の障害の場合は再試行が余計に負荷をあげてしまう(時には元の原因が解決したのに再試行が負荷の原因として残る)。
→再試行は強力な薬に例えている。弱ったところに強力な薬を投与すると、薬に耐えれずかえって体調を崩す、ということか。
そこで、バックオフを推奨。
エクスポネンシャルバックオフ 再試行の間隔を指数関数的に増加させるので、再試行そのものが過負荷の原因になるのを防止する。ただし別の問題が発生する。
そこで、再試行する回数に上限を設け、サービス指向アーキテクチャの初期で発生する障害を処理することで対策している。
その他リスクと課題
● 分散システムの場合
分散システムの各レイヤーごとに再試行を行うと、再試行の回数が大幅に増加して負荷も大きく増加。
スタックの最上位レイヤーで再試行すると、以前のリクエストが無駄になって効率が悪い。
スタックの単一ポイントで再試行するのが一番効率が良い。
スタック(Stack)とは 「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典より > スタック (stack)とは 後に入れたものが先に出る構造になっている何か
最上位レイヤー = 後に入れたもの ということ?
それを再試行すると、先に入れたものもやり直しになるから、ということだろうか?
● 負荷の増加
エラーのしきい値を超えると端末への呼び出しが遮断されるサーキットブレーカーが便利。
サーキットブレーカーはモーダル(表示されたタスクが完了するまで、ユーザーは他の作業を行えない)を採用していますが、テスト困難で復旧に時間がかかる。
トークンバケットを使用してローカルで再試行を制限することで解決している。
参考)
トラフィック(≒ API リクエスト数)を制御するために利用できるアルゴリズムで、バースト(瞬間的なトラフィック増大)を許容しつつ、トラフィックの平均的な流量を一定以下に保つことができるものである、と理解した。
トークンとバケット 一度に転送可能なトラフィックの最大を『バケット(バケット)』で表現し、トラフィックの転送を許可するための手形を『トークン』で表現する。 ひとつのトークンは一定サイズのトラフィックに対応し、トラフィックを転送する際にトラフィックに応じた数のトークンが消費(削除)される。
● 再試行のタイミング
べき等性がない(=副作用がある)APIは再試行しても安全ではない。
読み取り専用APIはべき等性があるが、リソースを作成するAPIはべき等性がない。
(Amazon EC2) RunInstances API などの一部の API は、明示的なトークンベースのメカニズムを提供して、べき等性を提供し、安全に再試行できるようにしている。
● 再試行したほうがいい障害かどうか選定
HTTPだとクライアントエラーとサーバーエラーは明確に区別される。
クライアントエラー (400–499) 通常再試行しても成功しない。 サーバエラー (500–599) 再試行すると成功する可能性がある。
ただし、システムの結果整合性により、クライアントエラーでも再試行により成功になる場合がある。
分散データベースの文脈で使われることが多く、データの更新の一貫性を即時担保するものではなく、更新後に一定時間経過していれば正しく更新データを取得できるという整合性の考え方
英語を直訳すると『最終的な一貫性』となり、和訳では『結果整合性』という訳が当てられ、2つの意味を統合すると直感的にわかりすい。
この同期を取るタイミングが様々なパターンがあるが、即時ではなく、時間経過があって何かしらの状態で結果として同じ状態になる状態のことを「結果整合性が取れる」と呼ぶ。
結果整合性のとり方はいろいろあるが、3分毎に複数のDBを同期するなども結果整合性を取るという意味では1つのアプローチと言えるはず。
課題やリスクはあるが、再試行は障害対策として強力なので、賢く使うべき。
ジッター
過負荷または競合が障害の原因である場合、バックオフが非常に役に立たないことがある。
同じタイミングでバックオフされてしまうと、過負荷になってしまうからである。
そこで、バックオフをランダムに行って負荷を分散させる、ジッターを採用している。
ジッターは再試行以外の用途にも使う。
定期的な作業の場合は、ランダムにジッターを行わず、各ホストが同じ数だけ実行するようにする。
ランダムだとトラブルシューティングが難しくなるからである。
まとめ
タイムアウトは、リクエストが長期化するのを防ぐ。
再試行は、リクエストの長期化をなかったことにする
バックオフ・ジッターは、再試行が集中するのを防ぐ。
Amazonのシステムは、負荷の状況や可用性を考慮して再試行を行う。
感想
べき等性、「何度再試行を繰り返してもリソースに影響」させない仕組みとはどういう仕組みだろうか。
なんとかわかりやすい日本語に直してみたものの、まだ血肉にはなっていない感が強い。
改めて見直してみて、補足したい。
以上になります、最後までお読みいただきありがとうございました。