08、Feign超时配置详解及源码分析

前言

在之前的文档中,介绍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

连接超时和读取超时配置

在配置中,有一个ConnectTimeoutReadTimeout,这是在发送请求时的基础配置,特别重要,所以接下来分析下这两个具体是干嘛的,源码是怎么处理的。

参数说明

ConnectTimeout连接超时时间,Feign 是基于HTTP 的远程调用,众所周知,HTTP 请求会进行TCP的三次握手,这个连接超时时间,就是多少秒没连接上,就会抛出超时异常。

ReadTimeout读取超时时间,HTTP成功连接后,客户端发会送请求报文,服务端收到后解析并返回响应报文,在写出响应报文时,如果超过了设置的时间还没写完,也会抛出超时异常。在某些接口请求数据量大的时候,很容易出现读取超时,所以要格外注意这个问题。

可以在RibbonClientConfiguration配置类中看到,客户端超时配置默认都是1秒,所以不自己改配置的话,很容易造成超时问题。
&nbsp;

配置案例

在订单服务中,让线程睡眠十秒才返回响应。
&nbsp;
访问账户服务,发现1秒左右就马上抛出超时异常了。
&nbsp;
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),可以看到这里又会去获取一次客户端配置。
&nbsp;
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;
    }

可以看到,负载均衡器客户端获取到了自定义配置,然后继续往下走。
&nbsp;
走到执行方法:

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对象中,超时配置,又到了默认的一秒。
&nbsp;
接着调用FeignLoadBalancer对象的executeWithLoadBalancer方法,均衡器开始执行,参数是一个Ribbon请求对象和请求配置IClientConfig对象(因为有自定义,所以这里是重新创建的,并不是容器中的)。
&nbsp;
在通过均衡算法,获取到真实的服务地址后,进入到execute 方法,该方法传入了可用服务和 请求配置IClientConfig对象(重新创建的)。
&nbsp;
在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的超时要设置一样,不然重新创建会消耗资源);