Carpe Diem

備忘録

Exponential Backoff (指数関数的に増えるリトライ間隔)

概要

fluentdのretryはExponential Backoffと呼ばれるもので、リトライの間隔が

1秒、2秒、4秒、8秒、16秒

と指数関数的に増えていきます。これによって無駄なリクエストを省きつつ、再試行する前に問題を修正して解決できるようになります。
特に外部APIが長期障害発生時に、単調に繰り返しリトライするとシステムへの不必要な負担をかけるので、そういった問題時の負荷軽減にもなります。

docs.fluentd.org

サンプル

Golangでは以下のライブラリが実装しています。

https://github.com/cenkalti/backoff

具体的なコード

func main() {
    b := backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 7)
    err := backoff.Retry(someMethod(), b)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("done")
}

func someMethod() func() error {
    var counter int
    return func() error {
        fmt.Println("retry: ", counter)
        fmt.Println(time.Now())
        counter += 1

        if counter < 4 {
            return fmt.Errorf("fail: %d\n", counter)
        }

        fmt.Println("success")
        return nil
    }
}

結果

retry:  0
2018-05-01 13:35:26.137561853 +0900 JST m=+0.000439637
retry:  1
2018-05-01 13:35:26.794051414 +0900 JST m=+0.656925736
retry:  2
2018-05-01 13:35:27.389121729 +0900 JST m=+1.251992913
retry:  3
2018-05-01 13:35:28.763284537 +0900 JST m=+2.626148474
success
done

このように指数的にリトライ間隔が増えていきます。

間隔が均等に増えていないのはなぜ?

fluentdに比べてこのライブラリはバラツキがあります。これはわざとランダム要素を持たせています。
どんな時にメリットが有るかというと、分散システムであった時にリトライが同時に実行されないようにするなどです。

ドキュメントには

randomized interval =
    RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor])

と書かれており、例えば初期パラメータが

RetryInterval = 2
RandomizationFactor = 0.5

である場合は

randomized interval = 2 * (0.5 ~ 1.5のランダム値)

ということになります。なので1~3秒のどこかになる、ということですね。

デメリットは?

失敗回数が増えると次回のリトライが非常に先になってしまうので、fluentdのように

  • 最大試行回数をせっていする
  • 強制的に実行する手段を用意する
  • プロセスの再起動時にリトライ中のものを何処かに保持する

と言った点を考慮する必要があります。

自前で実装するなら

googleapi clientではインタフェースでbackoffをストラテジーパターンで実装しています。
これだと汎用性があっていろんなbackoffを設定できますね。

func Retry(ctx context.Context, f func() (*http.Response, error), backoff BackoffStrategy) (*http.Response, error) {
    for {
        resp, err := f()

        var status int
        if resp != nil {
            status = resp.StatusCode
        }

        // Return if we shouldn't retry.
        pause, retry := backoff.Pause()
        if !shouldRetry(status, err) || !retry {
            return resp, err
        }

        // Ensure the response body is closed, if any.
        if resp != nil && resp.Body != nil {
            resp.Body.Close()
        }

        // Pause, but still listen to ctx.Done if context is not nil.
        var done <-chan struct{}
        if ctx != nil {
            done = ctx.Done()
        }
        select {
        case <-done:
            return nil, ctx.Err()
        case <-time.After(pause):
        }
    }
}

ref: google-api-go-client/retry.go at 612451d2aabbf88084e4f1c48c0781073c0d5583 · googleapis/google-api-go-client · GitHub

ソース