SpringCloud OpenFeign 请求重试
前言
真实的微服务业务场景中,可能出现跨服务调用失败的情况。最常见的就是被调用的服务正在发布,由于微服务之间通常有依赖关系,发布有一定的先后顺序,对于一个微服务应用常见的发布策略有两种
先停掉集群中一半的实例,然后重新启动这些应用,完成之后再停掉另一半的集群实例重新启动。
一台实例一台实例重启
那么此时被停掉的应用会处于临时的不可用,但是下线的信息还没有被同步到注册中心,导致 Feign
调用的时候还是有可能被负载均衡策略选择到已经停掉的机器,从而导致调用失败。这种情况下我们应该要重新发起一次请求。
请求重试前置知识
在决定做重试前,我们应该要思考以下几个问题
200、4xx、5xx
哪些错误响应码需要重试?GET、POST、PUT、DELETE...
什么类型的请求可以重试?重试的这次请求怎样避免再次访问到不可用实例?
我们可以先认真思考上述问题,然后我们开始分析上述问题。
哪些错误响应码需要重试
常见的响应码除了200
就是 4xx、5xx
了。通常 4xx
错误是不该重试的,因为这是客户端错误。以 400、405
为例,不管重试多少次都是一样的结果,没必要浪费系统资源。
接下来就是 5xx
了,503
是肯定应该重试的,那 500
要不要重试?这个东西很难去界定,如果说是因为程序本身的 bug
导致,那大概率不用重试,因为还会失败。如果被调用方请求了一个第三方接口,然后因为奇怪的原因返回了奇怪的异常。这种我觉得是可以进行重试的,所以对于 5xx
我觉得可以定义一些特殊的异常标明它们是可重试的。
具体对哪些响应码/哪些异常重试还是看不同公司,还有很多公司无论请求成功失败一律返回 200
的呢对吧。。。。
哪些请求类型可以重试
接口的幂等性,这是很重要的一点,你敢对 POST
请求重试吗?即便不是微服务的调用,在设计普通 Rest
接口时我们也要考虑接口的幂等性。对于 POST
这种本身就不幂等的 HTTP
请求,对它开启重试不是自己给自己找 bug
吗。顺便想提一句那些啥请求都 POST
一把梭的公司做微服务调用重试的时候是不是又要加工作量了......当然车到山前必有路,很可能别人会采取不通过 OpenFeign
重试来解决问题~~
我们都知道 HTTP
请求中 GET、HEAD、PUT、DELETE
等方法都是幂等的,所以在正确的实现下我们可以对这些请求类型进行重试。
重试如何选择到可用实例
以新一代负载均衡器 SpringCloud LoadBalancer
为例,提供了两种负载均衡策略实现。
RoundRobinLoadBalancer
轮询(默认)RandomLoadBalancer
随机
在应用发布的场景下,无论是一台一台发布还是一半一半发布,这两种策略都没法保证我们重试的那一次能够访问到可用的实例。具体原因和解决方案我将会在后面 SpringCloud LoadBalancer
文章中分析。
OpenFeign 开启重试
OpenFeign
是通过 Retry
来实现重试的,默认是关闭该功能的,这一点我们可以从 FeignClientsConfiguration
里面看到
所以开启重试我们只需要自己定义一个 Retry
的 Bean
即可。
我们观察 Retry.Default
类的核心成员变量
Retryer 重试的原理
这个其实很简单,追踪一下 OpenFeign
调用的源码即可, SynchronousMethodHandler
类
代码中上来就是一个循环,如果我们调用过程中抛出了 RetryableException
,并且 retryer.continueOrPropagate(e)
还没超过重试次数就继续发起请求,如果到了重试次数就抛出异常结束循环。这问题就简单了,也就是说如果我们要控制 OpenFeign
发起重试只需要抛出 RetryableException
。
实现 ErrorDecoder
前面的文章我们提到了 Feign
的解码器,用于解析正常响应的结果。其实在 Feign
的错误响应结果也有一个专用的解码器。
在这行代码中如果出现了异常会调用 ErrorDecoder.decode()
。默认的实现逻辑中,会根据响应头来判断要不要抛出 RetryableException
。观察源码
感觉怪怪的,这样是否需要重试决定权在被调用方,我觉得还是自己在客户端一方根据响应码去决定要不要重试会更好。所以我们可以实现自己的 ErrorDecoder
。
超时重试
Feign
默认会重试 IOException
,例如最常见的超时,首先我们配置超时时间
只要超过配置时间还未得到响应,当前应用就会抛出
它属于 IOException
。然后下面这行代码
这个方法内部捕捉了 IOException
,将它封装成 RetryException
抛出去触发 Retry
重试。对于 IOException
源码中会将它视为短暂网络抖动异常。
200 一把梭的方案怎么重试
很多公司没有按照规范来,无论接口响应是成功还是失败给的 HTTP 响应都是 200 。然后类似以下格式包装
这种情况无论成功还是失败,Http 的响应都是 200,也就是说它是不会走到 ErrorDecoder.decoder()
方法的,那我们要怎样控制它失败的时候抛出 RetryException
触发重试呢?前面的文章 SpringCloud OpenFeign 自定义响应解码器 。我们已经知道如何自定义 Feign
响应的 Decoder
。我们仿照之前自定义的 Decoder
,解析这个响应体,根据上述结构解析出来的 code
(前提是 JSON
内部结构要存在一个正确的 HTTP.code
)去抛出 RetryException
。部分代码如下:
我本以为触发异常能够让他走到 ErrorDecoder.decode
,但实际上没有,跟踪源码我们发现AsyncResponseHandler.handleResponse()
内部核心代码
很遗憾未能解决......我也懒得再去看怎么解决了,这就是不遵守规范的后果,后面要花费更多的精力来填坑!!!所以,自定义数据响应结构没关系,你可以把业务状态码包在内部结构里面,重要的是你特喵的要用标准的 HTTP 响应码啊!!!
重试雪崩
重试雪崩就是一个连串的微服务调用其中一个节点报错,会导致上游服务触发指数级别的重试次数。
这是个非常严重的问题,假设有一个业务流程非常复杂,其微服务调用流程是 A → B → C → D
。我们都配置了调用失败重试,maxAttempts = 5
。C → D
的过程中失败了,这时候会返回到 C
,触发 C
重试五次,然后五次都失败会返回到 B
,又触发 B
的五次重试,B
的每次重试都会导致 C
重试五次 D
,完了之后 D
已经重试了 25
次,B
五次失败又返回到 A
,触发 A
的五次重试,完了之后 D
总共被调用了 5*5*5 = 125
次 。
一个用户 125
次对于请求压力还不算什么,对于数据库的压力就很大了。我草,随便出个 bug
几百用户量就能瞬间把系统干垮了。。。。可怕吧?
如何防止重试雪崩?如果你的业务场景几乎只存在短链路调用,不会存在跨三个微服务的情况,那就不用考虑这个问题了。但是随着业务扩展,总是会出现长链路的调用场景,后面我们会学习如何防止重试雪崩。
来源:https://juejin.cn/post/7130943346117181476
评论