0
点赞
收藏
分享

微信扫一扫

Spring Cloud组件那么多超时设置,如何理解和运用?

源码之路 2022-01-28 阅读 84

前言

Spring Cloud 作为微服务解决方案 全家桶,集合了丰富的微服务组件,如GatewayFeignHystrix,RibbonOkHttpEureka等等。而作为服务调用环节涉及到的几个组件:FeignHystrix,RibbonOkHttp 都有超时时间的设置,Spring Cloud 是如何优雅地把它们协调好呢?本文将为你揭晓答案。

1. Spring Cloud 中发起一个接口调用,经过了哪些环节?

Spring Cloud 在接口调用上,大致会经过如下几个组件配合: Feign -----> Hystrix —>Ribbon —> Http Client(apache http components 或者 Okhttp) 具体交互流程上,如下图所示:

  • 接口化请求调用 当调用被@FeignClient注解修饰的接口时,在框架内部,会将请求转换成Feign的请求实例feign.Request,然后交由Feign框架处理。
  • Feign :转化请求 至于Feign的详细设计和实现原理,在此不做详细说明。 请参考我的另外一篇文章:Spring Cloud Feign 设计原理
  • Hystrix :熔断处理机制 Feign的调用关系,会被Hystrix代理拦截,对每一个Feign调用请求,Hystrix都会将其包装成HystrixCommand,参与Hystrix的流控和熔断规则。如果请求判断需要熔断,则Hystrix直接熔断,抛出异常或者使用FallbackFactory返回熔断Fallback结果;如果通过,则将调用请求传递给Ribbon组件。 关于Hystrix的工作原理,参考Spring Cloud Hystrix设计原理
  • Ribbon :服务地址选择 当请求传递到Ribbon之后,Ribbon会根据自身维护的服务列表,根据服务的服务质量,如平均响应时间,Load等,结合特定的规则,从列表中挑选合适的服务实例,选择好机器之后,然后将机器实例的信息请求传递给Http Client客户端,HttpClient客户端来执行真正的Http接口调用; 关于Ribobn的工作原理,参考Spring Cloud Ribbon设计原理
  • HttpClient :Http客户端,真正执行Http调用 根据上层Ribbon传递过来的请求,已经指定了服务地址,则HttpClient开始执行真正的Http请求。 关于HttpClient的其中一个实现OkHttp的工作原理,请参考Spring Cloud OkHttp设计原理

2.每个组件阶段的超时设置

如上一章节展示的调用关系,每个组件自己有独立的接口调用超时设置参数,下面将按照从上到下的顺序梳理:

2.1 feign的默认配置

feign 的配置可以采用feign.client.config.<feginName>....的格式为每个feign客户端配置,对于默认值,可以使用feign.client.config.default..的方式进行配置,该配置项在Spring Cloud中,使用FeignClientProperties类表示。

feign:
  client:
    config:
      <feignName>:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
        errorDecoder: com.example.SimpleErrorDecoder
        retryer: com.example.SimpleRetryer
        requestInterceptors:
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        decode404: false
        encoder: com.example.SimpleEncoder
        decoder: com.example.SimpleDecoder
        contract: com.example.SimpleContract

其中,关于feign的管理连接超时的配置项:

## 网络连接时间
feign.client.config.<clientname>.connectTimeout=
## 读超时时间
feign.client.config.<clientname>.readTimeout=

2.2 Spring Cloud 加载feign配置项的原理:

  1. 检查是否Feign是否制定了上述的配置项,即是否有FeignClientProperties实例;
  2. 如果有上述的配置项,则表明Feign是通过properties初始化的,即configureUsingProperties;
  3. 根据配置项feign.client.defaultToProperties的结果,使用不同的配置覆盖策略。

feign初始化的过程,其实就是构造Feign.Builder的过程,如下图所示:

相关代码实现如下:

protected void configureFeign(FeignContext context, Feign.Builder builder) {
		FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
		if (properties != null) {
			if (properties.isDefaultToProperties()) {
				configureUsingConfiguration(context, builder);
				configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
				configureUsingProperties(properties.getConfig().get(this.name), builder);
			} else {
				configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
				configureUsingProperties(properties.getConfig().get(this.name), builder);
				configureUsingConfiguration(context, builder);
			}
		} else {
			configureUsingConfiguration(context, builder);
		}
	}

2.3.场景分析

结合上述的加载原理,初始化过程可以分为如下几种场景:

  • 场景1:没有通过配置文件配置 在这种模式下,将使用configureUsingConfiguration,此时将会使用Spring 运行时自动注入的Bean完成配置:
	protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) {
		Logger.Level level = getOptional(context, Logger.Level.class);
		if (level != null) {
			builder.logLevel(level);
		}
		Retryer retryer = getOptional(context, Retryer.class);
		if (retryer != null) {
			builder.retryer(retryer);
		}
		ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class);
		if (errorDecoder != null) {
			builder.errorDecoder(errorDecoder);
		}
		Request.Options options = getOptional(context, Request.Options.class);
		if (options != null) {
			builder.options(options);
		}
		Map<String, RequestInterceptor> requestInterceptors = context.getInstances(
				this.name, RequestInterceptor.class);
		if (requestInterceptors != null) {
			builder.requestInterceptors(requestInterceptors.values());
		}

		if (decode404) {
			builder.decode404();
		}
	}

