分享
  1. 首页
  2. 文章

重试机制的实现

lryong · · 6142 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

服务在请求资源,如果遇到网络异常等情况,导致请求失败,这时需要有个重试机制来继续请求。 常见的做法是重试3次,并随机 sleep 几秒。 业务开发的脚手架,HTTP Client 基本会封装好 retry 方法,请求失败时根据配置自动重试。下面以一个常见的 HTTP Client 为例, 看下它是如何实现请求重试。 最后整理其他一些重试机制的实现。

<!--more-->

go-resty 重试机制的实现

先看下 go-resty 在发送 HTTP 请求时, 请求重试的实现:

// Execute method performs the HTTP request with given HTTP method and URL
// for current `Request`.
// resp, err := client.R().Execute(resty.GET, "http://httpbin.org/get")
func (r *Request) Execute(method, url string) (*Response, error) {
 var addrs []*net.SRV
 var resp *Response
 var err error
 if r.isMultiPart && !(method == MethodPost || method == MethodPut || method == MethodPatch) {
 return nil, fmt.Errorf("multipart content is not allowed in HTTP verb [%v]", method)
 }
 if r.SRV != nil {
 _, addrs, err = net.LookupSRV(r.SRV.Service, "tcp", r.SRV.Domain)
 if err != nil {
 return nil, err
 }
 }
 r.Method = method
 r.URL = r.selectAddr(addrs, url, 0)
 if r.client.RetryCount == 0 {
 resp, err = r.client.execute(r)
 return resp, unwrapNoRetryErr(err)
 }
 attempt := 0
 err = Backoff(
 func() (*Response, error) {
 attempt++
 r.URL = r.selectAddr(addrs, url, attempt)
 resp, err = r.client.execute(r)
 if err != nil {
 r.client.log.Errorf("%v, Attempt %v", err, attempt)
 }
 return resp, err
 },
 Retries(r.client.RetryCount),
 WaitTime(r.client.RetryWaitTime),
 MaxWaitTime(r.client.RetryMaxWaitTime),
 RetryConditions(r.client.RetryConditions),
 )
 return resp, unwrapNoRetryErr(err)
}

重试流程

梳理 Execute(method, url) 在请求时的重试流程:

  1. 如果没有设置重试次数,执行 r.client.execute(r) :直接请求 Request , 返回 Response 和 error。
  2. 如果 r.client.RetryCount 不等于0 ,执行 Backoff() 函数
  3. Backoff() 方法接收一个处理函数参数,根据重试策略, 进行 attempt 次网络请求, 同时接收 Retries()、WaitTime()等函数参数

Backoff函数

重点看下 Backoff() 函数做了什么动作。

Backoff()代码如下:

// Backoff retries with increasing timeout duration up until X amount of retries
// (Default is 3 attempts, Override with option Retries(n))
func Backoff(operation func() (*Response, error), options ...Option) error {
 // Defaults
 opts := Options{
 maxRetries: defaultMaxRetries,
 waitTime: defaultWaitTime,
 maxWaitTime: defaultMaxWaitTime,
 retryConditions: []RetryConditionFunc{},
 }
 for _, o := range options {
 o(&opts)
 }
 var (
 resp *Response
 err error
 )
 for attempt := 0; attempt <= opts.maxRetries; attempt++ {
 resp, err = operation()
 ctx := context.Background()
 if resp != nil && resp.Request.ctx != nil {
 ctx = resp.Request.ctx
 }
 if ctx.Err() != nil {
 return err
 }
 err1 := unwrapNoRetryErr(err) // raw error, it used for return users callback.
 needsRetry := err != nil && err == err1 // retry on a few operation errors by default
 for _, condition := range opts.retryConditions {
 needsRetry = condition(resp, err1)
 if needsRetry {
 break
 }
 }
 if !needsRetry {
 return err
 }
 waitTime, err2 := sleepDuration(resp, opts.waitTime, opts.maxWaitTime, attempt)
 if err2 != nil {
 if err == nil {
 err = err2
 }
 return err
 }
 select {
 case <-time.After(waitTime):
 case <-ctx.Done():
 return ctx.Err()
 }
 }
 return err
}

