淺談SpringCloud feign的http請求組件優化方案

1 描述

如果我們直接使用SpringCloud Feign進行服務間調用的時候,http組件使用的是JDK的HttpURLConnection,每次請求都會新建一個連接,沒有使用線程池復用。具體的可以從源碼進行分析

2 源碼分析

我們在分析源碼很難找到入口,不知道從何開始入手,我們在分析SpringCloud feign的時候可用在配置文件下面我講一下個人的思路。

1 首先我點擊@EnableFeignClients 看一下這個註解在哪個資源路徑下

如下圖所示:

2 找到服務啟動加載的配置文件

3 因為feign底層的負載均衡是基於Ribbon的所以很快就找到瞭FeignRibbonClientAutoConfiguration.java 這個類

@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@Configuration
@AutoConfigureBefore(FeignAutoConfiguration.class)
@EnableConfigurationProperties({ FeignHttpClientProperties.class })
//Order is important here, last should be the default, first should be optional
// see https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
 OkHttpFeignLoadBalancedConfiguration.class,
 DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {

首先我們從這三個類進行分析,從名字上來看我為瞭驗證沒有特殊配置,feign底層走的是不是默認的DefaultFeignLoadBalancedConfiguration.class

OkHttpFeignLoadBalancedConfiguration.class

HttpClientFeignLoadBalancedConfiguration.class

DefaultFeignLoadBalancedConfiguration.class

DefaultFeignLoadBalancedConfiguration.class

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

從上面代碼可知每次請求過來都會創建一個新的client,具體的源碼演示有興趣的可以深入研究,在這裡不是我們所研究的重點。

OkHttpFeignLoadBalancedConfiguration.class

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

從源碼可以看出

1 該類是個配置類,當引入OkHttpClient.Class會加載

client方法中可以看出會返回一個http連接池的client
HttpClientFeignLoadBalancedConfiguration
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
class HttpClientFeignLoadBalancedConfiguration {

這個類和OkHttpFeignLoadBalancedConfiguration原理類型

使用OKHttp替代默認的JDK的HttpURLConnection

使用appach httpclient使用教程類似

使用方法

1 pom

 <dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-okhttp</artifactId>
 </dependency>

2 Yml文件

feign:
 okhttp:
 enabled: true

3 自定義連接池

可以通過代碼進行配置,也可以通過yml配置

@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkHttpConfig {
 @Bean
 public okhttp3.OkHttpClient okHttpClient(){
 return new okhttp3.OkHttpClient.Builder()
  .readTimeout(60,TimeUnit.SECONDS)
  .connectTimeout(60,TimeUnit.SECONDS)
  .connectionPool(new ConnectionPool())
  .build();
 }
}

驗證

默認的Feign處理會走到如下位置;

位置處於如下圖所示

 @Override
 public Response execute(Request request, Options options) throws IOException {
 HttpURLConnection connection = convertAndSend(request, options);
 return convertResponse(connection).toBuilder().request(request).build();
 }

走okhttp客戶端會走如下代碼

具體位置如下圖所示:

@Override public Response execute() throws IOException {
 synchronized (this) {
 if (executed) throw new IllegalStateException("Already Executed");
 executed = true;
 }
 captureCallStackTrace();
 try {
 client.dispatcher().executed(this);
 Response result = getResponseWithInterceptorChain();
 if (result == null) throw new IOException("Canceled");
 return result;
 } finally {
 client.dispatcher().finished(this);
 }
 }

驗證結果

如下所示:

彩蛋

okhttp客戶端會走的代碼可以看出來okhttp有synchronized鎖線程安全的那默認的是否是線程安全的呢 有待去驗證。

追加

如果發現配置的超時時間無效,可以添加以下配置,因為讀取超時配置的時候沒有讀取上面的okhttp的配置參數,而是從Request中讀取。

具體配置如下所示:

 @Bean
 public Request.Options options(){
 return new Request.Options(60000,60000);
 }

補充:springCloud feign使用/優化總結

基於springCloud Dalston.SR3版本

1.當接口參數是多個的時候 需要指定@RequestParam 中的value來明確一下。

/**
 * 用戶互掃
 * @param uid 被掃人ID
 * @param userId 當前用戶ID
 * @return
 */
@PostMapping(REQ_URL_PRE + "/qrCodeReturnUser")
UserQrCode qrCodeReturnUser(@RequestParam("uid") String uid,@RequestParam("userId") Integer userId);

2.接口參數為對象的時候 需要使用@RequestBody註解 並采用POST方式。

3.如果接口是簡單的數組/列表參數 這裡需要使用Get請求才行

@GetMapping(REQ_URL_PRE + "/getUserLevels")
Map<Integer, UserLevel> getUserLevels(@RequestParam("userIds") List<Integer> userIds);

4.直接可以在@FeignClient中配置降級處理方式 對於一些不重要的業務 自定義處理很有幫助

@FeignClient(value = "cloud-user", fallback = IUsers.UsersFallback.class)

5.feign默認隻有HystrixBadRequestException異常不會走熔斷,其它任何異常都會進入熔斷,需要重新實現一下ErrorDecoder包裝業務異常

示例:https://github.com/peachyy/feign-support

6. feign HTTP請求方式選擇

feign默認使用的是基於JDK提供的URLConnection調用HTTP接口,不具備連接池。所以資源開銷上有點影響,經測試JDK的URLConnection比Apache HttpClient快很多倍。但是Apache HttpClient和okhttp都支持配置連接池功能。具體選擇需要權衡

7.默認不啟用hystrix 需要手動指定feign.hystrix.enabled=true 開啟熔斷

8.啟用壓縮也是一種有效的優化方式

feign.compression.request.enabled=true
feign.compression.response.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json

9.參數相關調優

hystrix線程數設置

設置參數hystrix.threadpool.default.coreSize 來指定熔斷隔離的線程數 這個數需要調優,經測試 線程數我們設置為和提供方的容器線程差不多,吞吐量高許多。

第一次訪問服務出錯的問題

啟用Hystrix後,很多服務當第一次訪問的時候都會失敗 是因為初始化負載均衡一系列操作已經超出瞭超時時間瞭 默認的超時時間為1S,設置參數超時時間hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=30000 可解決這個問題。

負載均衡參數設置

設置瞭Hystrix的超時參數會 還需設置一下ribbon的相關參數 這些參數和Hystrix的超時參數有一定的邏輯關系

請求處理的超時時間 ribbon.ReadTimeout=120000

請求連接的超時時間 ribbon.ConnectTimeout=30000

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。如有錯誤或未考慮完全的地方,望不吝賜教。

推薦閱讀: