重試工具 — retry-go

bazinga發表於2021-10-15

編輯推薦:Bazinga

簡介

在微服務架構中,通常會有很多的小服務,小服務之間存在大量 RPC 呼叫,但時常因為網路抖動等原因,造成請求失敗,這時候使用重試機制可以提高請求的最終成功率,減少故障影響,讓系統執行更穩定。retry-go 是一個功能比較完善的 golang 重試庫。

如何使用:

retry-go的使用非常簡單,直接使用 Do方法即可。如下是一個發起 HTTP Get 請求的重試示例 :

url := "https://gocn.vip"
var body []byte

err := retry.Do(
    func() error {
        resp, err := http.Get(url)
        if err != nil {
            return err
        }
        defer resp.Body.Close()
        body, err = ioutil.ReadAll(resp.Body)
        if err != nil {
            return err
        }

        return nil
    },
)

fmt.Println(body)

呼叫時,有一些可選的配置項:

  • attempts 最大重試次數
  • delay 重試延遲時間
  • maxDelay 最大重試延遲時間,選擇指數退避策略時,該配置會限制等待時間上限
  • maxJitter 隨機退避策略的最大等待時間
  • onRetry 每次重試時進行的一次回撥
  • retryIf 重試時的一個條件判斷
  • delayType 退避策略型別
  • lastErrorOnly 是否只返回上次重試的錯誤

BackOff 退避策略

對於一些暫時性的錯誤,如網路抖動等,立即重試可能還是會失敗,通常等待一小會兒再重試的話成功率會較高,並且這種策略也可以打散上游重試的時間,避免同時重試而導致的瞬間流量高峰。決定等待多久之後再重試的方法叫做退避策略。retry-go 實現了以下幾個退避策略:

func BackOffDelay

func BackOffDelay(n uint, _ error, config *Config) time.Duration

BackOffDelay 提供一個指數避退策略,連續重試時,每次等待時間都是前一次的 2 倍。

func FixedDelay

func FixedDelay(_ uint, _ error, config *Config) time.Duration

FixedDelay 在每次重試時,等待一個固定延遲時間。

func RandomDelay

func RandomDelay(_ uint, _ error, config *Config) time.Duration

RandomDelay 在 0 - config.maxJitter 內隨機等待一個時間後重試。

func CombineDelay

func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc

CombineDelay 提供結合多種策略實現一個新策略的能力。

retry-go預設的退避策略為 BackOffDelayRandomDelay結合的方式,即在指數遞增的同時,加一個隨機時間。

自定義的延時策略

下面是一個官方給出的例子,當請求的響應有Retry-After頭時,使用該值去進行等待,其他情況按照 BackOffDelay 策略進行延時等待。

var _ error = (*RetriableError)(nil)

func test2(){
    var body []byte

    err := retry.Do(
        func() error {
            resp, err := http.Get("URL")

            if err == nil {
                defer func() {
                    if err := resp.Body.Close(); err != nil {
                        panic(err)
                    }
                }()
                body, err = ioutil.ReadAll(resp.Body)
                if resp.StatusCode != 200 {
                    err = fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
                    if resp.StatusCode == http.StatusTooManyRequests {
                        // check Retry-After header if it contains seconds to wait for the next retry
                        if retryAfter, e := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 32); e == nil {
                            // the server returns 0 to inform that the operation cannot be retried
                            if retryAfter <= 0 {
                                return retry.Unrecoverable(err)
                            }
                            return &RetriableError{
                                Err:        err,
                                RetryAfter: time.Duration(retryAfter) * time.Second,
                            }
                        }
                        // A real implementation should also try to http.Parse the retryAfter response header
                        // to conform with HTTP specification. Herein we know here that we return only seconds.
                    }
                }
            }

            return err
        },
        retry.DelayType(func(n uint, err error, config *retry.Config) time.Duration {
            fmt.Println("Server fails with: " + err.Error())
            if retriable, ok := err.(*RetriableError); ok {
                fmt.Printf("Client follows server recommendation to retry after %v\n", retriable.RetryAfter)
                return retriable.RetryAfter
            }
            // apply a default exponential back off strategy
            return retry.BackOffDelay(n, err, config)
        }),
    )

    fmt.Println("Server responds with: " + string(body))
}

總結

重試可以提升服務呼叫的成功率,但重試時也要警惕由此帶來的放大故障的風險。選擇合適的退避策略,控制放大效應,才能優雅的提升服務的穩定性。

Reference

如何優雅地重試-InfoQ

[譯] 重試、超時和退避 | nettee 的 blog

更多原創文章乾貨分享,請關注公眾號
  • 重試工具 — retry-go
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章