在微服务架构中,通常会有很多的小服务,小服务之间存在大量 RPC 调用,但时常因为网络抖动等原因,造成请求失败,这时候使用重试机制可以提高请求的最终成功率,减少故障影响,让系统运行更稳定。retry-go 是一个功能比较完善的 golang 重试库。

安装

  1. go get https://github.com/avast/retry-go

快速使用

retry-go的使用非常简单,直接使用 Do方法即可。如下是一个发起 HTTP Get 请求的重试示例 :

  1. url := "http://example.com"
  2. var body []byte
  3. err := retry.Do(
  4. func() error {
  5. resp, err := http.Get(url)
  6. if err != nil {
  7. return err
  8. }
  9. defer resp.Body.Close()
  10. body, err = ioutil.ReadAll(resp.Body)
  11. if err != nil {
  12. return err
  13. }
  14. return nil
  15. },
  16. )
  17. fmt.Println(body)

调用时,有一些可选的配置项:

  • attempts 最大重试次数
  • delay 重试延迟时间
  • maxDelay 最大重试延迟时间,选择指数退避策略时,该配置会限制等待时间上限
  • maxJitter 随机退避策略的最大等待时间
  • onRetry 每次重试时进行的一次回调
  • retryIf 重试时的一个条件判断
  • delayType 退避策略类型
  • lastErrorOnly 是否只返回上次重试的错误

BackOff 退避策略

对于一些暂时性的错误,如网络抖动等,立即重试可能还是会失败,通常等待一小会儿再重试的话成功率会较高,并且这种策略也可以打散上游重试的时间,避免同时重试而导致的瞬间流量高峰。决定等待多久之后再重试的方法叫做退避策略。retry-go 实现了以下几个退避策略:

BackOffDelay

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

BackOffDelay 提供一个指数避退策略,连续重试时,每次等待时间都是前一次的 2 倍。

FixedDelay

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

FixedDelay 在每次重试时,等待一个固定延迟时间。

RandomDelay

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

RandomDelay 在 0 - config.maxJitter 内随机等待一个时间后重试。

CombineDelay

  1. func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc

CombineDelay 提供结合多种策略实现一个新策略的能力。

retry-go默认的退避策略为 BackOffDelay和RandomDelay结合的方式,即在指数递增的同时,加一个随机时间。

自定义的延时策略

下面是一个官方给出的例子,当请求的响应有Retry-After头时,使用该值去进行等待,其他情况按照BackOffDelay策略进行延时等待。

  1. var _ error = (*RetriableError)(nil)
  2. func test2(){
  3. var body []byte
  4. err := retry.Do(
  5. func() error {
  6. resp, err := http.Get("URL")
  7. if err == nil {
  8. defer func() {
  9. if err := resp.Body.Close(); err != nil {
  10. panic(err)
  11. }
  12. }()
  13. body, err = ioutil.ReadAll(resp.Body)
  14. if resp.StatusCode != 200 {
  15. err = fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
  16. if resp.StatusCode == http.StatusTooManyRequests {
  17. // check Retry-After header if it contains seconds to wait for the next retry
  18. if retryAfter, e := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 32); e == nil {
  19. // the server returns 0 to inform that the operation cannot be retried
  20. if retryAfter <= 0 {
  21. return retry.Unrecoverable(err)
  22. }
  23. return &RetriableError{
  24. Err: err,
  25. RetryAfter: time.Duration(retryAfter) * time.Second,
  26. }
  27. }
  28. // A real implementation should also try to http.Parse the retryAfter response header
  29. // to conform with HTTP specification. Herein we know here that we return only seconds.
  30. }
  31. }
  32. }
  33. return err
  34. },
  35. retry.DelayType(func(n uint, err error, config *retry.Config) time.Duration {
  36. fmt.Println("Server fails with: " + err.Error())
  37. if retriable, ok := err.(*RetriableError); ok {
  38. fmt.Printf("Client follows server recommendation to retry after %v\n", retriable.RetryAfter)
  39. return retriable.RetryAfter
  40. }
  41. // apply a default exponential back off strategy
  42. return retry.BackOffDelay(n, err, config)
  43. }),
  44. )
  45. fmt.Println("Server responds with: " + string(body))
  46. }

总结

重试可以提升服务调用的成功率,但重试时也要警惕由此带来的放大故障的风险。选择合适的退避策略,控制放大效应,才能优雅的提升服务的稳定性。

Reference

字节跳动团队-如何优雅地重试