微服务中任务失败后如何进行重试
一、短时间的故障原因
1. 应用所使用的资源是共享的,比如docker、虚拟机、物理机混布等,如果多个虚拟单位(docker镜像、虚拟机、进程等)之间的资源隔离没有做好,就可能产生一个虚拟单位侵占过多资源导致其它共享的虚拟单元出现错误。这些错误可能是短时的,也有可能是长时间的。
2. 现在服务器都是用比较便宜的硬件,互联网公司的通常做法也是通过冗余去保证高可用。贵和便宜的硬件之间有个很重要的指标差异就是故障率,便宜的机器更容易发生硬件故障,虽然故障率很低,但如果把这个故障率乘以互联网公司数以万计、十万计的机器,每天都会有机器故障自然是家常便饭。
3. 除掉本身的问题外,现今的互联网架构所需要的硬件组件也更多了,比如路由和负载均衡等等,更多的组件,意味着通信链路上更多的节点,意味着增加了更多的不可靠。
4. 应用之间的网络通信问题,在架构设计时,对网络的基本假设就是不可靠,我们需要通过额外的机制弥补这种不可靠。
二、grpc的重试设置
重试机制主要是在调度方设置,我们可以看客户端的调度传参设计
/grpc/clientconn.go
在针对DialOption方法提供了两个方法类型设置/grpc/dialoptions.go
如下是参数解析的方法/grpc/service_config.go
三、实践使用grpc重试机制
使用会颇有麻烦,grpc提供了两个方式进行设置/grpc/dialoptions.go
而重试机制的实际对象是/grpc/service_config.go
如下是具体设置的参数示例:→ 先处理服务端,在服务端中直接就是一个err的错误返回/apps/user/rpc/internal/logic/loginlogic.go
而如下是重点,客户端的配置设置/apps/user/rpc/internal/svc/servicecontext.go
在retryPolicy的参数详情是:
MaxAttempts:最大重试次数
InitialBackoff:默认退避时间
MaxBackoff:最大退避时间
BackoffMultiplier:退避时间增加倍率
RetryableStatusCodes: 服务端返回什么错误码才重试
关于InitialBackoff的参数
在InitialBackoff的参数中是固定以s为后缀,即秒为单位
关于RetryableStatusCodes的值
可看grpc/codes/code.go中的内容
四、理解grpc的重试机制
4.1 重试过程
感知错误。通常我们使用错误码识别不同类型的错误。比如在REST风格的应用里面,HTTP的status code可以用来识别不同类型的错误。
决策是否应该重试。不是所有错误都应该被重试,比如HTTP的4xx的错误,通常4xx表示的是客户端的错误,这时候客户端不应该进行重试操作。什么错误可以重试需要具体情况具体分析,对于网络类的错误,我们也不是一股脑都进行重试,比如zookeeper这种强一致的存储系统,发生了network partition之后,需要经过一系列复杂操作,简单的重试根本不管用。
选择重试策略。选择一个合适的重试次数和重试间隔非常的重要。如果次数不够,可能并不能有效的覆盖这个短时间故障的时间段,如果重试次数过多或者重试间隔太小,又可能造成大量的资源(CPU、内存、线程、网络)浪费。合适的次数和间隔取决于重试的上下文。
举例:如果是用户操作失败导致的重试,比如在网页上点了一个按钮失败的重试,间隔就应该尽量短,确保用户等待时间较短;如果请求失败成本很高,比如整个流程很长,一旦中间环节出错需要重头开始,典型的如转账交易,这种情况就需要适当增加重试次数和最长等待时间以尽可能保证短时间的故障能被处理而无需重头来过。失败处理与自动恢复。短时故障如果短时间没有恢复就变成了长时间的故障,这个时候我们就不应该再进行重试了,但是等故障修复之后我们也需要有一种机制能自动恢复
4.2 重试时间间隔策略
指数间隔。重试间隔时间按照指数增长,如等 3s 9s 27s后重试。指数避退能有效防止对对端造成不必要的冲击,因为随着时间的增加,一个故障从短时故障变成长时间的故障的可能性是逐步增加的,对于一个长时间的故障,重试基本无效。
重试间隔线性增加。重试间隔的间隔按照线性增长,而非指数级增长,如等 3s 7s 13s后重试。间隔增长能避免长时间等待,缩短故障响应时间。
固定间隔。重试间隔是一个固定值,如每3s后进行重试。
立即重试。有时候短时故障是因为网络抖动造成的,可能是因为网络包冲突或者硬件有问题等,这时候我们立即重试通常能解决这类问题。但是立即重试不应该超过一次,如果立即重试一次失败之后,应该转换为指数避退或者其它策略进行,因为大量的立即重试会给对端造成流量上的尖峰,对网络也是一个冲击。
随机间隔。当服务有多台实例时,我们应该加入随机的变量,比如A服务请求B服务,B服务发生短时间不可用,A服务的实例应该避免在同一时刻进行重试,这时候我们对间隔加入随机因子会很好的在时间上平摊开所有的重试请求
4.3 grpc的重试策略
https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md
在目前的版本中主要是重试策略、失败后进行重试。
上面的算法里有这么几个关键的参数
INITIAL_BACKOFF:第一次重试等待的间隔
MULTIPLIER:每次间隔的指数因子
JITTER:控制随机的因子
MAX_BACKOFF:等待的最大时长,随着重试次数的增加,我们不希望第N次重试等待的时间变成30分钟这样不切实际的值
MIN_CONNECT_TIMEOUT:一次成功的请求所需要的时间,因为即使是正常的请求也需要有响应时间,比如200ms,我们的重试时间间隔显然要大于这个响应时间才不会出现请求明明已经成功,但却进行重试的操作。
通过指数的增加每次重试间隔,gRPC在考虑对端服务和快速故障处理中间找到了一个平衡点。
4.4 源码
重试策略
grpc/stream.go在shouldRetry的方法中主要是判断是否应重试。
在流程中先验证是否为stream方式,并基于trailers获取到消息元数据
在下面的流程中获取到err的code信息,如果这个信息是我们所设计的可重试的参数则往后执行
可以看出,退避时间随着重试次数指数级增长 InitialBackoff * math.Pow(rp.BackoffMultiplier, float64(cs.numRetriesSincePushback))
业务重试grpc/stream.go
如果执行失败grpc/stream.go
首先,当前的尝试 cs.attempt 将被标记为已完成,其结果。
会被传递给 cs.finish()。
然后,会调用 cs.shouldRetry() 判断当前错误是否应该重试(服务端错误 DoNotTransparentRetry )。如果返回的错误可以重试,则会通过 cs.newAttemptLocked() 创建新的尝试,即新的流,并在其上发起新的请求。如果没有返回新的错误,则尝试从回放缓冲区 cs.replayBufferLocked() 中获取上一次重试的错误。
如果在回放缓冲区中找到了错误,继续进行下一次重试,否则返回 nil 表示重试成功。