重试

为了克服网络问题,重试是我们常用的手段之一。但必须记住:重试的姿势非常重要。照应一句古话:“差若毫厘,谬以千里”。

正确的姿势是便是:

如果请求没有成功,以指数型延迟重试

指数退避

Exponential backoff is an algorithm that uses feedback to multiplicatively decrease the rate of some process, in order to gradually find an acceptable rate

通俗的的讲,网络上的节点在发送数据冲突之后,不应立即尝试重发,而应该等待一段时间再发送,等待时间是指数增长,从而避免频繁的触发冲突。在计算机网络中,二进制指数退避算法常常作为避免网络堵塞的一部分,用于同一数据块的重发策略。

发生n次冲突之后,等待时间在0~2^n-1个间隙时间(slot time)之间随机选择。比如第一次冲突之后,每个发送方会等待0或者1个间隙;第二次冲突之后,或等待时间会在0到3个间隙任意选择,依次类推,随着冲突次数的增加,发送方等待的时间可能成倍增加。

冲突达到一定次数,指数运算会停止,表示等待时间不会无限制增加下去。比如设置上限n=10,则最长等待时间为1023个时间间隙。同样,发送不可能永远的尝试下去,所以流程一般会在16次重试之后终止。

具体的退避算法:

1. 确定基本退避时间:争用期
2. 确定等待时间上限(max)。假设重传次数超过10次之后,k就不再增大。计算公式:k表示计算冲突等待时间的指数, k=min(重传次数, max)
3. 当重传达到16次仍不成功,则数据需要丢弃,并向高层报告。

退避算法的应用场景:

1. 三方支付中交易结果的推送通知。
2. 轮询,不间断的固定时间间隔的请求接口。

重试的问题

以下面的代码说明一下:

retryTimes := 1
for err != nil && retryTimes <= 3 {
	//请求失败后,重新尝试
	body, err = curl.GetCurlClient(curl.DefaultTimeout).PostJson(link, data)
	retryTimes ++
}

假设后端服务每个接口的上限是10000QPS,在超过这个上限之后,优雅降级系统会拒绝所有的额外请求(503的状态码就是干这个用的)。前端业务以固定的QPS请求服务,如果网络稍微抖动,使得100QPS请求失败。重试就会导致后端后续会接收到10100的QPS。其中的100QPS请求会因为服务过载而失败。

如果它们的网络还有问题,那么后续就会有200QPS因为过载而失败。以此类推…

需要考虑的问题

放大服务负载的重试策略可能很难遇见。而且即使遇见了,也很难确定是不是重试导致的。但还是需要将重试的坏降到最小。

  1. 一定要使用随机化的,指数型递增的重试周期
  2. 将重试错误和非重试错误码做明确区分。这样业务端也可以依据错误码做请求策略调整​

Example Code

指数退避的Go代码:

//exponential back-off
func TryAgain(url string) error {
    const timeout = 1 * time.Minute
    deadline := time.Now().Add(timeout)
    for tries := 0; time.Now().Before(deadline); tries++ {
        //....
        time.sleep(time.Second << tries)
    }
}

参考文章:

  1. http://hugnew.com/?p=814