0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

retryablehttp の exponential backoff の挙動を検証

Last updated at Posted at 2025-04-23

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 を公開しているので既存のコードに簡単に取り込むことができます。

main.go
// 例)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 とは、リクエストが失敗した際に行うリトライで、待機時間を徐々に延ばしていく手法のことです。

main.go
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 を設定することでリトライ間隔の上限も設定できます

main.go
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 で確認できる
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?