「GoCN 酷 Go 推薦」重試工具 — retry-go

簡介

在微服務架構中,通常會有很多的小服務,小服務之間存在大量 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)

調用時,有一些可選的配置項:

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/DAEZQnC2UYwcFeEh0e8VoQ