Spring Cloud Open Feign系列【8】Feign超时配置详解及源码分析

Spring Cloud Open Feign系列【8】Feign超时配置详解及源码分析,第1张

Spring Cloud Open Feign系列【8】Feign超时配置详解及源码分析

文章目录
  • 前言
  • 官网案例
  • 连接超时和读取超时配置
    • 参数说明
    • 配置案例
    • 源码分析
      • 1. 启动项目
      • 2. 执行流程
      • 3. 总结

前言

在之前的文档中,介绍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(客户端名称) :也就是对应@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 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 的超时要设置一样,不然重新创建会消耗资源)。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5685204.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存