こんにちは、技術開発チームの小嶋です。
とある社内ツールの開発において、HTTPリクエストのリトライ処理の実装にgo-retryablehttpを利用しました。go-retryablehttpはHashiCorp社が中心となって開発しているOSSライブラリです。本記事ではリトライ処理の概要やgo-retryablehttpを利用した経緯について紹介します。
なぜリトライ処理が必要なのか
はじめに、リトライ処理の必要性について簡単に触れます。クライアントとサーバがネットワークで接続されている環境において、クライアントがサーバへリクエストを送信する処理について考えてみます。このような環境ではリクエストが失敗する要因をいくつか想定できます。例えば、次のような要因があります。
- クライアントのリクエストが誤った内容であり、リクエストが実行エラーとなった
- ネットワークで一時的な接続障害が発生し、リクエストがサーバに届かなかった
- 利用者の一時的な急増に伴ってサーバが過負荷になり、リクエストがタイムアウトになった
リクエストの失敗につながるこれらの要因は恒久的に発生するエラーと一時的に発生するエラーに分類できます。
- 恒久的に発生するエラー:クライアントのリクエストの誤りによるエラーが恒久的に発生するエラーを指す。リクエストの内容を修正するなど根本的な対策を施さなければ回復が見込めない。
- 一時的に発生するエラー:ネットワークの一時的な接続障害によるエラーや、利用者の一時的な急増に伴うサーバ過負荷によるタイムアウトが一時的に発生するエラーを指す。一定の時間内に回復が見込める。
一時的に発生するエラーは時間を置いて処理を再実行すれば成功する場合も少なくありません。すなわち、処理を繰り返し実行していれば最終的には成功に至ります。このように成功するまで処理を繰り返し実行する操作をリトライ処理と呼びます。サーバがクラウドサービスや自社管理外APIなどの環境ではサーバを直接変更できないため、クライアントによるリトライ処理が一時的に発生するエラーへの有効な対策の1つとなります。
リトライ処理の実装における考慮点
リトライ処理を実装するうえで考慮すべき点がいくつかあります。
1つ目の考慮点として、リトライ条件の判定があります。前述のようにエラーには恒久的なものと一時的なものがあります。恒久的なエラーは処理を繰り返し実行しても回復が見込めないため、一時的なエラーに限ってリトライ処理を実行しなければいけません。HTTPリクエストにおいてはコネクションエラーやHTTPステータスコードの429 Too Many Request/5xx台のものが一時的なエラーに該当します。
2つ目の考慮点として、処理の再実行の間に挟む待ち時間があります。待ち時間を挟まずに処理を繰り返し実行し続けた場合、多数のリクエストがサーバに送信されます。その結果、サーバが過負荷になるなど悪影響を与える恐れがあります。サーバが過負荷になっているシナリオにおいてはまさに泣きっ面に蜂の状況です。
リトライ処理による影響を防ぐためには処理の再実行の間に待ち時間を挟むことが有効です。この待ち時間を計算するアルゴリズムの1つとしてExponential Backoff
があります。Exponential Bakckoff
は「1秒, 2秒, 4秒, 8秒」のようにリトライ回数に応じて待ち時間を指数関数的に増加させます。そのうえで最大リトライ回数や最大待ち時間になるまで処理を繰り返し実行するアルゴリズムです。待ち時間を指数関数的に増加させることでリトライ処理による影響をより効果的に防げます。
go-retryablehttpによるHTTPリクエストのリトライ処理
前述のようにリトライ処理を実装するうえでは考慮すべき点があり、注意深く実装を進める必要があります。今回はリトライ処理の実装をOSSライブラリにまかせる方針をとり、社内ツールの開発言語としてGoを利用していたことからGoのOSSライブラリをいくつか調査しました。結果として、後述の理由のとおりgo-retryablehttpを選択しました。go-retryablehttpはHashiCorp社が中心となって開発しているOSSライブラリです。詳細はgo-retryablehttpのリポジトリやドキュメントを参照してください。
その他のOSSライブラリの候補としては、retry-goやbackoffがありました。これらのOSSライブラリはgo-retryablehttpと比べてよりシンプルな機能を提供しています。そのため、リトライ処理の内容がHTTPリクエストに限らない一般的な処理の場合やリトライ条件を明示的に指定したい場合に利用するとよいでしょう。
ここからはgo-retryablehttpを選択した理由と簡単な利用方法について触れます。go-retryablehttpを選択した1つ目の理由として、リトライ処理が実装されたHTTPクライアントをGo標準パッケージのHTTPクライアントへ変換できる点が挙げられます。下記のようにリトライ処理が実装されたHTTPクライアント(retryablehttp.Client
)を設定し、その後でGo標準パッケージのHTTPクライアント(http.Client
)へ変換できます。既存のコードでhttp.Client
を利用している部分を差し替えるだけでHTTPリクエストのリトライ処理を簡単に実装できます。go-retryablehttpを選択した最大のポイントです。
// retryablehttp.Clientの設定 retryClient := retryablehttp.NewClient() retryClient.RetryMax = 3 retryClient.RetryWaitMin = 1_000 * time.Millisecond retryClient.RetryWaitMax = 10_000 * time.Millisecond // retryablehttp.Clientをhttp.Clientへ変換 standardClient := retryClient.StandardClient() // http.ClientでHTTPリクエストの送信 req, _ := http.NewRequest(...) res, err := standardClient.Do(req)
go-retryablehttpを選択した2つ目の理由として、リトライ条件の判定が実装されている点が挙げられます。go-retryablehttpのデフォルトのリトライ条件の判定はDefaultRetryPolicyとして実装されています。コネクションエラーやHTTPステータスコードが5xx台の場合(501 Not Implementedを除く)にリトライ処理が実行されます。
go-retryablehttpを利用した理由の3つ目の理由として、複数の待ち時間アルゴリズムが実装されている点が挙げられます。go-retryablehttpのデフォルトの待ち時間アルゴリズムはDefaultBackoffとして実装されています。DefaultBackoffはExponential Backoff
を実装しており、最大リトライ回数や最小・最大の待ち時間を指定できます。その他の待ち時間アルゴリズムとしてはLinearJitterBackoffが実装されています。また、待ち時間アルゴリズムが満たすべき関数の型(Backoff)が公開されているので、独自の待ち時間アルゴリズムを実装して利用できます。
以上のような点を踏まえ、go-retryablehttpを利用してHTTPリクエストのリトライ処理を実装しました。go-retryablehttpを利用した感想として、HTTPリクエストのリトライ処理がクライアントの責務として抽象化されているのが良い点だと感じました。Go標準パッケージのHTTPクライアントを利用している箇所では既存のコードにほぼ修正を加えずにリトライ処理を実装できるため、他の社内ツールの開発でも積極的に利用していきたいです。
おわりに
本記事ではリトライ処理の概要やgo-retryablehttpを利用した経緯について紹介しました。お手軽に利用できるOSSライブラリなのでぜひ試してみてください。