Spring Cloud 框架最底層核心的組件就是服務調用方式,通常Spring Cloud框架採用的是HTTP的調用框架,本文將在 Spring Cloud應用場景下,介紹組件OkHttp3
的設計原理。html
Spring Cloud做爲組合式的分佈式微服務解決方案,再服務調用上,至少須要解決以下幾個環節:java
OkHttp是square
公司開發的一個同時支持Http和Http2協議的Java客戶端,可用於Android和Java應用中。
OKHttp有以下幾個特性:spring
本章節將詳細介紹OkHttp3
底層的設計原理,並結合設計原理,總結在使用過程當中應當注意的事項。編程
以以下的簡單交互代碼爲例,OkHttp3的簡單工做方式以下所示:瀏覽器
//Step1:初始化鏈接池 ConnectionPool connectionPool = new ConnectionPool(50, 5, TimeUnit.MINUTES); OkHttpClient.Builder builder = new OkHttpClient.Builder().connectionPool(connectionPool); //Step2:建立Client OkHttpClient client = builder.build(); //Step3:構造請求 Request request = new Request.Builder() .url("http://www.baidu.com") .build(); //Step4:發送請求 Response response = client.newCall(request).execute(); String result = response.body().string(); System.out.println(result);
根據上述的流程,其內部請求主要主體以下所示:緩存
OkHttp3在請求處理上,採用了攔截器鏈的模式來處理請求,攔截器鏈中,負責經過http請求調用服務方,而後將結果返回。服務器
OkHttp3的核心是攔截器鏈,經過攔截器鏈,處理Http請求:網絡
RetryAndFollowUpInterceptor
,重試和重定向攔截器,主要做用是根據請求的信息,建立StreamAllocation
和Address
實例;BridgeInterceptor
請求橋接攔截器,主要是處理Http請求的Header頭部信息,處理Http請求壓縮和解析;CacheInterceptor
緩存攔截器,此攔截器藉助於Http協議的客戶端緩存定義,模擬瀏覽器的行爲,對接口內容提供緩存機制,提升客戶端的性能;ConnectInterceptor
鏈接攔截器,負責根據配置信息,分配一個Connection實例對象,用於TCP/IP通訊。CallServerInterceptor
調用服務端攔截器,該攔截器負責向Server發送Http請求報文,並解析報文。CallServerInterceptor
攔截器底層使用了高性能的okio
(okhttp io components)子組件完成請求流的發送和返回流的解析。架構
做爲攔截器鏈的展開,下圖展現了OKHttp3的核心部件及其關係:併發
上述架構圖中,有以下幾個概念:
StreamAllocation
當一個請求發起時,會爲該請求建立一個StreamAllocation
實例來表示其整個生命週期;Call
該對象封裝了對某一個Http請求,相似於command命令模式;Request
,Response
當Call
被執行時,會轉換成Request對象, 執行結束以後,經過Response
對象返回表示HttpCodec
處理上述的Request
和Response
,將數據基於Http協議解析轉換Stream
這一層是okio
高性能層進行io轉換處理,聚焦於Source
和 Sink
的處理Address
okhttp3對於調用服務的地址封裝,好比www.baidu.com
則表示的百度服務的AddressRoute
框架會對Address判斷是否DNS解析,若是解析,一個Address
可能多個IP,每個IP被封裝成Route
RouteSelector
當存在多Route
的狀況下,須要定義策略選擇Route
Connection
表示的是Http請求對應的一個佔用Connection
,Connection
的分配時經過Connnection Pool
獲取Connection Pool
維護框架的鏈接池
OKHttp3對網絡鏈接過程當中,涉及到的幾種概念:
http(s)://<domain-or-ip>:<port>/path/to/service
,其中<domain-or-ip>
則表示的是服務器的地址 Adress <domain-or-ip>
,表示服務的域名或者IP鏈接池
中, OKHttp的鏈接池比較特殊,詳情參考後續章節。鏈接池經過最大閒置鏈接數(maxIdleConnections)和保持存活時間(keepAliveDuration)來控制鏈接池中鏈接的數量。
在鏈接池的內部,會維護一個守護線程,當每次往線程池中添加新的鏈接時,將會觸發異步清理閒置鏈接任務。
private final Runnable cleanupRunnable = new Runnable() { @Override public void run() { while (true) { //執行清空操做,返回下次執行清空的時間 long waitNanos = cleanup(System.nanoTime()); if (waitNanos == -1) return; if (waitNanos > 0) { long waitMillis = waitNanos / 1000000L; waitNanos -= (waitMillis * 1000000L); //將當前清理線程睡眠指定的時間片後再喚醒 synchronized (ConnectionPool.this) { try { ConnectionPool.this.wait(waitMillis, (int) waitNanos); } catch (InterruptedException ignored) { } } } } } }; /** * Performs maintenance on this pool, evicting the connection that has been idle the longest if * either it has exceeded the keep alive limit or the idle connections limit. * * <p>Returns the duration in nanos to sleep until the next scheduled call to this method. Returns * -1 if no further cleanups are required. */ long cleanup(long now) { int inUseConnectionCount = 0; int idleConnectionCount = 0; RealConnection longestIdleConnection = null; long longestIdleDurationNs = Long.MIN_VALUE; // Find either a connection to evict, or the time that the next eviction is due. synchronized (this) { //遍歷鏈接池中的每一個鏈接 for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) { RealConnection connection = i.next(); // If the connection is in use, keep searching. if (pruneAndGetAllocationCount(connection, now) > 0) { inUseConnectionCount++; continue; } idleConnectionCount++; // If the connection is ready to be evicted, we're done. long idleDurationNs = now - connection.idleAtNanos; //計算鏈接的累計閒置時間,統計最長的閒置時間 if (idleDurationNs > longestIdleDurationNs) { longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; } } //若是閒置時間超過了保留限額 或者閒置鏈接數超過了最大閒置鏈接數值 if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) { // We've found a connection to evict. Remove it from the list, then close it below (outside // of the synchronized block). //從鏈接池中剔除當前鏈接 connections.remove(longestIdleConnection); } else if (idleConnectionCount > 0) { // 若是未達到限額,返回移除時間點 // A connection will be ready to evict soon. return keepAliveDurationNs - longestIdleDurationNs; } else if (inUseConnectionCount > 0) { // All connections are in use. It'll be at least the keep alive duration 'til we run again. // 都在使用中,沒有被清理的,則返回保持存活時間 return keepAliveDurationNs; } else { // No connections, idle or in use. cleanupRunning = false; return -1; } } closeQuietly(longestIdleConnection.socket()); // Cleanup again immediately. return 0; }
默認狀況下:
鏈接池(Connection Pool)
的工做原理:
- 當某一個Http請求結束後,對應的Connection實例將會標識成idle狀態,而後鏈接池會立馬判斷當前
鏈接池
中的處於idle狀態的Connection實例
是否已經超過 maxIdleConnections 閾值,若是超過,則此Connection實例
將會被釋放,即對應的TCP/ IP Socket通訊也會被關閉。- 鏈接池內部有一個異步線程,會檢查
鏈接池
中處於idle實例的時長,若是Connection實例
時長超過了keepAliveDuration
,則此Connection實例
將會被剔除,即對應的TCP/ IP Socket通訊也會被關閉。
對於瞬時併發很高的狀況下,okhttp鏈接池中的TCP/IP鏈接將會衝的很高,可能和併發數量基本一致。可是,當http請求處理完成以後,鏈接池會根據maxIdleConnections
來保留Connection實例
數量。maxIdleConnections
的設置,應當根據實際場景請求頻次來定,才能發揮最大的性能。
假設咱們的鏈接池配置是默認配置,即:最大閒置鏈接數(maxIdleConnections):5,保持存活時間(keepAliveDuration):5(mins);
當前瞬時併發有100
個線程同時請求,那麼,在okhttp內建立100
個 tcp/ip鏈接,假設這100
個線程在1s
內所有完成,那麼鏈接池
內只有5
個tcp/ip鏈接
,其他的都將釋放;在下一波50
個併發請求過來時,鏈接池只有5
個能夠複用,剩下的95
個將會從新建立tcp/ip鏈接
,對於這種併發能力較高的場景下,最大閒置鏈接數(maxIdleConnections)
的設置就不太合適,這樣鏈接池的利用率只有5 /50 *100% = 10%,因此這種模式下,okhttp的性能並不高。
因此,綜上所述,能夠簡單地衡量鏈接池的指標:
鏈接池的利用率 = maxIdleConnections / 系統平均併發數
說明:根據上述公式能夠看出,利用率越高,maxIdleConnections
和系統平均併發數
這兩個值就越接近,即:maxIdleConnections
應當儘量和系統平均併發數
相等。
Spring cloud在對這個初始化的過程比較開放,默認的大小是200
,具體的指定關係和其實現關係。
package org.springframework.cloud.commons.httpclient; import okhttp3.ConnectionPool; import java.util.concurrent.TimeUnit; /** * Default implementation of {@link OkHttpClientConnectionPoolFactory}. * @author Ryan Baxter */ public class DefaultOkHttpClientConnectionPoolFactory implements OkHttpClientConnectionPoolFactory { @Override public ConnectionPool create(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) { return new ConnectionPool(maxIdleConnections, keepAliveDuration, timeUnit); } }
在設置上,共有兩個地方能夠指定鏈接參數:
maxTotalConnections
值,默認爲 :200
;getMaxConnections
值,默認爲:200
ribbon.okhttp.enabled
開啓配置):@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); //使用了ribbon的 maxTotalConnections做爲idle數量,ribbon默認值爲200 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) .connectTimeout(ribbon.connectTimeout(), TimeUnit.MILLISECONDS) .readTimeout(ribbon.readTimeout(), TimeUnit.MILLISECONDS) .followRedirects(ribbon.isFollowRedirects()) .connectionPool(connectionPool) .build(); return this.httpClient; } } }
feign.okhttp.enabled
參數開啓)@Configuration @ConditionalOnClass(OkHttpClient.class) @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer") @ConditionalOnMissingBean(okhttp3.OkHttpClient.class) @ConditionalOnProperty(value = "feign.okhttp.enabled") 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(); Boolean disableSslValidation = httpClientProperties.isDisableSslValidation(); this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation). 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(okhttp3.OkHttpClient client) { return new OkHttpClient(client); } }