重试

[TOC]

重试可能方法服务器过载问题

##限制重试次数

限制重试3次。某个请求如果已经失败3次, 我们会将该错误回应给调用者。如果一个请求已经三次选择了过载的任务, 再重试也很可能无济于事,这时整个数据中心可能都处于过载状态。

控制重试阈值

重试需要控制比例,每个客户端都跟踪重试与请求的比例。一个请求在这个比例低于10%的时候才会重试。这里的逻辑是,如果仅仅只有一小部分任务处于过载状态, 那么重试数量应该是相对较小的。

用一个实际例子来说(一个很糟糕的情况),我们假设一个数据中心正在接受一小部分请求,而大部分请求都被拒绝了。这里用X代表客户端逻辑向这个数据中心发出请求的速率。由于大部分请求会被重试,这里请求的数量将会增长得非常快,接近3X(由于每个请求会被重试3次)。虽然我们在这里限制了重试导致的请求数量,3倍的请求増长仍然是很多的,尤其是当拒绝请求的成本不能忽略的时候。然而,通过加入一个按客户端重试的限制(10%重试比例),实际上我们将重试请求限制在大多数情况下仅增加为原来的1.1倍。这样的改进是很显著的。

或者类似每分钟最多能重试多少次,超过阈值直接请求失败处理。这样至少不易造成全局连锁性故障;

避免多层级重试

考虑一个极端情况,如果终端APP 到 DB 有三层,如果允许重试3次,最多可以请求四次,如果每层都进行重试,最多可能有 4*4*4=64 次请求落到 DB 上,这样重试无法解决问题,而且会带来更大的灾难。

“过载; 无须重试”

流量是会被放大的,一次重试请求可能会在多层应用中进行传递,对于一些场景,重试可能无意义,反而徒增上有系统的压力。这时应该返回 “过载,无需重试” 而不是 “任务过载”。

请求只应该在被拒绝的层面上面的那一层进行重试。当我们决定某个请求无法被处理,同时不应该被重试时,返回一个“过载;无须重试”错误,以避免触发一个大型的重试爆炸。例如:

假设数据库前端( DB Frontend)目前正处于过载状态, 拒绝请求。在 Bcakend B 会根据前述的规则重试请求。 然而当 Bcakend B 确定数据库前端无法处理该请求时(例如,请求已经重试3次) Bcakend B 需要给 Bcakend A 返回一个“过载; 无须重试”错误, 或者某个降级回复给 Bcakend A 会根据它收到的回复进行处理。

need-not-retry

这里的关键点是, 数据库前端拒绝的请求应该仅仅在后端 Bcakend B 处重试一一在它的直接上层处。如果多层都要进行重试, 会造成批量重试爆炸情况

使用明确的返回代码

使用明确的返回代码, 同时详细考虑每个错误模式应该如何处理。

可重试错误和不可重试错误要分开。

重试时间随机化

重试时间需要考虑指数退让。同时还需要加入一些 jitter 即随机波动的值,尽量错开客户端的重试时间点,避免集中到一起。

小结

重试要控制最大次数;

重试要满足幂等;

全局重试预算,控制重试比例,不能大面积重试,或者类似每分钟最多能重试多少次,超过阈值直接请求失败处理。这样至少不易造成全局连锁性故障;

重试时间要指数退让,

重试时间加入随机话,要随机分布在一个时间窗口内,避免碰撞到一起;

对于已知的,重试无法解决的问题,直接回复无需重试,避免不必要的重试浪费;

重试不应该逐层传递,在出现失败的一层重试,若无法解决则返回客户端无需重试,避免无效重试和重试倍数放大;

代码实现,以 kratos 的实现为例:

// DefaultBackoffConfig uses values specified for backoff in common.
var DefaultBackoffConfig = BackoffConfig{
	MaxDelay:  120 * time.Second,
	BaseDelay: 1.0 * time.Second,
	Factor:    1.6,
	Jitter:    0.2,
}

// Backoff defines the methodology for backing off after a call failure.
type Backoff interface {
	// Backoff returns the amount of time to wait before the next retry given
	// the number of consecutive failures.
	Backoff(retries int) time.Duration
}

// BackoffConfig defines the parameters for the default backoff strategy.
type BackoffConfig struct {
	// MaxDelay is the upper bound of backoff delay.
	MaxDelay time.Duration

	// baseDelay is the amount of time to wait before retrying after the first
	// failure.
	BaseDelay time.Duration

	// factor is applied to the backoff after each retry.
	Factor float64

	// jitter provides a range to randomize backoff delays.
	Jitter float64
}


func (bc *BackoffConfig) Backoff(retries int) time.Duration {
	if retries == 0 {
		return bc.BaseDelay
	}
	backoff, max := float64(bc.BaseDelay), float64(bc.MaxDelay)
	for backoff < max && retries > 0 {
		backoff *= bc.Factor
		retries--
	}
	if backoff > max {
		backoff = max
	}
	// Randomize backoff delays 
	backoff *= 1 + bc.Jitter*(rand.Float64()*2-1)
	if backoff < 0 {
		return 0
	}
	return time.Duration(backoff)
}

扩展阅读

https://aws.amazon.com/cn/blogs/architecture/exponential-backoff-and-jitter/

《Google SRE》

更新时间: