当前位置:   article > 正文

浅谈SpringCloud 如何使用OpenFeign进行请求重试?_openfeign 报错重试

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 里面看到

  1. @Bean
  2. @ConditionalOnMissingBean
  3. public Retryer feignRetryer() {
  4. return Retryer.NEVER_RETRY; //代表永远不重试
  5. }
  6. 复制代码

所以开启重试我们只需要自己定义一个 RetryBean 即可。

  1. @Bean
  2. public Retryer retryer(){
  3. return new Retryer.Default();
  4. }
  5. 复制代码

我们观察 Retry.Default 类的核心成员变量

  1. private final int maxAttempts; //最大重试次数,初始调用算一次,默认实现值是 5
  2. private final long period; //初始重试间隔 ,默认实现值是 100 ms
  3. private final long maxPeriod; //最大重试间隔 ,默认实现值是 1000 ms
  4. 复制代码

Retryer 重试的原理

这个其实很简单,追踪一下 OpenFeign 调用的源码即可, SynchronousMethodHandler

  1. @Override
  2. public Object invoke(Object[] argv) throws Throwable {
  3. //可以看到 OpenFeign 就是用 RestTemplate 实现远程调用的
  4. RequestTemplate template = buildTemplateFromArgs.create(argv);
  5. Options options = findOptions(argv);
  6. Retryer retryer = this.retryer.clone();
  7. while (true) {
  8. try {
  9. return executeAndDecode(template, options);
  10. } catch (RetryableException e) {
  11. try {
  12. retryer.continueOrPropagate(e);
  13. } catch (RetryableException th) {
  14. //...省略throw
  15. //...省略日志
  16. continue;
  17. }
  18. }
  19. }
  20. }
  21. 复制代码

代码中上来就是一个循环,如果我们调用过程中抛出了 RetryableException,并且 retryer.continueOrPropagate(e) 还没超过重试次数就继续发起请求,如果到了重试次数就抛出异常结束循环。这问题就简单了,也就是说如果我们要控制 OpenFeign 发起重试只需要抛出 RetryableException

实现 ErrorDecoder

前面的文章我们提到了 Feign 的解码器,用于解析正常响应的结果。其实在 Feign 的错误响应结果也有一个专用的解码器。

  1. return executeAndDecode(template, options);
  2. 复制代码

在这行代码中如果出现了异常会调用 ErrorDecoder.decode()。默认的实现逻辑中,会根据响应头来判断要不要抛出 RetryableException。观察源码

  1. @Override
  2. public Exception decode(String methodKey, Response response) {
  3. FeignException exception = errorStatus(methodKey, response);
  4. //根据 response 里面的 header.Retry-After 来决定要不要重试
  5. Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
  6. if (retryAfter != null) {
  7. return new RetryableException(
  8. response.status(),
  9. exception.getMessage(),
  10. response.request().httpMethod(),
  11. exception,
  12. retryAfter,
  13. response.request());
  14. }
  15. return exception;
  16. }
  17. 复制代码

感觉怪怪的,这样是否需要重试决定权在被调用方,我觉得还是自己在客户端一方根据响应码去决定要不要重试会更好。所以我们可以实现自己的 ErrorDecoder

  1. @Configuration
  2. public class FeignErrorDecoder implements ErrorDecoder {
  3. @Override
  4. public Exception decode(String methodKey, Response response) {
  5. if (is4xx(response.status())) {
  6. log.error("请求xxx服务-{},返回:{}", response.status(), response.body());
  7. throw new ClientException(response.status(), "xx");
  8. }
  9. FeignException exception = errorStatus(methodKey, response);
  10. if (response.request().httpMethod() == Request.HttpMethod.GET) { //只对 GET 重试
  11. return new RetryableException(
  12. response.status(),
  13. exception.getMessage(),
  14. response.request().httpMethod(),
  15. exception,
  16. new Date(),
  17. response.request());
  18. }
  19. return exception;
  20. }
  21. }
  22. 复制代码

超时重试

