重试

Hertz 为用户提供了自定义的重试逻辑,下面来看一下 Client 的 Retry 使用方法。注意:Hertz 版本 >= v0.4.0

Retry 次数及延迟策略配置

首先创建 Client ,使用配置项 WithRetryConfig() 来配置 Retry 相关逻辑(这一部分主要配置 Retry 的次数和延时部分)

package main

import (
	"github.com/cloudwego/hertz/pkg/app/client"
	"github.com/cloudwego/hertz/pkg/app/client/retry"
)

func main() {
	cli, err := client.NewClient(
		client.WithRetryConfig(
			retry.WithXxx(), // 设置 Retry 配置的方式
		),
	)
}
配置名称 类型 介绍
WithMaxAttemptTimes uint 用于设置最大尝试次数,默认1次(即只请求1次不重试)
WithInitDelay time.Duration 用于设置初始延迟时间,默认 1ms
WithMaxDelay time.Duration 用于设置最大延迟时间,默认100ms
WithMaxJitter time.Duration 用于设置最大扰动时间,需要配合 RandomDelayPolicy 使用,会生成不超过最大扰动时间的随机时间,默认20ms
WithDelayPolicy type DelayPolicyFunc func(attempts uint, err error, retryConfig *Config) time.Duration 用于设置延迟策略,可以使用以下四种的任意结合, FixedDelayPolicy, BackOffDelayPolicy, RandomDelayPolicy, DefaultDelayPolicy(详情见下一小节: 延迟策略)默认使用 DefaultDelayPolicy (即重试延迟为0)

延迟策略

retry.WithDelayPolicy() 使用方法

cli, err := client.NewClient(
		client.WithRetryConfig(
			...
			retry.WithDelayPolicy(retry.CombineDelay(retry.FixedDelayPolicy, retry.BackOffDelayPolicy, retry.RandomDelayPolicy)),
    		...
		),
	)
函数名称 说明
CombineDelay 用于将下面四种策略进行任意组合,将所选策略计算出的值进行加和。当你只需要下面四种策略中的一种时,你可以选择使用 CombineDelay 或选择直接将任意一种策略传入 WithDelayPolicy 作为参数
FixedDelayPolicy 用于设置固定延迟时间,使用 WithInitDelay 设置的值,来生成等值的延迟时间
BackOffDelayPolicy 用于设置指数级延迟时间,使用 WithInitDelay 设置的值,根据当前是第几次重试,指数级生成延迟时间
RandomDelayPolicy 用于设置随机延迟时间,使用 WithMaxJitter 设置的值,生成不超过该值的随机延迟时间
DefaultDelayPolicy 用于设置默认延迟时间,返回 0 ,一般单独使用,和其他策略结合没有效果

完整示例

package main

import (
	"github.com/cloudwego/hertz/pkg/app/client"
	"github.com/cloudwego/hertz/pkg/app/client/retry"
)
func main() {

	cli, err := client.NewClient(
		client.WithRetryConfig(
			retry.WithMaxAttemptTimes(3), // 最大的尝试次数,包括初始调用
			retry.WithInitDelay(1*time.Millisecond), // 初始延迟
			retry.WithMaxDelay(6*time.Millisecond), // 最大延迟,不管重试多少次,策略如何,都不会超过这个延迟
			retry.WithMaxJitter(2*time.Millisecond), // 延时的最大扰动,结合 RandomDelayPolicy 才会有效果
			/*
			   配置延迟策略,你可以选择下面四种中的任意组合,最后的结果为每种延迟策略的加和
			   FixedDelayPolicy 使用 retry.WithInitDelay 所设置的值 ,
			   BackOffDelayPolicy 在 retry.WithInitDelay 所设置的值的基础上随着重试次数的增加,指数倍数增长,
			   RandomDelayPolicy 生成 [0,2*time.Millisecond)的随机数值 ,2*time.Millisecond 为 retry.WithMaxJitter 所设置的值,
			   DefaultDelayPolicy 生成 0 值,如果单独使用则立刻重试,
			   retry.CombineDelay() 将所设置的延迟策略所生成的值加和,最后结果即为当前次重试的延迟时间,
			   第一次调用失败 -> 重试延迟:1 + 1<<1 + rand[0,2)ms -> 第二次调用失败 -> 重试延迟:min(1 + 1<<2 + rand[0,2) , 6)ms -> 第三次调用成功/失败
			*/
			retry.WithDelayPolicy(retry.CombineDelay(retry.FixedDelayPolicy, retry.BackOffDelayPolicy, retry.RandomDelayPolicy)),
		),
	)
}

Retry 条件配置

如果你想要自定义配置重试发生的条件,你可以使用 client.SetRetryIfFunc() 配置,该函数的参数是一个函数,签名为:

func(req *protocol.Request, resp *protocol.Response, err error) bool

相关参数包括 Hertz 请求中的 reqresperr 字段,你可以通过这些参数,判断这个请求该不该重试。在如下例子中,当请求返回的状态码不是 200 或者调用过程中 err != nil 时我们返回 true,即进行重试。

cli.SetRetryIfFunc(func(req *protocol.Request, resp *protocol.Response, err error) bool {
   return resp.StatusCode() != 200 || err != nil
})

需要注意的是,如果你没有设置 client.SetRetryIfFunc() 。我们将会按照 Hertz 默认的重试发生条件进行判断,即判断请求是否满足下面的 DefaultRetryIf() 函数并且判断该调用是否是幂等调用(幂等调用:即 pkg/protocol/http1/client.go::Do()pkg/protocol/http1/client.go::doNonNilReqResp()canIdempotentRetry 为 true 的情况

// DefaultRetryIf Default retry condition, mainly used for idempotent requests.
// If this cannot be satisfied, you can implement your own retry condition.
func DefaultRetryIf(req *protocol.Request, resp *protocol.Response, err error) bool {
   // cannot retry if the request body is not rewindable
   if req.IsBodyStream() {
      return false
   }

   if isIdempotent(req, resp, err) {
      return true
   }
   // Retry non-idempotent requests if the server closes
   // the connection before sending the response.
   //
   // This case is possible if the server closes the idle
   // keep-alive connection on timeout.
   //
   // Apache and nginx usually do this.
   if err == io.EOF {
      return true
   }

   return false
}
func isIdempotent(req *protocol.Request, resp *protocol.Response, err error) bool {
   return req.Header.IsGet() ||
      req.Header.IsHead() ||
      req.Header.IsPut() ||
      req.Header.IsDelete() ||
      req.Header.IsOptions() ||
      req.Header.IsTrace()
}

Table - 1 Hertz 源码 doNonNilReqResp()canIdempotentRetry 为 true 的情况

doNonNilReqResp() 返回 true 的情况
err = conn.SetWriteDeadline(currentTime.Add(c.WriteTimeout))
err = reqI.Write(req, zw)
err = reqI.ProxyWrite(req, zw)
err = zw.Flush()
err = conn.SetReadTimeout(c.ReadTimeout)
( err = respI.ReadHeaderAndLimitBody() || err = respI.ReadBodyStream() ) && (err != errs.ErrBodyTooLarge)