在当前主流的微服务架构体系中,由于存在大量的远程服务调用,会存在各种各样的稳定性问题,包括但不仅限于网络拥堵,服务宿主机系统抖动,服务配置下发导致的额外开销等等。为了尽可能的提升服务质量,我们常常需要在各种存在风险的远程调用中,采用适当的重试策略,今天就简单讨论下重试相关的问题。
重试策略
所谓重试策略,首先关注的是两个参数:1.重试次数;2.调用间隔。
两个参数都很重要,首先来说重试次数,如果对重试次数不加限制,在出现下游系统故障,或者恰好命中下游系统bug的情况下,可能出现在相当一段时间内的重试都会以失败告终,这时候的重试不仅没有起到提升对外服务质量的效果,反而会对当前服务和下游服务都造成非常大的不必要负荷。
其次是重试调用间隔的问题,常见的调用间隔策略包括:
- 固定时间 每两次重试调用之间的间隔固定
- 指数增长 每两次重试调用之间的时间间隔指数增长
在此基础上,为了尽可能降低请求尖刺,会适当引入一定的随机策略。
代码实践
知道了重试需要关注的两个方面之后,我们来简单看下在golang
语言中,关于重试策略的一些实际代码实现。
首先看下继承自Hystrix的hystrix-go,它来源于Spring Cloud中重试组件的设计实现。在使用上,提供了较为完善的位置选项,能够满足不同的重试策略的配置,同时借助go
的channel
特性,同时对外提供了同步和异步的API。为了保证下游服务的负载,同时引入的熔断策略作为保护。
另外还有一个非常优秀的重试策略的库实现backoff,值得一提的是,这个库关于重试指数策略的部分,借鉴了Google
对于JAVA
的http client
的相关实现算法,旨在自适应的调整合适的适配下游负载的重试间隔。
具体到现实的RPC调用中,grpc
官方提供了retry的middleware
,来支持最基础的重试功能。
上面几个库都支持了对于重试条件的支持,即根据请求返回的报错,来决定要不要继续进行重试。
更进一步
由于重试存在潜在的请求放大的风险,针对于此,Google
的SRE
实践给出了一些实践建议:
- 针对每个失败请求,设置重试次数的上限,比如最多重试3次。
- 针对整个客户端的调用,设置最大的重试与请求的比例。即重试请求最大不会超过某个时间窗口内的请求数的10%,即写放大指数最大就是110%。
- 客户端记录一段时间内的重试次数,判断在最近的时间窗口内,如果出现了大量的服务都需要重试的情况,可以判断当前服务端处于过载状态,服务端也可以通过状态码直接返回“拒绝重试”的状态,而这个状态会被带到请求链路中抛到上层,避免更高层服务调用的重试。
另外,关于重试的时机选择,目前大多数的实现都是等前一个调用返回失败请求或者超时之后才进行下一步重试。对于重试请求的时机,Google有一篇文章谈到了一些优化措施,比如客户端可以根据过去一个时间窗口内的请求时长的pct999,判断大多数正常请求的耗时分布,当请求耗时已经达到这个阈值(在各个场景下,这个值都小于超时阈值),不必等请求返回而直接重试,这种策略叫做backup requests。在超时出现比较多的场景下,这种提早重试策略能够提升服务的响应速度,所带来的代价就是可能出现的一些额外请求。
最后,在微服务中对于重试的实践中,具体在哪层操作重试?有的是在最外层请求包装重试,优点在于直接对最外层服务负责,请求方法指数最方便控制,缺点在于单次重试开销较大;有的是在各个服务请求处就近重试,有点在于请求重试开销较小,有利于提升各个服务的服务质量指标,缺点在于可能出现多层嵌套重试的情况,如果重试次数限制有问题的话,容易出现请求放大的问题。