持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
Kitex 重试机制
什么时候会重试?
kitex 目前有如下三种重试机制:
- 超时重试
- Backup Request(一段时间内未收到响应,进行重试)
- 连接失败重试
框架目前有三类重试:超时重试、Backup Request,建连失败重试(默认)。其中建连失败是网络层面问题,由于请求未发出,框架会默认重试,业务无需关注。
超时重试
超时异常由于网络抖动,下游负载高或者 GC 等导致 GC 卡顿导致超时。
如何设置重试策略?
异常一般有如下三种:
- 业务自定义异常
- 超时异常
- 非超时框架类异常
业务判断异常一般两种方式
-
下游抛出的Error
-
下游返回的错误码
-
返回Error自定义异常配置方式如下:
RetryWithError(IsErrorRetry)
- 通过
BaseResp自定义错误码配置
RetryWithResult(IsResultRetry)
- 重试次数配置 重试最大次数,如果超过最大重试次数则停止重试。
WithMaxRetry(count)
- 请求总耗时
首次失败请求后重试请求耗时达到了限制的
duration则停止后续重试,默认无限制。 异常重试策略代码:
退避策略
- 无退避策略 - NoneBackOff 默认
- 固定时长退避 - FixedBackOff WithFixedBackOff(time)
- 随机时长退避 - RandomBackOff WithRandomBackOff(minTime, maxTime)
- 线性递增退避 - IncrementingBackOff WithIncrementingBackOff(initTime, incrementTime)
- 指数递增退避 - ExponentialBackOff WithExponentialBackOff(maxTime)
func NewFailurePolicy() *FailureRetryPolicy {
p := &FailureRetryPolicy{
RetryFailures: make([]Failure, 1),
StopPolicy: StopPolicy{
MaxRetry: 3,
},
BackOffPolicy: noneBackOff,
CBPolicy: CBPolicy{
Enabled: false,
},
}
// 默认超时重试
p.RetryFailures[0] = timeoutFailureObj
return p
}
type FailureRetryPolicy struct {
RetryFailures []Failure
StopPolicy StopPolicy
BackOffPolicy BackOffPolicy
CBPolicy CBPolicy
Percentage float64
failureStatus int
}
func (p *FailureRetryPolicy) RetryWithError(f IsErrorRetryFunc) *FailureRetryPolicy {
// ...
}
func (p *FailureRetryPolicy) RetryWithResult(f IsResultRetryFunc) *FailureRetryPolicy {
// ...
}
func (p *FailureRetryPolicy) WithMaxRetry(retryTimes int) *FailureRetryPolicy {
p.StopPolicy.MaxRetry = maxRetry
return p
}
func (p *FailureRetryPolicy) WithMaxDuration(d time.Duration) *FailureRetryPolicy {
p.StopPolicy.MaxDuration = d
return p
}
func (p *FailureRetryPolicy) WithChainRetryStop() *FailureRetryPolicy {
p.StopPolicy.ChainStop = true
return p
}
func (p *FailureRetryPolicy) WithDDLStop() *FailureRetryPolicy {
p.StopPolicy.DDLStop = true
return p
}
func (p *FailureRetryPolicy) WithFixedBackOff(d time.Duration) *FailureRetryPolicy {
p.BackOffPolicy = &FixedBackOffPolicy{d}
return p
}
func (p *FailureRetryPolicy) WithRandomBackOff(min time.Duration, max time.Duration) *FailureRetryPolicy {
p.BackOffPolicy = &RandomBackOff{min, max}
return p
}
func (p *FailureRetryPolicy) WithRetryBreaker(errRate float64) {
p.CBPolicy.ErrorRate = errRate
p.CBPolicy.Enabled = true
}
func (p *FailureRetryPolicy) WithRetryPercentage(pert float64) {
p.Percentage = pert
}
业务配置开启异常重试后,框架会按照配置策略进行重试,默认重试策略是超时重试
超时重试
// 开启链路中止和DDL中止
var opts []client.Option
fp := retry.NewFailurePolicy()
fp.WithChainRetryStop().WithDDLStop() // 开启链路中止和DDL中止
opts = append(opts, client.WithFailureRetry(fp))
xxxCli := xxxservice.NewClient("psm", opts...)
Backup Request
BackupRequest 策略代码:
func NewBackupPolicy(d time.Duration) *BackupPolicy {
if d <= 0 {
panic("invalid backup retry duration")
}
p := &BackupPolicy{
RetryDuration: d,
StopPolicy: StopPolicy{
MaxRetry: 2,
},
CBPolicy: CBPolicy{
Enabled: false,
},
}
return p
}
type BackupPolicy struct {
RetryDuration time.Duration
StopPolicy StopPolicy
CBPolicy CBPolicy
}
func (p *BackupPolicy) WithChainRetryStop() *BackupPolicy {
p.StopPolicy.ChainStop = true
return p
}
func (p *BackupPolicy) WithRetryBreaker(errRate float64) {
p.CBPolicy.ErrorRate = errRate
p.CBPolicy.Enabled = true
}
func (p *BackupPolicy) WithMaxRetry(retryTimes int) *BackupPolicy {
if retryTimes > maxRetryTimes {
panic("maxRetryTimes for backup request is 3")
}
p.StopPolicy.MaxRetry = retryTimes
return p
}
配置如下:
// 首次请求20ms未返回,发起 backup 请求,并开启链路中止
var opts []client.Option
bp := retry.NewBackupPolicy(20 * time.Millisecond) // 请求20ms未返回则发起backup request
bp.WithChainRetryStop() // 开启链路中止
opts = append(opts, client.WithBackupRequest(bp))
xxxCli := xxxservice.NewClient("psm", opts...)
重试如何防雪崩?
重试只是一种对偶发失败的补偿,但在下游节点宕机或负载过高时,重试是无意义的或会加剧下游服务压力导致雪崩,可以通过一些策略避免。
限制链路重试
Google SRE中指出了Google内部使用特殊错误码的方式来实现:
- 统一约定一个特殊的
status code,它表示:调用失败,但别重试 - 任何一级重试失败后,生成该
status code并返回给上层 - 上层收到该
status code后停止对这个下游的重试,并将错误码再传给自己的上层
这种方式理想情况下只有最下一层发生重试,它的上游收到错误码后都不会重试,放大倍数最大也就是r倍。
限制单点重试
实现起来就是限制 失败/成功 的比率,即给重试增加熔断功能。
我们采用了常见的滑动窗口的方法来实现。如下图,内存中为每个调用维护一个滑动窗口,比如窗口分10个bucket,每个bucket里面记录了1s内某个rpc的请求结果数据(成功、失败、超时等等)。新的一秒到来时,生成新的bucket,并淘汰最早的一个bucket,只维持10s的数据。在新请求这个rpc失败时,根据前10s内的 失败/成功 是否超过阈值来判断是否retry。默认阈值是0.1,下游最多承受平常 1.1 倍的QPS。当然也支持用户自行设置熔断开关和阈值。