默认情况下,Spring Cloud对此超时时间的设置为:

connectTimeoutMillis = 10 * 1000
readTimeoutMillis = 60 * 1000
  • 场景2:配置了FeignClientProperties,并且配置了feign.client.defaultToProperties = true,此时的这种场景,其配置覆盖顺序如下所示: configureUsingConfiguration—> configurationUsingPropeties("default")----> configurationUsingProperties("<client-name>") 如下图配置所示,最终超时时间为:connectionTimeout=4000,readTimeout=4000
feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
      <client-name>:
        connectTimeout: 4000
        readTimeout: 4000
  • 场景3:配置了FeignClientProperties,并且配置了feign.client.defaultToProperties = false,此时的这种场景,配置覆盖顺序是: configurationUsingPropeties("default")----> configurationUsingProperties("<client-name>")—> configureUsingConfiguration 如果按照这种策略,则最终的超时时间设置就为connectionTimeout=10000,readTimeout=6000

2.2 Hystrix的超时设置

Hystrix的超时设置,在于命令执行的时间,一般而言,这个时间要稍微比Feign的超时时间稍微长些,因为Command除了请求调用之外,还有一些业务代码消耗。hystrix的配置规则和feign的风格比较类似:hystrix.command.<service-name>

hystrix.command.default.execution.isolation.strategy = THREAD
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds = 10000
hystrix.command.default.execution.timeout.enabled = true
hystrix.command.default.execution.isolation.thread.interruptOnTimeout = true
hystrix.command.default.execution.isolation.thread.interruptOnFutureCancel = false

2.3 Ribbon 的超时时间

Ribbon的超时时间可以通过如下配置项指定,默认情况下,这两项的值和feign的配置保持一致:

<service-name>.ribbon.ConnectTimeout= <feign-default: 10000>
<service-name>.ribbon.ReadTimeout= <feign-default:6000>

其核心代码逻辑如下:

	IClientConfig getClientConfig(Request.Options options /*feign配置项*/, String clientName) {
		IClientConfig requestConfig;
		if (options == DEFAULT_OPTIONS) {
			requestConfig = this.clientFactory.getClientConfig(clientName);
		} else {
			requestConfig = new FeignOptionsClientConfig(options);
		}
		return requestConfig;
	}
       static class FeignOptionsClientConfig extends DefaultClientConfigImpl {
                //将Feign的配置设置为Ribbon的`IClientConfig`中
		public FeignOptionsClientConfig(Request.Options options) {
			setProperty(CommonClientConfigKey.ConnectTimeout,
					options.connectTimeoutMillis());
			setProperty(CommonClientConfigKey.ReadTimeout, options.readTimeoutMillis());
		}

		@Override
		public void loadProperties(String clientName) {

		}

		@Override
		public void loadDefaultValues() {

		}

	}
@Configuration
@ConditionalOnProperty("ribbon.okhttp.enabled")
@ConditionalOnClass(name = "okhttp3.OkHttpClient")
public class OkHttpRibbonConfiguration {
   @RibbonClientName
   private String name = "client";

   @Configuration
   protected static class OkHttpClientConfiguration {
   	private OkHttpClient httpClient;

   	@Bean
   	@ConditionalOnMissingBean(ConnectionPool.class)
   	public ConnectionPool httpClientConnectionPool(IClientConfig config,
   												   OkHttpClientConnectionPoolFactory connectionPoolFactory) {
                       
   		RibbonProperties ribbon = RibbonProperties.from(config);
   		int maxTotalConnections = ribbon.maxTotalConnections();
   		long timeToLive = ribbon.poolKeepAliveTime();
   		TimeUnit ttlUnit = ribbon.getPoolKeepAliveTimeUnits();
   		return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
   	}

   	@Bean
   	@ConditionalOnMissingBean(OkHttpClient.class)
   	public OkHttpClient client(OkHttpClientFactory httpClientFactory,
   							   ConnectionPool connectionPool, IClientConfig config) {
   		RibbonProperties ribbon = RibbonProperties.from(config);
   		this.httpClient = httpClientFactory.createBuilder(false)
                       //使用Ribbon的超时时间来初始化OKHttp的 
   				.connectTimeout(ribbon.connectTimeout(), TimeUnit.MILLISECONDS)
   				.readTimeout(ribbon.readTimeout(), TimeUnit.MILLISECONDS)
   				.followRedirects(ribbon.isFollowRedirects())
   				.connectionPool(connectionPool)
   				.build();
   		return this.httpClient;
   	}

