php 重试,重试机制的实现

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

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

case

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 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() 执行完成。

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

你可能感兴趣的:(php,重试)