服务在申请资源,如果遇到网络异样等状况,导致申请失败,这时须要有个重试机制来持续申请。 常见的做法是重试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)
在申请时的重试流程:
- 如果没有设置重试次数,执行
r.client.execute(r)
:间接申请 Request , 返回 Response 和 error。 - 如果
r.client.RetryCount
不等于0 ,执行Backoff()
函数 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()
函数的流程:
Backoff()
接管 处理函数 和 可选的 Option 函数(retry optione) 作为参数- 默认策略3次重试, 通过 步骤一 预设的 Options, 自定义重试策略
- 设置申请的 repsonse 和 error 变量
开始进行
opts.maxRetries
次 HTTP 申请:- 执行处理函数 (发动 HTTP 申请)
- 如果返回后果不为空并且 context 不为空,放弃 repsonse 的申请上下文。 如果上下文出错, 退出
Backoff()
流程 - 执行
retryConditions()
, 设置查看重试的条件。 - 依据 needsRetry 判断是否退出流程
- 通过
sleepDuration()
计算 Duration(依据此次申请resp, 等待时间配置,最大超时工夫和重试次数算出 sleepDuration。 工夫算法绝对简单, 具体参考: Exponential Backoff And Jitter) - 期待 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()
执行实现。