概要
fluentdのretryはExponential Backoffと呼ばれるもので、リトライの間隔が
1秒、2秒、4秒、8秒、16秒
と指数関数的に増えていきます。これによって無駄なリクエストを省きつつ、再試行する前に問題を修正して解決できるようになります。
特に外部APIが長期障害発生時に、単調に繰り返しリトライするとシステムへの不必要な負担をかけるので、そういった問題時の負荷軽減にもなります。
サンプル
Goでは以下のライブラリが実装しています。
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のように
- 最大試行回数をせっていする
- 強制的に実行する手段を用意する
- プロセスの再起動時にリトライ中のものを何処かに保持する
と言った点を考慮する必要があります。
自前で実装するなら
googleのapi 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): } } }