retryablehttp とは
The retryablehttp package provides a familiar HTTP client interface with automatic retries and exponential backoff. It is a thin wrapper over the standard net/http client library and exposes nearly the same public API. This makes retryablehttp very easy to drop into existing programs.
自動リトライと exponential backoff を備えた HTTP クライアントインターフェースを提供するパッケージです。
retryablehttp は標準バッケージである net/http のラッパーであり、ほぼ同じ API を公開しているので既存のコードに簡単に取り込むことができます。
// 例)retryablehttp を使って作成した client で http リクエストを送る
package main
import (
"io"
"log"
"net/http"
"time"
"github.com/hashicorp/go-retryablehttp"
)
func main() {
// サーバーを立てる
go serve()
// サーバーが起動するのを待つためスリープ
time.Sleep(1 * time.Second)
retryClient := retryablehttp.NewClient()
standardClient := retryClient.StandardClient() // *http.Client
resp, err := standardClient.Get("http://localhost:8080/hello")
if err != nil {
log.Println(err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
log.Println(string(body))
}
func serve() {
helloHandler := func(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "Hello, world!\n")
}
http.HandleFunc("/hello", helloHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
出力
2025/02/08 13:06:01 [DEBUG] GET http://localhost:8080/hello
2025/02/08 13:06:01 Hello, world!
retryablehttp の exponential backoff を試してみる
exponential backoff とは、リクエストが失敗した際に行うリトライで、待機時間を徐々に延ばしていく手法のことです。
package main
import (
"io"
"log"
"net/http"
"time"
"github.com/hashicorp/go-retryablehttp"
)
func main() {
// サーバーを立てる
go serve()
// サーバーが起動するのを待つためスリープ
time.Sleep(1 * time.Second)
retryClient := retryablehttp.NewClient()
retryClient.RetryMax = 5 // リトライ回数を 5 回に指定
retryClient.RetryWaitMin = 1 * time.Second // 最小リトライ間隔を 1 秒に設定
standardClient := retryClient.StandardClient() // *http.Client
resp, err := standardClient.Get("http://localhost:8080/hello")
if err != nil {
log.Println(err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
log.Println(string(body))
}
func serve() {
helloHandler := func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusInternalServerError) // status 500
}
http.HandleFunc("/hello", helloHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
出力
2025/02/08 13:53:16 [DEBUG] GET http://localhost:8080/hello
2025/02/08 13:53:16 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 1s (5 left)
2025/02/08 13:53:17 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 2s (4 left)
2025/02/08 13:53:19 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 4s (3 left)
2025/02/08 13:53:23 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 8s (2 left)
2025/02/08 13:53:31 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 16s (1 left)
2025/02/08 13:53:47 Get "http://localhost:8080/hello": GET http://localhost:8080/hello giving up after 6 attempt(s)
RetryWaitMin
に設定した 1 秒からはじまり、
1秒, 2秒, 4秒, 8秒... と間隔を延ばしながらリトライが行われていることがわかります
また、 RetryWaitMax
を設定することでリトライ間隔の上限も設定できます
package main
import (
"io"
"log"
"net/http"
"time"
"github.com/hashicorp/go-retryablehttp"
)
func main() {
// サーバーを立てる
go serve()
// サーバーが起動するのを待つためスリープ
time.Sleep(1 * time.Second)
retryClient := retryablehttp.NewClient()
retryClient.RetryMax = 5
retryClient.RetryWaitMin = 1 * time.Second
retryClient.RetryWaitMax = 10 * time.Second // 最大リトライ間隔を 10 秒に設定
standardClient := retryClient.StandardClient() // *http.Client
resp, err := standardClient.Get("http://localhost:8080/hello")
if err != nil {
log.Println(err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
log.Println(string(body))
}
func serve() {
helloHandler := func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusInternalServerError) // status 500
}
http.HandleFunc("/hello", helloHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
出力
2025/02/08 13:57:23 [DEBUG] GET http://localhost:8080/hello
2025/02/08 13:57:23 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 1s (5 left)
2025/02/08 13:57:24 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 2s (4 left)
2025/02/08 13:57:26 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 4s (3 left)
2025/02/08 13:57:30 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 8s (2 left)
2025/02/08 13:57:38 [DEBUG] GET http://localhost:8080/hello (status: 500): retrying in 10s (1 left)
2025/02/08 13:57:48 Get "http://localhost:8080/hello": GET http://localhost:8080/hello giving up after 6 attempt(s)
5回目のリトライが 16 秒ではなく 10 秒になっている
retryablehttp のリトライ間隔の算出ロジックを確認してみる
Client 構造体に Backoff
のフィールドがあり、特に指定しなければ DefaultBackoff がリトライ間隔の算出をします。
その実装は github から確認することができます。
// DefaultBackoff provides a default callback for Client.Backoff which // will perform exponential backoff based on the attempt number and limited // by the provided minimum and maximum durations. // // It also tries to parse Retry-After response header when a http.StatusTooManyRequests // (HTTP Code 429) is found in the resp parameter. Hence it will return the number of // seconds the server states it may be ready to process more requests from this client. func DefaultBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration { if resp != nil { if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable { if sleep, ok := parseRetryAfterHeader(resp.Header["Retry-After"]); ok { return sleep } } } mult := math.Pow(2, float64(attemptNum)) * float64(min) sleep := time.Duration(mult) if float64(sleep) != mult || sleep > max { sleep = max } return sleep }
hashicorp/go-retryablehttp より引用
まとめ
- retryablehttp を使って、リトライ間隔をリトライの度に延ばす exponential backoff が実現できる
-
RetryMax
を設定して、最大リトライ回数を制限できる -
RetryWaitMin
を設定して、リトライ間隔の下限を決められる -
RetryWaitMax
を設定して、リトライ間隔の上限を決められる
-
- リトライ間隔を算出する
DefaultBackoff
について、実装が GitHub で確認できる