SpringCloud系列之-feign请求解析及功能优化

我从feign的入口开始串feign的请求初始化流程:

1、流程串联

通过springboot项目启动加载@EnableAutoConfiguration-->spring.factories,FeignRibbonClientAutoConfiguration这个配置类会被加载。

SpringCloud系列之-feign请求解析及功能优化

在这个类经过加载一些配置判断最终通过LoadBalancerFeignClient产生客户端供我们使用,默认情况下我们采用的是DefaultFeignLoadBalancedConfiguration的配置(前提是HttpClientFeignLoadBalancedConfiguration和OkHttpFeignLoadBalancedConfiguration没有被实现的情况下)

SpringCloud系列之-feign请求解析及功能优化

2、配置详解

对以上import导入的三个配置类做以下配置分析:

HttpClientFeignLoadBalancedConfiguration 
        为feigin配置appache client的线程池,当引入ApacheHttpClient.class类时,会初始化这个配置类、方法feignClient()中:
        根据@ConditionalOnMissingBean(Client.class)知道如果有HttpClient 对象,则创建的ApacheHttpClient使用自己定义的HttpClient。如果没有,则使用默认值。最后生成LoadBalancerFeignClient对象。

@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
class HttpClientFeignLoadBalancedConfiguration {

	@Configuration
	@ConditionalOnMissingBean(CloseableHttpClient.class)
	protected static class HttpClientFeignConfiguration {
		private final Timer connectionManagerTimer = new Timer(
				"FeignApacheHttpClientConfiguration.connectionManagerTimer", true);

		private CloseableHttpClient httpClient;

		@Autowired(required = false)
		private RegistryBuilder registryBuilder;

		@Bean
		@ConditionalOnMissingBean(HttpClientConnectionManager.class)
		public HttpClientConnectionManager connectionManager(
				ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
				FeignHttpClientProperties httpClientProperties) {
			final HttpClientConnectionManager connectionManager = connectionManagerFactory
					.newConnectionManager(httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(),
							httpClientProperties.getMaxConnectionsPerRoute(),
							httpClientProperties.getTimeToLive(),
							httpClientProperties.getTimeToLiveUnit(), registryBuilder);
			this.connectionManagerTimer.schedule(new TimerTask() {
				@Override
				public void run() {
					connectionManager.closeExpiredConnections();
				}
			}, 30000, httpClientProperties.getConnectionTimerRepeat());
			return connectionManager;
		}

		@Bean
		@ConditionalOnProperty(value = "feign.compression.response.enabled", havingValue = "true")
		public CloseableHttpClient customHttpClient(HttpClientConnectionManager httpClientConnectionManager,
											  FeignHttpClientProperties httpClientProperties) {
			HttpClientBuilder builder = HttpClientBuilder.create().disableCookieManagement().useSystemProperties();
			this.httpClient = createClient(builder, httpClientConnectionManager, httpClientProperties);
			return this.httpClient;
		}

		@Bean
		@ConditionalOnProperty(value = "feign.compression.response.enabled", havingValue = "false", matchIfMissing = true)
		public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory, HttpClientConnectionManager httpClientConnectionManager,
											  FeignHttpClientProperties httpClientProperties) {
			this.httpClient = createClient(httpClientFactory.createBuilder(), httpClientConnectionManager, httpClientProperties);
			return this.httpClient;
		}
		private CloseableHttpClient createClient(HttpClientBuilder builder, HttpClientConnectionManager httpClientConnectionManager,
												 FeignHttpClientProperties httpClientProperties) {
			RequestConfig defaultRequestConfig = RequestConfig.custom()
					.setConnectTimeout(httpClientProperties.getConnectionTimeout())
					.setRedirectsEnabled(httpClientProperties.isFollowRedirects())
					.build();
			CloseableHttpClient httpClient = builder.setDefaultRequestConfig(defaultRequestConfig).
					setConnectionManager(httpClientConnectionManager).build();
			return httpClient;
		}
		@PreDestroy
		public void destroy() throws Exception {
			connectionManagerTimer.cancel();
			if(httpClient != null) {
				httpClient.close();
			}
		}
	}

		@Bean
		@ConditionalOnMissingBean(Client.class)
		public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
								  SpringClientFactory clientFactory, HttpClient httpClient) {
			ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
			return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
		}

}

OkHttpFeignLoadBalancedConfiguration 
        为feigin配置OkHttp,类似apache httpclient。 

@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);
	}
}

DefaultFeignLoadBalancedConfiguration 
        为feigin配置HttpURLConnection,方法feignClient():只有以上两个Client没有生产对象时,才在这个方法中使用Client.Default生成LoadBalancerFeignClient。

