SpringCloud OpenFeign 请求重试

2023-11-09

前言

真实的微服务业务场景中,可能出现跨服务调用失败的情况。最常见的就是被调用的服务正在发布,由于微服务之间通常有依赖关系,发布有一定的先后顺序,对于一个微服务应用常见的发布策略有两种

  • 先停掉集群中一半的实例,然后重新启动这些应用,完成之后再停掉另一半的集群实例重新启动。
  • 一台实例一台实例重启

那么此时被停掉的应用会处于临时的不可用,但是下线的信息还没有被同步到注册中心,导致 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 里面看到

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

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

@Bean
public Retryer retryer(){
    return new Retryer.Default();
}
复制代码

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

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

Retryer 重试的原理

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

@Override
public Object invoke(Object[] argv) throws Throwable {
  //可以看到 OpenFeign 就是用 RestTemplate 实现远程调用的
  RequestTemplate template = buildTemplateFromArgs.create(argv);
  Options options = findOptions(argv);
  Retryer retryer = this.retryer.clone();
  while (true) {
    try {
      return executeAndDecode(template, options);
    } catch (RetryableException e) {
      try {
        retryer.continueOrPropagate(e);
      } catch (RetryableException th) {
        //...省略throw
        //...省略日志
        continue;
      }
    }
  }
}
复制代码

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

实现 ErrorDecoder

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

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

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

@Override
public Exception decode(String methodKey, Response response) {
  FeignException exception = errorStatus(methodKey, response);
  //根据 response 里面的 header.Retry-After 来决定要不要重试
  Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
  if (retryAfter != null) {
    return new RetryableException(
        response.status(),
        exception.getMessage(),
        response.request().httpMethod(),
        exception,
        retryAfter,
        response.request());
  }
  return exception;
}
复制代码

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

@Configuration
public class FeignErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        if (is4xx(response.status())) {
            log.error("请求xxx服务-{},返回:{}", response.status(), response.body());
            throw new ClientException(response.status(), "xx");
        }
        FeignException exception = errorStatus(methodKey, response);
        if (response.request().httpMethod() == Request.HttpMethod.GET) { //只对 GET 重试
            return new RetryableException(
                    response.status(),
                    exception.getMessage(),
                    response.request().httpMethod(),
                    exception,
                    new Date(),
                    response.request());
        }
        return exception;
    }
}
复制代码

超时重试

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

feign:
  client:
    config:
      default:
        connectTimeout: 100 #单位 ms
        readTimeout: 100    #单位 ms
复制代码

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

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

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

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

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

200 一把梭的方案怎么重试

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

HTTP 200 OK
{
    "success": true/false,
    "code": "0/1",
    "message": "success/error",
    "data": Object/xxxException
}
复制代码

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

com.feign.test.config.Response<?> result =
    ( com.feign.test.config.Response<?>) this.decoder.decode(response, newType);
log.info("-----{}---",result);
if(result.code == 500){
    return new RetryableException(
            result.code,
            result.message,
            response.request().httpMethod(),
            new RuntimeException(),
            new Date(),
            response.request());
}
复制代码

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

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

很遗憾未能解决......我也懒得再去看怎么解决了,这就是不遵守规范的后果,后面要花费更多的精力来填坑!!!所以,自定义数据响应结构没关系,你可以把业务状态码包在内部结构里面,重要的是你特喵的要用标准的 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 几百用户量就能瞬间把系统干垮了。。。。可怕吧?

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

结语

本篇文章简单介绍了 OpenFeign 的重试相关知识,下篇文章会介绍新一代负载均衡器 SpringCloud LoadBalancer

如果这篇文章对你有帮助,记得点赞加关注!你的支持就是我继续创作的动力!

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