梳理 Backoff() 函数的流程:

  1. Backoff() 接收 处理函数 和 可选的 Option 函数(retry optione) 作为参数
  2. 默认策略3次重试, 通过 步骤一 预设的 Options, 自定义重试策略
  3. 设置请求的 repsonse 和 error 变量
  4. 开始进行 opts.maxRetries 次 HTTP 请求:

    1. 执行处理函数 (发起 HTTP 请求)
    2. 如果返回结果不为空并且 context 不为空,保持 repsonse 的请求上下文。 如果上下文出错, 退出 Backoff() 流程
    3. 执行 retryConditions(), 设置检查重试的条件。
    4. 根据 needsRetry 判断是否退出流程
    5. 通过 sleepDuration()计算 Duration(根据此次请求resp, 等待时间配置,最大超时时间和重试次数算出 sleepDuration。 时间算法相对复杂, 具体参考: Exponential Backoff And Jitter)
    6. 等待 waitTime 进行下个重试。 如果请求完成退出流程。

一个简单的 Demo

看具体 HTTP Client (有做过简单封装)的请求:

func getInfo() {
 request := client.DefaultClient().
 NewRestyRequest(ctx, "", client.RequestOptions{
 MaxTries: 3,
 RetryWaitTime: 500 * time.Millisecond,
 RetryConditionFunc: func(response *resty.Response) (b bool, err error) {
 if !response.IsSuccess() {
 return true, nil
 }
 return
 },
 }).SetAuthToken(args.Token)
 resp, err := request.Get(url)
 if err != nil {
 logger.Error(ctx, err)
 return 
 }
 body := resp.Body()
 if resp.StatusCode() != 200 {
 logger.Error(ctx, fmt.Sprintf("Request keycloak access token failed, messages:%s, body:%s","message", resp.Status(),string(body))),
 )
 return 
 }
 ...
}

根据以上梳理的 go-resty 的请求流程, 因为 RetryCount 大于0,所以会进行重试机制,重试次数为3。然后 request.Get(url) 进入到 Backoff() 流程,此时重试的边界条件是: !response.IsSuccess(), 直到请求成功。

一些其他重试机制的实现

可以看出其实 go-resty 的 重试策略不是很简单, 这是一个完善,可定制化, 充分考虑 HTTP 请求场景下的一个机制, 它的业务属性相对比较重。

再来看看两个常见的 Retry 实现:

实现一

// retry retries ephemeral errors from f up to an arbitrary timeout
func retry(f func() (err error, mayRetry bool)) error {
 var (
 bestErr error
 lowestErrno syscall.Errno
 start time.Time
 nextSleep time.Duration = 1 * time.Millisecond
 )
 for {
 err, mayRetry := f()
 if err == nil || !mayRetry {
 return err
 }
 if errno, ok := err.(syscall.Errno); ok && (lowestErrno == 0 || errno < lowestErrno) {
 bestErr = err
 lowestErrno = errno
 } else if bestErr == nil {
 bestErr = err
 }
 if start.IsZero() {
 start = time.Now()
 } else if d := time.Since(start) + nextSleep; d >= arbitraryTimeout {
 break
 }
 time.Sleep(nextSleep)
 nextSleep += time.Duration(rand.Int63n(int64(nextSleep)))
 }
 return bestErr
}

每次重试等待随机延长的时间, 直到 f() 执行完成 或不再重试。

实现二

func Retry(attempts int, sleep time.Duration, f func() error) (err error) {
 for i := 0; ; i++ {
 err = f()
 if err == nil {
 return
 }
 if i >= (attempts - 1) {
 break
 }
 time.Sleep(sleep)
 }
 return fmt.Errorf("after %d attempts, last error: %v", attempts, err)
}

对函数重试 attempts 次,每次等待 sleep 时间, 直到 f() 执行完成。


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:lryong

查看原文:重试机制的实现

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
6142 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