无状态高可用:重试
从前一节”超时控制“可以看到分布式系统远没有单机系统可靠,因为网络,主机,或者服务自身问题导致分布式系统调用失败。 如何提供可靠的微服务调用,其答案就是重试策略。 重试这种策略实际上已经存在于底层的分布式框架中,以及早存在更为底层的网络和操作系统中。
以TCP协议为例子,发送TCP报文并开启定时器,如果未收到响应时候,可以通过重试确保数据包发送出去,并会重试多次。
分布式框架Dubbo,默认会而外重试两次,且选取不同节点重试
重试策略需要关注重试次数,以及重试间隔时间。
| 重拾策略 | 描述 |
|---|---|
| 禁止重试 | 禁止重试,业务调用失败。尽管这看来不可取,但在微服务场景下,快速失败而不是重拾,避免了上游调用等待超时。 |
| 重试一次 | 大部分分布式调用推荐这种方式,这种方式在服务器不可用情况下,再试一次,如果失败,尽快返回错误给调用者, |
| 重试N次 | 并指定每次重拾之间的休眠间隔,休眠时间保持固定,比如每次休眠10毫秒。 如果设置N较大而休眠时间设置较少,有可能对服务端造成较大压力 |
| 重试N次,休眠时间逐次增加 | 并指定每次重拾之间的休眠间隔,休眠时间随着每次重试增加,可以线性增长或者退避指数增长,比如第一次休眠10毫秒,随后30毫秒,70毫秒,130毫秒 |
| 指定总的重试时间 | 这个时间段内如果访问服务失败,休眠后重试,直到超出指定时间 |
重试间隔通常采用退避指数计算,即制定一个基础间隔时间和最大重试价格,每次重试间隔是按照退避指数增加, Feign的退避指数实现方式。代码如下:
long interval = (long) (period * Math.pow(1.5, attempt - 1));
interval = interval > maxPeriod ? maxPeriod : interval;、默认情况下,Feign的最大休眠间隔为maxPeriod是1000毫秒, period是基础间隔时间,为100毫秒,根据上面代码,休眠时间为 100、150、225、337、506...
Curator的ExponentialBackoffRetry 是退避指数实现方式,会每次随机延长休眠时间,实现代码与Feign类似:
long sleepMs = baseSleepTimeMs * Math.max(1, random.nextInt(1 << (retryCount + 1)));
if ( sleepMs > maxSleepMs )
{
log.warn(String.format("Sleep extension too large (%d). Pinning to %d", sleepMs, maxSleepMs));
sleepMs = maxSleepMs;
}我之前的一家物联公司,设备在弱网(网络不稳定)情况下连接物联网总是超时,后来分析是因为设备设置了退避指数但没有设置超时封顶时长,导致设备在不稳定的网络中,重试间隔非常大,当网络恢复稳定,设备还在休眠等待下一次重试。
在分布式系统中,重试N次需要谨慎设置,当服务不可用,或者正在恢复过程中,过多的重拾对网络和服务造成压力,严重会再次崩溃。另外一个情况是服务器本来已经处于高负载情况下,重试会加重服务器的负载导致系统无法恢复正常。实现退避指数休眠时间有助于保护对端服务。但也可能目标服务已经恢复正常,但自身还处于休眠时间.
下面的表格列出了常用分布式组件提供的重拾策略
| 组件 | 默认重试策略 |
|---|---|
| Dubbo | 提供了retries 配置指定重试次数,默认是2(org.apache.dubbo.common.Constants.DEFAULT_RETRIES) ,当Dubbo调用服务失败,会选择另外一个节点重试,直到超过重试次数 |
| Feign | 默认重拾策略为Retryer.NEVER_RETRY,不重试。 |
| Curator | Zookeeper的Java客户端,在org.apache.curator.retry包下提供了多种重试策略,重试1次,重试N次,无限次重试,指定总的重试时间等 |
| gRpc | 提供多种重试策略,最常用的是退避指数重试,gRpc也可以请求对冲策略,即并发发出多个请求,取第一个成功的的返回为结果 |
| Kafka | 批量发送消息的时候,通过Sender.canRetry 判断是否能重试,Kafka设定一个重试次数和最多重试时间,当能重试的时候,将发送失败消息放入重试队列等待发送。 |
| Jedis | Jedis 是一款访问Redis的Java客户端工具,默认会尝试5次链接Redis Server。 Jedis通过连接池管理redis链接,如果链接空闲,有可能先被Redis服务器主动关闭。这时候如果如果使用此链接,将导致访问失败,因此重试是解决redis暂时访问出错的一个好办法。 |
| Lettuce | Lettuce是另外一个Redis的Java客户端工具,它的默认重试机制是默认最小延迟:100ms,最大延迟 10s,每次失败指数增长,叠加随机抖动避免雪崩 |
并不是所有失败都需要重试,属于业务范畴的是调用失败,如扣费中的余额不足,下发命令到设备,已离线这种无需重拾,HTTP中404代表服务或者页面不存在,不需要重试。
选择策略依赖于具体应用,如定时上报数据到服务器,可以尝试多次,指定更长的超时。然而高并发的服务调用,这尽量减少重试次数。以避免对网络和服务器造成更大压力,较少的重试次数,也避免客户端堆积还海量请求导致内存溢出等故障,从而造成分布式系统连锁故障
如果分布式框架不具备重试策略设置,或者你的业务代码需要实现自己的重试策略,不依赖底层分布式框架重试,可以使用断路器机制,这在《快速失败里》中介绍使用Resilience4j实现断路器
注意:当客户端重试,服务端来说必须做到如下俩个功能,这将在后续章节里说明。
| 功能点 | 描述 |
|---|---|
| 幂等 | 客户端不管执行多少次,最终结果是一样的。幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。比如查询订单操作,总是幂等的,扣除余额则需要通过技术实现幂等 |
| 限流 | 因为重试带来较大的流量,尤其是服务端在高负载情况(高流量或者正在预热阶段),因此服务端需要限流来保护自己,不能完全信任客户端采取了限流等措施。 |