SpringCloud OpenFeign 请求重试 的相关文章

  • 合并两个 Jasper 报告

    我有一个带有下拉菜单的网络应用程序 用户可以从中选择报告可视化的类型 报告 1 报告 2 报告 3 等 根据所选的报告 Jasper 报告将在服务器上编译并以 PDF 格式的弹出窗口打开 在服务器端 我使用下面的代码以单独的方法实现每个报告
  • 什么 RoundingMode 常量 100% 与 Math.round 一样工作?

    我的意思是有没有舍入模式 https docs oracle com javase 8 docs api java math RoundingMode html常数准确地描述了什么Math round arg 做 据我所知 Math rou
  • 颜色资源 ID 返回错误值

    我试图在 onCreate 期间以编程方式从颜色资源设置文本颜色 但得到了一些奇怪的结果 我在 res colors xml 中定义了一个颜色资源
  • 使用 Netbeans 导出 JAR

    如何使用Netbeans将java项目导出到JAR 我找不到像 Eclipse 那样的选项 您需要启用该选项 Project Properties gt Build gt Packaging gt Build JAR after compi
  • 如何在 Java 中通过 TLS v.1.2 创建安全的 TCP 连接

    我想通过 TLS v1 2 在两个系统之间创建通信 其中包含的信息是保密的 我想避免 https Web 服务调用 而直接想在 TCP 层执行消息交换 您能否建议如何实现此功能 以便我可以通过 TLS v1 2 安全地传输数据 EDIT 阅
  • 计算两个 Java 日期实例之间的差异

    我正在使用Java的java util DateScala 中的类并想要比较Date对象和当前时间 我知道我可以使用 getTime 计算增量 new java util Date getTime oldDate getTime 然而 这只
  • 负字符值JAVA

    为什么会出现以下情况 char p 0 p System out println p result 65535 为什么不给出编译错误或运行时异常 我预计它是因为字符不能为负数 相反 它从颠倒开始倒数 提前致谢 为什么不给出编译错误或运行时异
  • Axis2 不返回自己的对象

    我编写了一些网络服务 其中一些返回一个简单的字符串 这个可以工作 另一些返回一个对象列表 不 axis2 1 5 1 无法处理集合类型 所以我将返回类型更改为Object 但我仍然收到这个异常 ERROR java lang Runtime
  • Java:getInstance 与静态

    目的是什么getInstance 在Java中 在我的研究过程中我一直在读getInstance 有助于实现单例设计模式 根据我的理解 这意味着整个程序中只有一个实例 但我不能只使用静态吗 这不是静态的全部意义吗 如果我只有静态方法和字段
  • 基本的 Swing 库? JGoodies,JFreeChart [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • 如何模拟类路径上属性文件的存在?

    这肯定是一个常见问题 我有一个像这样的属性文件my settings properties由应用程序类读取 当我编写测试类时 它需要测试可能存在的不同场景my settings properties为了确保最大的代码覆盖率 例如空属性文件
  • 通过反思思考工厂设计模式

    我正在对工厂模式进行研发 我开发了下面的代码 现在我知道子类是 Dog 和 Cat 但是如果我想通过在 main 中传递类名来通过反射实现同样的事情 请告诉我该怎么做 爪哇 public abstract class Animal publ
  • Android 折叠工具栏在折叠时没有隐藏其他元素

    我在 Android 上有一个布局 支持设计折叠工具栏 其中包含 TextView 但是当我折叠工具栏时 一些带有工具栏标题的 TextView 我想隐藏所有其他内容 而不是工具栏和标题 这是我的布局
  • 如何用 JSON 表示数据库中的图像

    我需要基于数据库中的 blob 创建 JSON 为了获取 blob 图像 我使用下面的代码并在 json 数组中显示之后 Statement s connection createStatement ResultSet r s execut
  • 单击按钮后更改 JPanel

    我正在为我的应用程序构建简单的 GUI 我有几个 JPanel 我想根据单击 JButton 执行的操作来显示它们 如何禁用一个 JPanel 并启用另一个 JPanel 几个细节 我有一个 JFrame 课程 我正在其中构建启动 gui
  • 如何在 spring-ws 中解析 SoapFaultClientException

    我正在使用 spring ws 2 3 1 在为 Web 服务创建客户端时 有时我得到SoapFaultClientException像下面这样
  • 从邮件服务器读取发送的邮件

    我知道如何从 INBOX 文件夹中检索邮件 但现在我想从 SENT ITEMS 文件夹中检索邮件 我正在使用 imap 检索数据 让我知道我应该在此函数中传递什么参数才能从 SENT ITEMS 文件夹中获取邮件Folder folder
  • 无法在 Windows 7 64 位中正确设置 java 路径

    我已经在计算机中安装了 java 1 6 并且还使用高级变量设置设置了路径 当我尝试执行 java 时 在命令提示符下出现此错误 Error could not open C Program Files Java jre7 lib amd6
  • 根据 netbeans 中的单选按钮切换组件的“启用”属性

    我在按钮组中有两个单选按钮 在同一面板中我有一个文本框和一个按钮 我想仅在选择第二个按钮时启用文本框和按钮 并在选择另一个单选按钮时禁用文本框和按钮 我已经尝试过这个但没有成功 private void radio button2Actio
  • @Transactional 方法调用另一个没有 @Transactional 注解的方法?

    我在 Service 类中看到了一个方法 该方法被标记为 Transactional 但它还调用同一类中的一些其他方法 这些方法未标记为 Transactional 这是否意味着对单独方法的调用导致应用程序打开与数据库的单独连接或挂起父事务

随机推荐

  • jmap、jstat、jinfo、jstack命令详解

    jmap jmap histo pid gt log txt 此命令可以用来查看内存信息 实例个数以及占用内存大小 num 序号 instances 实例数量 bytes 占用空间大小 class name 类名称 C is a char
  • Windows中.exe程序的启动过程和C/C++运行时库<转载>

    很是受益 Windows中 exe程序的启动过程和C C 运行时库 lt 转载 gt Windows系统中 exe后缀的文件一般可以双击运行 编程时 编译出来的最终结果一般也表现为一个exe程序和其他的为程序执行提供支持的dll 我们双击一
  • Unity3d-简单AR游戏

    Unity3d 简单AR游戏 一 图片识别与建模 Vufria模块的导入 首先是安装Vuforia 模块 2017版本后的可以直接使用Unity Hub安装 安装完成后可以直接在软件中使用 然后在菜单目录的GameObject gt Vuf
  • 利用docx4j word转pdf

    依赖
  • 不能导入当前目录下的py模块,不能导入自己写的包

    遇到一个很奇怪的问题 在jupyter里面 明明这个包就在当前目录下就是不能倒入 后来 发现os getcwd 返回的也不是当前文件所在目录 真是奇哉怪也 然后我在终端cd进去我要运行代码的目录 然后在 jupoyter notebook
  • 【计算机考研】从二本到浙大

    报名志愿 浙大 计算机科学与技术学院 软件工程专业 初试成绩 分数不高 大佬轻喷 以下学习方法仅供参考 小tip 放在前头 1 不要照搬别人的学习方案 马克思主义要中国化 学习也要个人化 学习是很私人的事情 一定要找到最适合自己的学习作息和
  • 关于IDEA中tomcat启动控制台乱码(server Tomcat Localhost Log Tomcat Catalina Log乱码)问题

    之前在网上查了好多 但好多都是乱改一通 没有实际效果 经过自己的几次试验后 终于找到了原因 希望可以帮助大家解决问题 少走些弯路 具体解释如下 在这之前说下 tomcat安装目录中 conf文件夹中的logging properties文件
  • linux vim配置

    vimrc config vim 配置 没有vimrc就之间创建新的 vi vimrc set nu 设置显示行号 set tabstop 4 shiftwidth 4 softtabstop 4 tab 等于四个空格 set expand
  • org.dom4j.DocumentException: null Nested exception: null解决

    org dom4j DocumentException null Nested exception null at org dom4j io SAXReader read SAXReader java 484 at org dom4j io
  • 【SpringCloud实战开发总结】

    Vue开发总结 1 Vue 开启Watch监听 2 on blur 3 disabled 4 InputNumber标签中的 max和 min 5 Select标签用于模糊查询 6 强制渲染的三种方法 7 增加下拉框宽度 8 vue前端校验
  • DHT11温湿度传感器编程详解

    一 DHT11介绍 DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器 采用专用的数字模块采集技术和温湿度传感技术 无需复杂的电路处理 传感器包括一个电阻式感湿元件和一个NTC测温元件 并与一个高性能8位单片机相连接
  • D3D初学入门一(配置开发环境及绘制D3D窗口)

    最近一直接触的都是C 的东东 好久没写C 代码了 怕手生忘记了 打算写写C 的代码 写什么好呢 想来想去 以前的工作学过接触了些OpenGL 那我就学习一下D3D吧 原以为D3D的中文入门资料会很多的 结果找了半天也没找到合适的 哎 随便将
  • struts2+hibernate+spring配置详解

    struts2 hibernate spring配置详解 struts2 hibernate spring配置详解 哎 当初一个人做好难 现在终于弄好了 希望自学这个的能少走些弯路 以下是自己配置的案例 注意 要想明白的比较好 请下载这个配
  • java字符串是否相等的三种判断方法

    1 比较的是否是同一对象 eg String str1 abc str2 abc if str1 str2 结果为true 因为在java中字符串的值是不可改变的 相同的字符串在内存中只会存 一份 所以a和b指向的是同一个对象 eg Str
  • 14.Netty框架的C#实战使用

    文章目录 博客概述 场景与实现 C 服务器端实现 C 客户端的写法 博客概述 我是java线的工程师 但是技术栈有点全栈的意思 在某次项目中需要用到c 的socket通讯 查询之后惊喜的发现c 也有netty框架 dotnetty gith
  • window10下libpng编译

    系列文章目录 文章目录 系列文章目录 前言 一 问题原因 二 使用步骤 1 引入zlib库 2 configure 3 Grnerate 前言 libpng编译出错 Selecting Windows SDK version 10 0 19
  • Basic Level 1081 检查密码 (15分)

    题目 本题要求你帮助某网站的用户注册模块写一个密码合法性检查的小功能 该网站要求用户设置的密码必须由不少于6个字符组成 并且只能有英文字母 数字和小数点 还必须既有字母也有数字 输入格式 输入第一行给出一个正整数 N 100 随后 N 行
  • Hyper-V虚拟机网络设置

    Hyper V是基于64位系统 在Windows10自带的虚拟化工具 1 Hyper V的启动和管理 控制面板 gt 程序 gt 打开或关闭Windows功能中启用Hyper V 第一次设置后需要重启电脑 2 打开Hyper V管理器 3
  • C语言数据结构循环双链表

    include
  • SpringCloud OpenFeign 请求重试

    前言 真实的微服务业务场景中 可能出现跨服务调用失败的情况 最常见的就是被调用的服务正在发布 由于微服务之间通常有依赖关系 发布有一定的先后顺序 对于一个微服务应用常见的发布策略有两种 先停掉集群中一半的实例 然后重新启动这些应用 完成之后