前言
在之前的文档中,介绍Ribbion
原理及基本使用,接下来介绍下其他的一些配置使用。
官网案例
在官网入门案例中,有一个客户端的配置文件,这里面就包含了Ribbion 的常用配置项。
# 同一服务上的最大重试次数(不包括第一次重试))
sample-client.ribbon.MaxAutoRetries=1
# 要重试的下一台服务的最大数量(不包括第一台服务)
sample-client.ribbon.MaxAutoRetriesNextServer=1
# 是否可以重试此客户端的所有操作
sample-client.ribbon.OkToRetryOnAllOperations=true
# 刷新服务列表的时间间隔
sample-client.ribbon.ServerListRefreshInterval=2000
# Http 客户端连接超时时间
sample-client.ribbon.ConnectTimeout=3000
# Http 客户端读取超时时间
sample-client.ribbon.ReadTimeout=3000
# 服务初始列表
sample-client.ribbon.listOfServers=www.microsoft.com:80,www.yahoo.com:80,www.google.com:80
客户端配置的格式为:
<clientName>.<nameSpace>.<propertyName>=<value>
各项说明如下:
- clientName(客户端名称) :也就是对应@FeignClient注解中的名称,Feign 会使用这个名称来标识每一个Http客户端。
- nameSpace (命名空间)是可配置的,默认情况下是“ribbon”
- propertyName(属性名): 所有的配置属性可以在CommonClientConfigKey类中查看
- value(值):配置属性对应的值
如果配置了clientName
,则表示这是一个局部配置,只作用于当前客户端,如果没有配置clientName
,则适用于所有客户端的属性(也就是全局配置)。
例如以下配置表示,为所有客户端设置默认的 ReadTimeout 属性。
ribbon.ReadTimeout=1000
连接超时和读取超时配置
在配置中,有一个ConnectTimeout
、ReadTimeout
,这是在发送请求时的基础配置,特别重要,所以接下来分析下这两个具体是干嘛的,源码是怎么处理的。
参数说明
ConnectTimeout
连接超时时间,Feign
是基于HTTP 的远程调用,众所周知,HTTP 请求会进行TCP的三次握手,这个连接超时时间,就是多少秒没连接上,就会抛出超时异常。
ReadTimeout
读取超时时间,HTTP成功连接后,客户端发会送请求报文,服务端收到后解析并返回响应报文,在写出响应报文时,如果超过了设置的时间还没写完,也会抛出超时异常。在某些接口请求数据量大的时候,很容易出现读取超时,所以要格外注意这个问题。
可以在RibbonClientConfiguration
配置类中看到,客户端超时配置默认都是1秒,所以不自己改配置的话,很容易造成超时问题。
配置案例
在订单服务中,让线程睡眠十秒才返回响应。
访问账户服务,发现1秒左右就马上抛出超时异常了。
Feign 支持在ribbon
或者feign
配置项下配置,feign 下配置优先级最高,而且最新版已经移除了ribbon,所以推荐配置在feign
中。
1、在ribbon 中配置
在ribbon
命名空间下添加配置,将会作用于所有客户端。
ribbon:
ConnectTimeout: 5000
ReadTimeout: 12000
可以为某个单独的客户端配置不同的超时配置,配置前缀为客户端名称。
order-service:
ribbon:
ConnectTimeout: 6000
ReadTimeout: 13000
2、在feign 中配置
也可以在feign 下配置,default 表示作用于所有客户端,也可替换default 为客户端名称,表示作用于单个客户端。
feign:
okhttp:
enabled: true
client:
config:
default:
ConnectTimeout: 6000
ReadTimeout: 13000
源码分析
1. 启动项目
那么这些参数是怎么加载,最后作用到哪里了呢,接下来以Feign 下配置超时时间,分析下源码。
Feign
通过接口生成代理对象,扫描到Feign 接口,构建代理对象,在Feign.builder()
创建构建者时,会完成客户端的初始化配置。在这个时候会创建一个Options
对象。
public Builder() {
// 日志级别
this.logLevel = Level.NONE;
this.contract = new Default();
this.client = new feign.Client.Default((SSLSocketFactory)null, (HostnameVerifier)null);
this.retryer = new feign.Retryer.Default();
this.logger = new NoOpLogger();
// 编码解码器
this.encoder = new feign.codec.Encoder.Default();
this.decoder = new feign.codec.Decoder.Default();
this.queryMapEncoder = new FieldQueryMapEncoder();
this.errorDecoder = new feign.codec.ErrorDecoder.Default();
//
this.options = new Options();
this.invocationHandlerFactory = new feign.InvocationHandlerFactory.Default();
this.closeAfterDecode = true;
this.propagationPolicy = ExceptionPropagationPolicy.NONE;
this.forceDecoding = false;
this.capabilities = new ArrayList();
}
Options
对象封装了超时时间,构造方法初始化的超时时间分别为10S、60S,这是Feign 原生框架的配置,但是会被覆盖。
public Options() {
this(10L, TimeUnit.SECONDS, 60L, TimeUnit.SECONDS, true);
}
代理对象生成时,会初始化方法处理器,这里又会为每个方法设置Options
对象,这里的Options
就是加载我们配置的超时参数了。
private SynchronousMethodHandler(Target<?> target, Client client, Retryer retryer, List<RequestInterceptor> requestInterceptors, Logger logger, Level logLevel, MethodMetadata metadata, feign.RequestTemplate.Factory buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder, boolean decode404, boolean closeAfterDecode, ExceptionPropagationPolicy propagationPolicy, boolean forceDecoding) {
this.target = (Target)Util.checkNotNull(target, "target", new Object[0]);
this.client = (Client)Util.checkNotNull(client, "client for %s", new Object[]{
target});
// 省略.....
this.options = (Options)Util.checkNotNull(options, "options for %s", new Object[]{
target});
// 省略.....
}
}
2. 执行流程
之前我们分析过,客户端的上下文及配置,是在其第一次访问时才会进行加载。
Feign 接口方法执行时,实际是SynchronousMethodHandler
的invoke 方法代理执行,在该方法中会完成请求模板创建、参数解析、重试机制加载。该处理器会查询方法参数中是否有Options 对象,没有则会将初始化加载的超时配置,传递到下游。
public Object invoke(Object[] argv) throws Throwable {
// 1. 构建请求模板,封装参数、路径等信息。
RequestTemplate template = this.buildTemplateFromArgs.create(argv);
// 2. 查询超时配置,将方法的参数集合转为Stream 流,如果没有发现参数中有Options 对象,
// 则会使用方式执行器中的Options ,也就是从yml 中加载的配置
Options options = this.findOptions(argv);
Retryer retryer = this.retryer.clone();
while(true) {
try {
return this.executeAndDecode(template, options);
} catch (RetryableException var9) {
// 省略....
}
}
继续走到负载均衡客户端(Ribbon),可以看到这里又会去获取一次客户端配置。
在getClientConfig
方法中,会处理超时配置Options
对象。
IClientConfig getClientConfig(Options options, String clientName) {
Object requestConfig;
// 查看Options 是否默认的,也就是是否是1秒。
if (options == DEFAULT_OPTIONS) {
// 是默认的,则直接加载IClientConfig (容器中)对象中的配置
requestConfig = this.clientFactory.getClientConfig(clientName);
} else {
// 是自定义了超时配置,则设置自定义配置到IClientConfig(自己创建)对象中
requestConfig = new LoadBalancerFeignClient.FeignOptionsClientConfig(options);
}
return (IClientConfig)requestConfig;
}
可以看到,负载均衡器客户端获取到了自定义配置,然后继续往下走。
走到执行方法:
return ((RibbonResponse)this.lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig)).toResponse();
负载均衡器会调用本身的this.lbClient(clientName)
方法,调用工厂创建一个负载均衡器FeignLoadBalancer
,会查询缓存,没有则会创建一个并放入缓存。
public FeignLoadBalancer create(String clientName) {
// 缓存查询
FeignLoadBalancer client = (FeignLoadBalancer)this.cache.get(clientName);
if (client != null) {
return client;
} else {
// 没有则又会查询一次客户端配置,直接查询IClientConfig Bean 对象
// 在自动配置类RibbonClientConfiguration中, 超时配置都是1秒。
IClientConfig config = this.factory.getClientConfig(clientName);
ILoadBalancer lb = this.factory.getLoadBalancer(clientName);
ServerIntrospector serverIntrospector = (ServerIntrospector)this.factory.getInstance(clientName, ServerIntrospector.class);
FeignLoadBalancer client = this.loadBalancedRetryFactory != null ? new RetryableFeignLoadBalancer(lb, config, serverIntrospector, this.loadBalancedRetryFactory) : new FeignLoadBalancer(lb, config, serverIntrospector);
this.cache.put(clientName, client);
return (FeignLoadBalancer)client;
}
}
可以看到,创建的FeignLoadBalancer
对象中,超时配置,又到了默认的一秒。
接着调用FeignLoadBalancer
对象的executeWithLoadBalancer
方法,均衡器开始执行,参数是一个Ribbon请求对象和请求配置IClientConfig
对象(因为有自定义,所以这里是重新创建的,并不是容器中的)。
在通过均衡算法,获取到真实的服务地址后,进入到execute 方法,该方法传入了可用服务和 请求配置IClientConfig
对象(重新创建的)。
在execute 方法可以看到,又有对Options
进行一次判断。该请求存在自定义超时配置,则会解析并封装为Options
,没有配置,则使用默认配置(1秒)。
public FeignLoadBalancer.RibbonResponse execute(FeignLoadBalancer.RibbonRequest request, IClientConfig configOverride) throws IOException {
// 这次请求的配置参数
Options options;
// 发现当前请求存在客户端配置
if (configOverride != null) {
// 将配置中的自定义超时 解析,并封装为Options 对象
RibbonProperties override = RibbonProperties.from(configOverride);
options = new Options(override.connectTimeout(this.connectTimeout), override.readTimeout(this.readTimeout));
} else {
// 没有配置,则默认使用Ribbon 客户端配置,而Ribbon 又是从IClientConfig Bean 对象获取的,默认都是一秒。
options = new Options(this.connectTimeout, this.readTimeout);
}
Response response = request.client().execute(request.toRequest(), options);
return new FeignLoadBalancer.RibbonResponse(request.getUri(), response);
}
最终均衡器,会调用HTTP 客户端进行请求发送,这里使用的是OkHttpClient 。这里会覆盖掉OkHttpClient 超时配置,使用自定义或者默认的超时配置(所以在OkHttp中的配置超时没有啥用…)。
public Response execute(feign.Request input, Options options) throws IOException {
okhttp3.OkHttpClient requestScoped;
// 查看OkHttp 的超时配置是否和 Feign 配置的超时一样
// 一样则不处理。
if (this.delegate.connectTimeoutMillis() == options.connectTimeoutMillis() && this.delegate.readTimeoutMillis() == options.readTimeoutMillis() && this.delegate.followRedirects() == options.isFollowRedirects()) {
requestScoped = this.delegate;
} else {
// 不一样,则重新构建一个 OkHttpClient ...(这里是否有优化空间,Ribbon 会覆盖OkHttpClient 配置)
requestScoped = this.delegate.newBuilder().connectTimeout((long)options.connectTimeoutMillis(), TimeUnit.MILLISECONDS).readTimeout((long)options.readTimeoutMillis(), TimeUnit.MILLISECONDS).followRedirects(options.isFollowRedirects()).build();
}
Request request = toOkHttpRequest(input);
// 执行请求
okhttp3.Response response = requestScoped.newCall(request).execute();
return toFeignResponse(response, input).toBuilder().request(input).build();
}
请求发送以后,如果触发了超时时间,就会抛出超时异常,所有Feign 超时配置,最后是作用到了底层的HTTP 框架。
3. 总结
1、 Feign原生构建客户端,超时时间是10S、60S,但是没有用到;
2、 方法处理器在加载时,会获取到自定义配置;
3、 第一次加载时,客户端配置类IClientConfig
注入到了IOC中,默认超时都是1S;
4、 请求执行时,会构建超时配置类Options
,如果存在自定义配置,就会使用自定义配置创建Options
对象,并将该对象传递给HTTP客户端框架;
5、 HTTP客户端会判断自身设置的超时时间和Feign设置的是否相同,不同则会重新创建一个客户端请求,一样则会使用Feign代理的客户端(所以需要注意HTTP框架和Feign的超时要设置一样,不然重新创建会消耗资源);