@Configuration
class DefaultFeignLoadBalancedConfiguration {
	@Bean
	@ConditionalOnMissingBean
	public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
							  SpringClientFactory clientFactory) {
		return new LoadBalancerFeignClient(new Client.Default(null, null),
				cachingFactory, clientFactory);
	}
}

在默认的情况下通过new Client.Default()方法可见,Default 使用HttpURLConnection 建立连接且每次请求都建立一个新的连接,效率不高。

SpringCloud系列之-feign请求解析及功能优化

3、性能调优

       默认情况下,服务之间调用使用的HttpURLConnection,效率非常低。为了提高效率,可以通过连接池提高效率,我们使用appache httpclient做为连接池为例。配置OkHttpClient连接池,也是类似的方法。 
配置线程池方法:引入appache httpclient并启动对应配置,最后还需要生成HttpClient对象。
1)引入pom依赖

<!-- 增加feign-appache httpclient -->
 <dependency>
     <groupId>io.github.openfeign</groupId>
     <artifactId>feign-httpclient</artifactId>
 </dependency>

2)配置参数application-hystrix-feign.yml启用httpclient客户端

# feign配置
feign:
  hystrix:
    # 在feign中开启hystrix功能,默认情况下feign不开启hystrix功能
    enabled: true
  ## 配置httpclient线程池
  httpclient:
    enabled: true
  okhttp:
    enabled: false

3)自定义配置类

       使用配置类,生成HttpClient 对象。因为使用PoolingHttpClientConnectionManager连接池,我们需要启动定时器,定时回收过期的连接。因为PoolingHttpClientConnectionManager里存储的连接,如果连接被服务器端关闭了,客户端监测不到连接的状态变化。在httpclient中,当连接空闲超过10s后,服务端会关闭本端连接。但是客户端的连接一直保持连接,即使服务端关闭连接,客户端也不会关闭连接。所以下次使用连接,程序从连接中获取一个连接(即使这个连接已经被服务端),也需要进行确认,如果发现连接异常,则服务端会发送RST信令,双方重新建立新的连接。

       为了解决这个问题HttpClient会在使用某个连接前,监测这个连接是否已经过时,如果服务器端关闭了连接,那么会重现建立一个连接。但是这种过时检查并不是100%有效。所以建立创建一个监控进程来专门回收由于长时间不活动而被判定为失效的连接。

@Configuration
public class HttpClientPool {

    //这些配置可以统一抽取至配置文件方便管理和维护(这里仅仅是方便展示)
    @Bean
    public HttpClient httpClient(){
        System.out.println("init feign httpclient configuration " );
        // 生成默认请求配置
        RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
        // 超时时间
        requestConfigBuilder.setSocketTimeout(5 * 1000);
        // 连接时间
        requestConfigBuilder.setConnectTimeout(5 * 1000);
        RequestConfig defaultRequestConfig = requestConfigBuilder.build();
        // 连接池配置
        // 长连接保持30秒
        final PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.MILLISECONDS);
        // 总连接数
        pollingConnectionManager.setMaxTotal(5000);
        // 同路由的并发数
        pollingConnectionManager.setDefaultMaxPerRoute(100);

        // httpclient 配置
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        // 保持长连接配置,需要在头添加Keep-Alive
        httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
        httpClientBuilder.setConnectionManager(pollingConnectionManager);
        httpClientBuilder.setDefaultRequestConfig(defaultRequestConfig);
        HttpClient client = httpClientBuilder.build();


        // 启动定时器,定时回收过期的连接
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                //        System.out.println("=====closeIdleConnections===");
                pollingConnectionManager.closeExpiredConnections();
                pollingConnectionManager.closeIdleConnections(5, TimeUnit.SECONDS);
            }
        }, 10 * 1000, 5 * 1000);
        System.out.println("===== Apache httpclient 初始化连接池===");

        return client;
    }
}

4)填坑

       我们系统采用zuul网关(2台)进行路由,请求路由至auth服务(2台)进行校验,最后到各个接口服务,单压接口没有任何问题(4台服务器),但是通过zuul网关访问吞吐量达不到2000,看了zuul的版本配置2.0的zuul基于netty使用异步非阻塞,参数也尽可能满足,但是吞吐量还是上不去。最后排查是在zuul里面调用auth服务的时候,auth服务使用的是feign客户端请求,而且使用的是feign的默认配置(默认是没有连接池的)。最终优化效果吞吐量达到3000以上。

如有披露或问题欢迎留言或者入群探讨

SpringCloud系列之-feign请求解析及功能优化