OpenFeign 的超时重试机制以及底层实现原理
OpenFeign 的超时重试机制以及底层实现原理
目录
1. 什么是 OpenFeign?
OpenFeign 是一款基于 Feign 的声明式的 Web 服务客户端,它使得编写Web服务客户端变得更加容易。它可以帮助你轻松调用远程服务的工具。
【举个例子】
想象一下你在使用手机的微信和朋友聊天,你只需要知道朋友的微信号就可以给他发消息,不需要知道他的手机号或者其他复杂的信息。OpenFeign 也是这样,它允许你在编写代码时,只需要知道你想调用的服务的名字和需要交互的部分(比如服务中的某个功能或接口),你不需要处理底层的网络连接或者复杂的HTTP请求过程,这些都由OpenFeign自动帮你完成。它提供了一种类似于调用本地方法的感觉来访问远程服务,从而让开发者可以专注于编写业务代码,而不是底层的网络通信细节。
2. OpenFeign 的功能升级
OpenFeign 在 Feign 的基础上还提供了增强、扩展功能:
更好的集成SpringCloud组件:
- OpenFeign与Spring Cloud的其他组件(如服务发现、负载均衡)紧密集成,它能够自动利用服务发现和负载均衡的功能,无需额外配置。
支持
@FeignClient
注解:
- OpenFeign引入了
@FeignClient
注解,使得声明式的客户端创建变得简单。你只需要在接口上使用@FeignClient
指定服务名即可,而无需创建具体的RestTemplate或者使用URL硬编码远程服务调用。错误处理改进:
- OpenFeign提供了更为人性化的错误处理方式。它允许你通过自定义错误解码器来对特定的错误响应进行处理。这样你可以捕捉并处理远程服务调用中的异常,使得异常管理更加灵活和精确。(报错信息也更加细化、明确了)
更丰富的配置项:
- OpenFeign提供了比原生Feign更为详尽的配置项,比如超时设置、重试策略。
3. OpenFeign 内置的超时重试机制
在微服务架构中,服务之间是通过网络进行通信的,而网络又是非常复杂和不稳定的,所以在服务调用的过程中可能会失败或超时,那么在这种情况下,OpenFeign 就需要超时重试机制来解决了。
什么是超时重试 ?
当你的服务请求因为网络问题或服务延迟等原因没能在预定时间内得到响应,超市重试机制会帮你自动重新发送请求。就像你用浏览器打开网页,如果一时加载不出来,你可能会点击刷新重试。OpenFeign 的超时重试就是自动帮你做这件事,尝试几次后如果还是不行,就会告诉你请求失败。这样可以提高服务的可靠性,防止因为网络不稳定、服务不可用、响应延迟等不确定性因素导致服务不可用。
OpenFeign 默认情况下是不会自动开启超时重试机制的,所以想要使用超时重试功能,需要手动配置:
- 配置超时重试
- 覆盖 Retryer 对象
后续的操作都是基于上篇博客中的案例进行演示,在 SpringBoot 整合 Nacos 的案例中,已经涵盖了 OpenFeign 的基础使用了,不会的可以先去看看:https://blog.csdn.net/xaiobit_hl/article/details/134142521
3.1 配置超时重试
spring:
application:
name: nacos-consumer-demo
cloud:
nacos:
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
register-enabled: false #消费者(不需要将此服务注册到 nacos)
openfeign:
client:
config:
default: # 全局配置
connect-timeout: 1000 # 连接超时时间(毫秒)
read-timeout: 1000 # 读取的超时时间(毫秒)
3.2 覆盖 Retryer 对象
@Configuration // 存储 Ioc
public class RetryerConfig {
@Bean
public Retryer retryer() {
return new Retryer.Default(
1000, // 重试间隔时间
1000, // 最大重试间隔时间
3 // 最大重试次数
);
}
}
PS:设置的最大重试次数为 3 次,最大重试间隔时间为 1s,重试间隔时间是 1s。
修改服务提供者代码 >>
配置信息里边设置的读取超时时间为 1 秒,就是为了触发超时重试机制,按道理来说,这个时候我们的消费者就需要在接口里边 sleep 1秒,为了更好的看见问题,可以设置 1.5 秒:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private ServletWebServerApplicationContext context;
// 服务
@RequestMapping("/getnamebyid")
public String getNameById(Integer id) throws InterruptedException {
System.out.println("------- do provider getNameById method" +
LocalDateTime.now());
Thread.sleep(1500); // 休眠 1.5 s
return "provider-name-" + id +
" | port:" + context.getWebServer().getPort();
}
}
PS:① 启动一个临时服务实例,② 保护阈值设为 0 ,③ 启动消费者。
如果有多个实例,客户端默认的负载均衡是轮询,会导致现象并不明显; 如果有保护阈值,也会导致现象更复杂,所以搞一个服务实例,保护阈值不要去设置。
使用 http://localhost:8080/getnamebyid?id=2 获取服务:
此时服务已经获取不到了, 并且已经触发超时重试机制了,打开服务提供者的控制台:
总共打印了三次日志,因为我们设置的最大重试次数就是 3。
为什么不是 4 次呢?为什么日志的时间间隔是 2s 打印一次呢 ?
① 为什么不是打印 4 次 ?
因为 Retryer 的 Default 方法源码中重试次数变量 attempt 是从 1 开始的,然后核心方法 continueOrPropagate 中的 if 判断是当 attemp >= maxAttempts 时,才抛出异常。
下标从 1 开始 :
if 判断:
② 为什么日志的时间间隔是 2s 打印一次呢 ?
我们设置的连接超时时间是 1s,此处肯定是能连的上,所以跟 connect-timeout 没关系。
此处是因为我们设置的读取超时时间(read-timeout)是 1s,并且我们设置了一个间隔时间,也是 1s,所以每次打印日志的间隔时间就是 2s。
4. 自定义超时重试机制
4.1 为什么需要自定义超时重试机制
因为 OPenFeign 内置的超时重试机制,它的重试策略是固定次数的重试,而这种策略在某些场景下效果并不理想。例如我们设置的重试次数为 3,此时因为网络短暂抖动造成了服务调用失败,而固定策略可能在网络恢复前就已经用完了所有的重试次数,这样就导致重试机制的作用不大。有时候我们更需要的是指数增长的重试策略,就像 TCP 的超时重传一样,那种指数增长的重试策略更加的智能,它会在每次重试失败后增加等待时间,给网络或者服务更多的恢复时间,并减少因短时间内多次重试对服务的潜在影响。
4.2 如何自定义超时重试机制
- 自定义超时重试类(实现 Retryer 接口,并重写 continueOrPropagate 方法)
- 设置配置文件
① 自定义超时重试类(指数增长)
/**
* 自定义超时重试类
*/
public class CustomRetryer implements Retryer {
private final int maxAttempts; // 最大尝试次数
private final long backoff; // 重试间隔时间
int attempt; // 当前重试次数
public CustomRetryer() {
this.maxAttempts = 3;
this.backoff = 1000L;
this.attempt = 0;
}
@Override
public void continueOrPropagate(RetryableException e) {
if(attempt++ >= maxAttempts) {
throw e;
}
long interval = this.backoff;
System.out.println(LocalDateTime.now() + " | 执行一次重试: " + interval);
try {
Thread.sleep(interval * attempt); // 指数增长
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}
@Override
public Retryer clone() {
return new CustomRetryer();
}
}
② 设置配置文件
retryer 后面写上自定义超时重试类的包名+ 类名。
启动服务实例和消费者,并尝试获取服务,报错后,再查看控制台:
1. 此处我设置了最大重传次数为 3 打印 4 次日志,是因为我自定义的重试类的 attempt 变量从 0 开始的。
2. 观察日志与日志间的时间间隔:从 2s -> 3s -> 4s,最初 attempt 为 1,1*1 + read-timeout 的 1s 所以就是 2s,然后再试 1*2 + read-timeout,以此类推...
5. OpenFeign 超时重试的底层原理
想要搞懂 OpenFeign 超时重试的底层原理,就得先搞清楚 OpenFeign 的底层实现:
① 加注解
在启动类或者配置类上添加 @EnableFeignClients
注解
② 动态代理
这个注解会触发Spring框架的自动配置机制,扫描所有标记有@FeignClient
的接口,并为它们创建代理实例
③ RequestTemplate 发送HTTP请求
此处的 RequeustTemplate 我们可以理解为 RestTemplate,因为他俩的目的相同。OpenFeign 不能直接发送 HTTP 请求,它在动态代理里面做了一件事,它将注解里面请求的路由地址拿出来,然后就能拼出来一个 URL 请求的地址,然后再使用 RequestTemplate(RestTemplate)去发送 HTTP 请求。
④ RestTemplate 依靠 HTTP 框架实现 web 请求 (把它理解为 RestTemplate)
RestTemplate 只是一个模板方法类,它只是规定了一个调用的 API,它底层并没有实现,它依靠的是 HTTP 框架实现的 web 请求 (阿帕奇的 HttpClient 框架)
5.1 超时重试原理
所以我们的超时重试原理,就是在 HttpClient 里边设置超时时间,然后如果超时了,还没获取到服务列表,报错信息就会一步一步返回给上层,最终给到 OpenFeign。
想要在了解的细致一些,就可以去看看 OpenFeign 的底层源码:
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = this.buildTemplateFromArgs.create(argv);
Request.Options options = this.findOptions(argv);
Retryer retryer = this.retryer.clone();
// 死循环,如果成功或者重试结束就返回 [通过throw终止while循环]
while(true) {
try {
// 通过 Http Client 发起通信
return this.executeAndDecode(template, options);
} catch (RetryableException var9) {
RetryableException e = var9;
try {
// 判断是否重试
retryer.continueOrPropagate(e);
} catch (RetryableException var8) {
Throwable cause = var8.getCause();
if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP && cause != null) {
throw cause;
}
throw var8;
}
if (this.logLevel != Logger.Level.NONE) {
this.logger.logRetry(this.metadata.configKey(), this.logLevel);
}
}
}
}
源码中使用 RequestTemplate 发送 HTTP 请求,我们主要关注两个地方:
- this.executeAndDecode(template, options); // HttpClient 进行通信
- retryer.continueOrPropagate(e); // 重试机制(默认 / 自定义)
所以,OpenFeign 的重试机制是通过其内置的 Retryer 组件和底层的 HTTP 客户端实现的。
Retryer 组件提供了重试策略的逻辑实现,而远程接口则通过 HTTP 客户端来完成调用!
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)