https://gocn.vip/topics/10135
重试解决什么问题
短时故障的产生原因
处理短时故障的挑战
重试分为几步
gRPC 是如何进行重试的
长时间不可用,如光纤被挖断,机房被炸等
短时间不可用,比如网络出现抖动,正在通信的对端机器正好重新上线等
而重试是应对短时故障利器,简单却异常有效。
就互联网公司的服务而言,通过冗余,各种切换等已经极大提高了整体应用的可用性,但其内部的短时故障却是连绵不断,原因有这么几个:
应用所使用的资源是共享的,比如 docker、虚拟机、物理机混布等,如果多个虚拟单位 (docker 镜像、虚拟机、进程等) 之间的资源隔离没有做好,就可能产生一个虚拟单位侵占过多资源导致其它共享的虚拟单元出现错误。这些错误可能是短时的,也有可能是长时间的。
现在服务器都是用比较便宜的硬件,即使是最重要的数据库,互联网公司的通常做法也是通过冗余去保证高可用。贵和便宜的硬件之间有个很重要的指标差异就是故障率,便宜的机器更容易发生硬件故障,虽然故障率很低,但如果把这个故障率乘以互联网公司数以万计、十万计的机器,每天都会有机器故障自然是家常便饭。这里有个硬盘故障率统计很有意思可以看看。
除掉本身的问题外,现今的互联网架构所需要的硬件组件也更多了,比如路由和负载均衡等等,更多的组件,意味着通信链路上更多的节点,意味着增加了更多的不可靠。
应用之间的网络通信问题,在架构设计时,对网络的基本假设就是不可靠,我们需要通过额外的机制弥补这种不可靠,有人问了,我的应用就是一个纯内网应用,网络都是内网,也不可靠么?嗯是的,不可靠。
感知。应用需要能够区分不同类型的错误,不同类型的错误对应的错误处理方式是不同的,没有哪种应对手段可以处理所有的错误。比如网络抖动我们简单重试即可,如果网络不可用,对于一个可靠的存储系统,可能就需要经历选主,副本切换等复杂操作才能保证数据的正确性。
处理。如何选择一个合适的处理策略对于快速恢复故障、缩短响应时间以及减少对对端的冲击是非常重要的。
5.2 如何决策
对于哪些错误可以重试是可配置的。通常而言,只有那些明确标识对端没有接收处理请求的错误才需要被重试,比如对端返回一个 UNAVAILABLE 错误,这代表对端的服务目前处于不可用状态。但也可以配置一个更加激进的重试策略,但关键是需要保证这些被重试的 gRPC 请求是幂等的,这个需要服务使用者和提供者共同协商出一个可以被重试的错误集合。
5.3 重试策略
gRPC 的重试策略分为两类
重试策略,失败后进行重试。
对冲策略,一次请求会给对端发出多个相同请求,只要有一个成功就认为成功。
先说下重试策略
重试策略
重试之时间策略
gPRC 用了上面我们提到的 指数避退 + 随机间隔 组合起来的方式进行重试,详见这里
/* 伪码 */
ConnectWithBackoff()
current_backoff = INITIAL_BACKOFF
current_deadline = now() + INITIAL_BACKOFF
while (TryConnect(Max(current_deadline, now() + MIN_CONNECT_TIMEOUT))
!= SUCCESS)
SleepUntil(current_deadline)
current_backoff = Min(current_backoff * MULTIPLIER, MAX_BACKOFF)
current_deadline = now() + current_backoff +
UniformRandom(-JITTER * current_backoff, JITTER * current_backoff)
上面的算法里有这么几个关键的参数
INITIAL_BACKOFF:第一次重试等待的间隔
MULTIPLIER:每次间隔的指数因子
JITTER:控制随机的因子
MAX_BACKOFF:等待的最大时长,随着重试次数的增加,我们不希望第 N 次重试等待的时间变成 30 分钟这样不切实际的值
MIN_CONNECT_TIMEOUT:一次成功的请求所需要的时间,因为即使是正常的请求也需要有响应时间,比如 200ms,我们的重试时间间隔显然要大于这个响应时间才不会出现请求明明已经成功,但却进行重试的操作。
通过指数的增加每次重试间隔,gRPC 在考虑对端服务和快速故障处理中间找到了一个平衡点。
重试之次数策略
上面的算法里面没有关于次数的限制,gRPC 中的最大重试次数是可配置的,硬限制的最大值为5 次,设置这个硬限制的目的我想主要还是出于对对端服务的保护,避免一些人为的错误。
再说下对冲策略
对冲策略
对冲之时间策略
对冲策略里面,请求是按照如下逻辑发出的:
第一次正常的请求正常发出
在等待固定时间间隔后,没有收到正确的响应,第二个对冲请求会被发出
再等待固定时间间隔后,没有收到任何前面两个请求的正确响应,第三个会被发出
一直重复以上流程直到发出的对冲请求数量达到配置的最大次数
一旦收到正确响应,所有对冲请求都会被取消,响应会被返回给应用层
对冲之次数策略
次数和上面重试是一样的限制,都是 5 次。
其它需要注意的问题
不同的对冲请求应该被对端不同的实例处理
对冲策略应该只用于幂等的操作,因为不同的对冲的请求通常是由不同的对端实例处理的
5.4 重试失败
当然不能一直重试,对于重试失败,gRPC 有以下的策略以顾全大局,对于每个 server,客户端都可配置一个针对该 server 的限制策略如下:
“retryThrottling”: {
“maxTokens”: 10,
“tokenRatio”: 0.1
}
对于每个 server,gRPC 的客户端都维护了一个 token_count 变量,变量初始值为配置的 maxTokens 值,每次 RPC 请求都会影响这个 token_count 变量值:
每次失败的 RPC 请求都会对 token_count 减 1
每次成功的 RPC 请求都会对 token_count 增加 tokenRation 值
如果 token_count <= (maxTokens / 2),那么后续发出的请求即使失败也不会进行重试了,但是正常的请求还是会发出去,直到这个 token_count > (maxTokens / 2) 才又恢复对失败请求的重试。这种策略可以有效的处理长时间故障。
当然重试失败还能更进一步,比如 Netflix 出品的hytrix能对故障进行熔断&降级处理,感兴趣的读者可以进一步了解