电子说
今天来聊一聊前段时间看到的一个面试题,也是在实际项目中需要考虑的一个问题,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 的依赖。
这里演示的是 Feign 原生的使用方式,脱离于 SpringCloud 环境,所以 Spring 的那些 @GetMappring 就不支持了,改用 Feign 本身提供的注解。
测试代码
这里面的请求路径都是不存在的,因为我们只关心传给 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 还提供了通过配置文件的方式配置,如下:
同样地,debug 就可以看见。
这段代码在 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,那么就可以通过声明 OptionsBean 和配置文件的方式更加方便地来设置超时时间。
最终其实还是通过 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) (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 超时时间 >= (连接超时时间 + 读超时时间) * 重试次数。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !