服务在申请资源,如果遇到网络异样等状况,导致申请失败,这时须要有个重试机制来持续申请。 常见的做法是重试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 timeoutfunc 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() 执行实现。