Feign 默认会重试 IOException,例如最常见的超时,首先我们配置超时时间

  1. feign:
  2. client:
  3. config:
  4. default:
  5. connectTimeout: 100 #单位 ms
  6. readTimeout: 100 #单位 ms
  7. 复制代码

只要超过配置时间还未得到响应,当前应用就会抛出

  1. java.net.SocketTimeoutException: Read timed out
  2. 复制代码

它属于 IOException 。然后下面这行代码

  1. return executeAndDecode(template, options);
  2. 复制代码

这个方法内部捕捉了 IOException ,将它封装成 RetryException 抛出去触发 Retry 重试。对于 IOException 源码中会将它视为短暂网络抖动异常。

200 一把梭的方案怎么重试

很多公司没有按照规范来,无论接口响应是成功还是失败给的 HTTP 响应都是 200 。然后类似以下格式包装

  1. HTTP 200 OK
  2. {
  3. "success": true/false,
  4. "code": "0/1",
  5. "message": "success/error",
  6. "data": Object/xxxException
  7. }
  8. 复制代码

这种情况无论成功还是失败,Http 的响应都是 200,也就是说它是不会走到 ErrorDecoder.decoder() 方法的,那我们要怎样控制它失败的时候抛出 RetryException 触发重试呢?我们已经知道如何自定义 Feign 响应的 Decoder。我们仿照之前自定义的 Decoder,解析这个响应体,根据上述结构解析出来的 code (前提是 JSON 内部结构要存在一个正确的 HTTP.code)去抛出 RetryException。部分代码如下:

  1. com.feign.test.config.Response<?> result =
  2. ( com.feign.test.config.Response<?>) this.decoder.decode(response, newType);
  3. log.info("-----{}---",result);
  4. if(result.code == 500){
  5. return new RetryableException(
  6. result.code,
  7. result.message,
  8. response.request().httpMethod(),
  9. new RuntimeException(),
  10. new Date(),
  11. response.request());
  12. }
  13. 复制代码

我本以为触发异常能够让他走到 ErrorDecoder.decode,但实际上没有,跟踪源码我们发现AsyncResponseHandler.handleResponse()内部核心代码

  1. else if (response.status() >= 200 && response.status() < 300) {
  2. if (isVoidType(returnType)) {
  3. resultFuture.complete(null);
  4. } else {
  5. final Object result = decode(response, returnType); // 这里并没有向外抛出 RetryException 有点奇怪
  6. shouldClose = closeAfterDecode;
  7. resultFuture.complete(result);
  8. }
  9. 复制代码

很遗憾未能解决......我也懒得再去看怎么解决了,这就是不遵守规范的后果,后面要花费更多的精力来填坑!!!所以,自定义数据响应结构没关系,你可以把业务状态码包在内部结构里面,重要的是你特喵的要用标准的 HTTP 响应码啊!!!

重试雪崩

重试雪崩就是一个连串的微服务调用其中一个节点报错,会导致上游服务触发指数级别的重试次数。

这是个非常严重的问题,假设有一个业务流程非常复杂,其微服务调用流程是 A → B → C → D。我们都配置了调用失败重试,maxAttempts = 5C → D 的过程中失败了,这时候会返回到 C,触发 C 重试五次,然后五次都失败会返回到 B ,又触发 B 的五次重试,B 的每次重试都会导致 C 重试五次 D,完了之后 D 已经重试了 25 次,B 五次失败又返回到 A,触发 A 的五次重试,完了之后 D 总共被调用了 5*5*5 = 125 次 。

一个用户 125 次对于请求压力还不算什么,对于数据库的压力就很大了。我草,随便出个 bug 几百用户量就能瞬间把系统干垮了。。。。可怕吧?

如何防止重试雪崩?如果你的业务场景几乎只存在短链路调用,不会存在跨三个微服务的情况,那就不用考虑这个问题了。但是随着业务扩展,总是会出现长链路的调用场景,后面我们会学习如何防止重试雪崩。

结语

本篇文章简单介绍了 OpenFeign 的重试相关知识。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/84642
推荐阅读
相关标签
  

闽ICP备14008679号