   	@PreDestroy
   	public void destroy() {
   		if(httpClient != null) {
   			httpClient.dispatcher().executorService().shutdown();
   			httpClient.connectionPool().evictAll();
   		}
   	}
   }

2.4 Http Client的超时时间

为了保证整个组件调用链的超时关系,一般Spring Cloud采取的策略是:依赖方的超时配置覆盖被依赖方的配置 当然这个也不是绝对的,实际上对于Feign而言,可以直接指定FeignHttpClient之间的配置关系,如下所示:

@ConfigurationProperties(prefix = "feign.httpclient")
public class FeignHttpClientProperties {
	public static final boolean DEFAULT_DISABLE_SSL_VALIDATION = false;
	public static final int DEFAULT_MAX_CONNECTIONS = 200;
	public static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 50;
	public static final long DEFAULT_TIME_TO_LIVE = 900L;
	public static final TimeUnit DEFAULT_TIME_TO_LIVE_UNIT = TimeUnit.SECONDS;
	public static final boolean DEFAULT_FOLLOW_REDIRECTS = true;
	public static final int DEFAULT_CONNECTION_TIMEOUT = 2000;
	public static final int DEFAULT_CONNECTION_TIMER_REPEAT = 3000;

	private boolean disableSslValidation = DEFAULT_DISABLE_SSL_VALIDATION;
        //连接池最大连接数,默认200
	private int maxConnections = DEFAULT_MAX_CONNECTIONS;
        //每一个IP最大占用多少连接 默认 50
	private int maxConnectionsPerRoute = DEFAULT_MAX_CONNECTIONS_PER_ROUTE;
        //连接池中存活时间,默认为5
	private long timeToLive = DEFAULT_TIME_TO_LIVE;
        //连接池中存活时间单位,默认为秒
	private TimeUnit timeToLiveUnit = DEFAULT_TIME_TO_LIVE_UNIT;
        //http请求是否允许重定向
	private boolean followRedirects = DEFAULT_FOLLOW_REDIRECTS;
        //默认连接超时时间:2000毫秒
	private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
        //连接池管理定时器执行频率:默认 3000毫秒
	private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT;

}

Http Client的实现OkHttp为例,如果指定了feign.okhttp.enabled,则会初始化Okhttp,其中,OkHttp的超时时间设置为:feign.httpclient.connectionTimeout,默认值为2000毫秒

@Configuration
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty(value = "feign.okhttp.enabled")
class OkHttpFeignLoadBalancedConfiguration {

	@Configuration
	@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
	protected static class OkHttpFeignConfiguration {
		private okhttp3.OkHttpClient okHttpClient;

		@Bean
		@ConditionalOnMissingBean(ConnectionPool.class)
		public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties,
													   OkHttpClientConnectionPoolFactory connectionPoolFactory) {
			Integer maxTotalConnections = httpClientProperties.getMaxConnections();
			Long timeToLive = httpClientProperties.getTimeToLive();
			TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
			return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
		}

		@Bean
		public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
										   ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) {
			Boolean followRedirects = httpClientProperties.isFollowRedirects();
			Integer connectTimeout = httpClientProperties.getConnectionTimeout();
			this.okHttpClient = httpClientFactory.createBuilder(httpClientProperties.isDisableSslValidation()).
					connectTimeout(connectTimeout, TimeUnit.MILLISECONDS).
					followRedirects(followRedirects).
					connectionPool(connectionPool).build();
			return this.okHttpClient;
		}

		@PreDestroy
		public void destroy() {
			if(okHttpClient != null) {
				okHttpClient.dispatcher().executorService().shutdown();
				okHttpClient.connectionPool().evictAll();
			}
		}
	}

	@Bean
	@ConditionalOnMissingBean(Client.class)
	public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
							  SpringClientFactory clientFactory, okhttp3.OkHttpClient okHttpClient) {
		OkHttpClient delegate = new OkHttpClient(okHttpClient);
		return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
	}
}

3. 最佳实践

综上所述,一般在Spring Cloud设置过程中,

  • 只需要指定Feign使用什么Http Client客户端即可,比如feign.okhttp.enabled=true
  • Feign客户端的Http Client的配置项,统一使用如下配置即可,Spring Cloud会拿才配置项初始化不同的Http Client客户端的。
### http client最大连接数,默认200
feign.httpclient.maxConnections = 200
### 每个IP路由最大连接数量
feign.httpclient.maxConnectionsPerRoute= 50
### 连接存活时间
feign.httpclient.timeToLive = 900
### 连接存活时间单位
feign.httpclient.timeToLiveUnit = SECONDS
### 连接超时时间
feign.httpclient.connectionTimeout = 2000
### 连接超时定时器的执行频率
fein.httpclient.connectionTimeout=3000
  • Hystrix的作用:Feign或者Http Client 只能规定所有接口调用的超时限制,而Hystrix可以设置到每一个接口的超时时间,控制力度最细,相对应地,配置会更繁琐。
举报

相关推荐

0 条评论