今天来聊一聊前段时间看到的一个面试题,也是在实际项目中需要考虑的一个问题,Feign的超时时间如何设置?
Feign的超时时间设置方式并不固定,它取决于Feign在项目中是如何使用的,不同的使用方式,超时时间设置方式也不大相同,甚至还可能有坑。
前置知识
由于文章会涉及到Feign的底层知识,如果不懂点Feign的基本概念的话,后面就看不下去了
所以为了方便不了解Feign的小伙伴也能够读得懂文章,这里我就简单地说说Feign的原理,点到为止,虽然不深入,但足够应付这篇文章了
Feign的作用
在项目中,我们经常需要调用第三方提供的Http接口,此时我们就可以使用一些Http框架来实现,比如HttpClient
public class HttpClientDemo {
public static void main(String[] args) throws Exception {
//创建一个HttpClient
HttpClient httpClient = HttpClientBuilder.create().build();
//构建一个get请求
HttpGet httpGet = new HttpGet("http://192.168.100.1:8080/order/1");
//发送请求,获取响应
HttpResponse httpResponse = httpClient.execute(httpGet);
HttpEntity httpEntity = httpResponse.getEntity();
//读出响应值
String response = EntityUtils.toString(httpEntity);
System.out.println("Response: " + response);
}
}
如果项目中只有一两个这种第三方接口这样写还行,但是一旦这种三方接口过多的话,每次都得这样组装参数,发送请求,写一堆同样的代码,就显然很麻烦了。
所以为了简化发送Http请求的开发,减少重复代码,Feign就出现了。
Feign是一个声明式的Http框架
当你需要调用Http接口时,你需要声明一个接口,加一些注解就可以了
而像组装参数、发送Http请求等重复性的工作都交给Feign来完成。
Feign的原理
虽然有了接口,但是仅仅有接口是不够的,因为接口又不能创建对象,我们得需要对象。
Feign为了方便我们为接口创建对象,提供的Feign.Builder
这个内部类
这个类的作用就是解析接口的上的注解,为接口生成一个动态代理对象,后面通过这个代理对象就可以发送请求了。
这个内部类有很多属性,这些属性都是Feign的核心组件。
在这些核心的组件中有一个叫Client
的,上图中我圈出来了。
这个Client
类划个重点,非常非常重要,本文讨论的东西跟他有密切关系。
它只有一个方法Response execute(Request request, Options options)
方法的第一个参数Request
就是封装了http请求的url、请求方法,请求头、请求体之类的参数
第二个参数Options
就是本文的主题,封装了超时时间。
返回值Response
就是封装了一些响应码status、响应头之类的
所以通过方法的参数和返回值也可以猜出来,这个Client
作用是用来组装Http请求参数,发送Http请求的
并且http请求超时时间是根据传给Client
的Options
参数来决定的
Feign单独使用时超时时间设置
Feign本身就是一个http客户端,可独立使用,Feign提供了两种超时时间设置方式
1、通过Feign.Builder设置
前面提到,Feign.Builder
的作用是为接口的动态代理对象的
Feign.Builder
里面有很多属性,其中就有关于超时时间的属性Options
如果你不设置,那么超时时间就是默认的
默认的就是连接超时10s,读超时60s
所以可以通过设置Feign.Builder
中的options
来设置超时时间
来个demo
环境准备,就是一个简单的SpringBoot项目,引入一个Feign的依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
</dependencies>
声明接口 + 注解
public interface UserApi {
@RequestLine("GET /user/{userId}")
User queryUser(@Param("userId") Integer userId);
}
这里演示的是Feign原生的使用方式,脱离于SpringCloud环境,所以Spring的那些@GetMappring就不支持了,改用Feign本身提供的注解
测试代码
public class FeignDemo {
public static void main(String[] args) {
UserApi client = Feign.builder()
//设置连接和读超时间都是5s
.options(new Request.Options(5, TimeUnit.SECONDS, 5, TimeUnit.SECONDS, true))
.target(UserApi.class, "http://localhost:8088");
User user = client.queryUser(123);
}
}
这里面的请求路径都是不存在的,因为我们只关心传给Client
的Options
参数值
Client
在我们不设置的时候,就用默认的实现Client.Default
断点打到execute
方法的实现,运行,走起
结果就是我们设置的5s
2、在接口方法参数设置
除了在通过Feign.Builder时设置之外,Feign还支持在接口的方法参数上设置
此时你只需要在接口的方法上加一个Options
类型的参数
@RequestLine("GET /user/{userId}")
User queryUser(@Param("userId") Integer userId, Request.Options options);
这样在传参数时就可以设置超时时间了
User user = client.queryUser(123, new Request.Options(3, TimeUnit.SECONDS, 3, TimeUnit.SECONDS, true));
同样地,debug就可以看见我们设置的3s了
这两种设置超时时间的主要区别就是方法参数设置超时时间的优先级高于Feign.Builder设置的超时时间
用一张图来总结一下上面的关系
所以,如果你单独使用Feign的时候,你就可以通过如上的两种方式来设置超时时间。
SpringCloud下Feign单独使用超时时间设置
在SpringCloud环境下,只是对Feign进行了一层包装,所以即使没有Ribbon和注册中心,Feign也是可以单独使用的,但是用法有点变化
- 注解都换成SpringMVC的注解
- 接口上需要加@FeignClient注解
- 用@EnableFeignClients扫描这些接口
不过,默认情况下Feign还是需要结合Ribbon来使用的
如果你只想单独使用Feign,那么就设置一下@FeignClient注解的url属性,指定请求的地址和端口就可以了
所以,既然只是包装,前面提到的两种方式设置超时时间当然可以继续使用:
- 通过Feign.Builder
- 通过接口的方法参数
方法参数设置形式跟前面提到的一模一样,但是通过Feign.Builder来设置却不太一样
由于SpringCloud会自己创建Feign.Builder,不需要我们创建,所以在设置Options
时,Spring提供了两种快捷方式来设置
不过最终还是设置到Feign.Builder中
1、声明一个Options Bean
Spring在构建Feign.Builder
的时,会从容器中查找Options
这个Bean,然后设置到Feign.Builder
中
@Configuration
public class FeignConfiguration {
@Bean
public Request.Options options() {
return new Request.Options(8, TimeUnit.SECONDS, 8, TimeUnit.SECONDS, true);
}
}
此时debug就可以看到设置到Feign.Builder
的代码
这段代码在FeignClientFactoryBean中的configureUsingConfiguration方法中
2、配置文件中设置
除了声明Bean之外,Spring还提供了通过配置文件的方式配置,如下:
feign:
client:
config:
default:
connectTimeout: 10000
readTimeout: 10000
同样地,debug就可以看见
这段代码在FeignClientFactoryBean中的configureUsingConfiguration方法中
声明Bean和配置文件都可以设置,那么同时设置哪种优先级高呢?
如无特殊配置,遵守SpringBoot本身的配置规定
约定 > 配置 > 编码
所以基于这个规定,配置文件的配置优先级大于手动声明Bean的优先级。
到这,我们又学到了两种Spring为了方便我们设置Feign.Builder
提供的配置方式:
- 声明Options Bean
- 配置文件
把他们俩加到前面画的图中
所以,如果你使用了SpringCloud提供的方式来使用Feign,那么就可以通过声明Options
Bean和配置文件的方式更加方便地来设置超时时间
最终其实还是通过Feign.Builder
来设置的
SpringCloud下通过Ribbon来设置
当Feign配合Ribbon使用时,除了上面两种方式之外,还可以通过Ribbon来设置超时时间。
但是这里我不知道你会不会好奇
Ribbon不是负载均衡组件,怎么可以设置超时时间?
其实这跟Ribbon的定位有关,除了负载均衡组件之外,Ribbon也干发送Http请求的事,也就是不配合Feign,他照样可以发送http请求。
来个简单demo
解释一下上面的代码意思
- 第一步,设置user服务的两个服务实例地址
- 第二步,获取user服务对应的RestClient,这RestClient就可以用来发送http请求
- 第三步,构建一个http请求
- 第四步,就是发送http请求,以负载均衡的方式
这样,此时就会从两个服务实例中根据负载均衡选取一个服务地址发送http请求,
Ribbon既然可以发送Http请求,那么自然而然就可以设置超时时间
Feign在整合Ribbon的时候,为了统一配置,就默认将自己的超时时间交由Ribbon管理
所以,在默认情况下,Feign的超时时间可以由Ribbon配置
而Ribbon默认连接和读超时时间只有1s,所以在默认情况下,Feign的超时时间只有1s。
IClientConfig是Ribbon的配置类,Ribbon所有的配置都可以从IClientConfig中获取。
所以,在默认情况下,很容易就发生超时,不过我们可以通过配置文件修改即可
ribbon:
ConnectTimeout: 5000
ReadTimeout: 5000
你知道你发现没,上面说通过Ribbon设置Feign的超时时间,一直提到前面一直提到这个词
默认
什么情况下叫默认呢?
所谓的默认,就是当你不主动设置Feign的超时时间的时候,就是默认。
换句话说,一旦你通过上面说的那些配置方式设置Feign的超时时间,就不是默认了
此时通过Ribbon设置的超时时间就不会生效了
Feign是如何在默认情况下将超时时间交给Ribbon管理的?
要想回答这个问题,就得先搬出前面反复提到的Client接口了。
在SpringCloud的环境下,有一个Client的实现,叫LoadBalancerFeignClient
通过名字就可以看出,带有负载均衡的Client实现,负载均衡的实现肯定是交给Ribbon来实现的
所以当Feign配合Ribbon时用的就是这个Client实现
既然实现了Client
接口,那就看看execute
方法的实现逻辑
图中getClientConfig
方法就是判断使用Feign或者Ribbon配置的核心逻辑
核心的判断逻辑就是这一行
options == DEFAULT_OPTIONS
DEFAULT_OPTIONS
就是一个超时时间的常量
当上述判断条件成立时,就会通过this.clientFactory.getClientConfig(clientName)
获取到Ribbon配置
由于这是Ribbon的逻辑,这里就不深扒了,知道是这个意思就行
当条件不成立时,用Options
构建一个FeignOptionsClientConfig
FeignOptionsClientConfig
就是简单地将Options
配置读出来,设置到父类DefaultClientConfigImpl
超时时间配置上
DefaultClientConfigImpl
就算你不知道是什么也无所谓,你能看出的一件事就是,超时时间用的是传递给Client
的Options
参数
所以,综上,我们的问题就变得非常easy了,那就是什么时候
options == DEFAULT_OPTIONS
只有当这个条件成立时,才使用Ribbon的配置。
这里我们先来捋一捋前面提到的东西
前面我们反复提到,Client
的Options
最终只来自于两种配置
- Feign.Builder
- 方法参数
所以DEFAULT_OPTIONS
这个Options
一定是通过上面两种方法中的其中一种设置的
而方法参数是不可能设置的成DEFAULT_OPTIONS
因为这是我们控制的,只要我们参数不传DEFAULT_OPTIONS
,那么永远都不可能是DEFAULT_OPTIONS
。
此时只剩下一种情况,那就是Spring在构建在Feign.Builder的时候,设置成DEFAULT_OPTIONS
。
通过查找DEFAULT_OPTIONS
的使用,我们可以追踪到这么一段代码
这不就是前面提到的通过声明Bean的方式来设置超时时间
不同的是它加了@ConditionalOnMissingBean
,这个注解就是说,一旦我们自己没有声明Options
,就用他这个Options
到这终于真像大白了。
我们不设置超时时间,Spring就会给Feign.Builder加一个DEFAULT_OPTIONS
这个Options
在执行的时候,发现是DEFAULT_OPTIONS
,说明我们没有主动设置过超是时间,就会使用Ribbon的超时时间。
为了方便理清上面的逻辑,这里整一张图
虽然Feign可以使用Ribbon的超时时间,但是Ribbon的配置的优先级是最最低的
方法参数 > Feign配置文件 > 声明Options > Ribbon配置
Feign or Ribbon配置用哪个好?
其实我个人更倾向于使用Ribbon的配置方式。
因为Ribbon除了可以设置超时时间之外,还可以配置重试机制、负载均衡等其它的配置
为了简化和统一管理配置,使用Ribbon来配置超时时间。
可能你会有疑问,Feign也支持重试机制,为什么不选择Feign?
这是因为Feign重试机制没有Ribbon的好
Ribbon重试的时候会换一个服务实例来重试,因为原来出错的可能不可用
而Feign并不会换一个服务实例重试,他并不知道上一次使用的是哪个服务实例,这就导致可能会出现在一个不可用的服务实例上多次重试的情况。
引入Hystrix时超时时间设置
如果你之前的确没有研究过关于Feign超时时间的配置关系,那么此时你应该有所收获了。
但是这就结束了么?
不,事情没那么简单。
如果你的项目中使用了Hystrix,那么就得小心前面说的那些配置了。
由于Hystrix跟Feign毕竟是一家人,所以当引入Hystrix时,Feign就跟之前不一样了。
Hystrix会去干一件事,那就是给每个Feign的http接口保护起来,毕竟Hystrix就是干保镖这个事的。
但是这没保护还好,一保护问题就不自觉地出现了。
Hystrix在保护的时候,一旦发现被保护的接口执行的时间超过Hystrix设置的最大时间,就直接进行降级操作。
怎么降级的,这里咱不关心,咱关心的是这个Hystrix超时的最大值是多少。
因为一旦这个时间小于Feign的超时时间,那么就会出现Http接口正在执行,也没有异常,仅仅是因为执行时间长,就被降级了。
而Hystrix的默认的超时时间的最大值就只有1s。
所以就算你Feign超时时间设置的再大,超过1s就算超时,然后被降级,太坑了。。
所以我们需要修改这个默认的超时时间的最大值,具体的配置项如下
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 30000
并且时间上大致要符合下面这个原则
Hystrix超时时间 >= (连接超时时间 + 读超时时间) * 重试次数
重试次数我们前面也提到了,虽然一般我们不设置,但是为了严谨还是得加上,因为一次Http接口的执行时间肯定跟重试次数有关,重试次数越多,时间就越长。
而连接超时时间 + 读超时时间设置方式,前面提到很多次,不论是通过Feign本身设置还是通过Ribbon来设置,都是可以的
总结
今天给大家扒了扒在不同使用条件下Feign的超时时间设置,总结起来大致如下:
- 单独使用Feign时:通过
Feign.Builder
和方法参数 - SpringCloud环境下单独使用Feign:方法参数、配置文件、声明
Options
Bean - 跟Ribbon配合使用:通过Ribbon的超时参数设置
- 跟Hystrix配合使用:修改默认的超时时间,尽量符合 Hystrix超时时间 >= (连接超时时间 + 读超时时间) * 重试次数