Spring Cloud OkHttp設計原理

Spring Cloud 框架最底層核心的組件就是服務調用方式,通常Spring Cloud框架採用的是HTTP的調用框架,本文將在 Spring Cloud應用場景下,介紹組件OkHttp3的設計原理。html

1. Spring Cloud的接口調用工做模式

Spring Cloud接口調用基本工做方式

Spring Cloud做爲組合式的分佈式微服務解決方案,再服務調用上,至少須要解決以下幾個環節:java

  • 面向接口的編程形式
    接口調用過程,除了拼裝Http請求外,爲了提升接口調用的無感性,在這個環節上,目前採用的是Feign工具完成的。至於feign的工做原理,請參考個人另外一篇博文:

    客戶端負載均衡Feign之三:Feign設計原理

  • 服務負載均衡和選擇機制
    做爲分佈式調用框架,服務消費方須要經過必定的機制知道應當調用某一特定服務提供方實例,Spring Cloud 目前採用的是 Ribbon來完成的。至於Ribbon的工做原理,請參考個人另外一篇博文:
    Spring Cloud Ribbon設計原理.
  • 做爲http 客戶端,向服務器發起Http請求
    Http客戶端在Java語言中,目前比較流行的有 Apache HttpClients components,HttpUrlConnection,OkHttp等,OkHttp 在性能、體積各方面表現比較好,採用此框架做爲http 客戶端是一個不錯的選擇。本文將深刻OkHttp的底層設計原理,經過分析整理出它的最佳打開方式。

2. 什麼是OkHttp,它有什麼特色?

OkHttp是square公司開發的一個同時支持Http和Http2協議的Java客戶端,可用於Android和Java應用中。
OKHttp有以下幾個特性:spring

  • 支持Http1.一、SPDY,和Http2
  • 內部採用鏈接池機制,可以緩存和複用Tcp/IP鏈接,減小請求延遲。
  • 支持GZIP格式壓縮,減小數據傳輸大小
  • 對重複請求返回結果進行緩存,減小交互次數
  • OKHttp底層採用DNS反解析,當其中一個實例不可用時,會自動切換至下一個服務,有較好的鏈接管理能力。
  • OkHttp支持最新的TLS特性(TLS 1.3, ALPN, certificate pinning)
  • 同時支持同步調用和異步調用兩種方式

3. Okhttp3的設計原理

本章節將詳細介紹OkHttp3底層的設計原理,並結合設計原理,總結在使用過程當中應當注意的事項。編程

3.1 Ohttp3的的基本工做流程

以以下的簡單交互代碼爲例,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請求調用服務方,而後將結果返回。服務器

3.2 okHttp3的攔截器鏈

 

OkHttp3的核心是攔截器鏈,經過攔截器鏈,處理Http請求:網絡

  • RetryAndFollowUpInterceptor,重試和重定向攔截器,主要做用是根據請求的信息,建立StreamAllocationAddress實例;
  • BridgeInterceptor 請求橋接攔截器,主要是處理Http請求的Header頭部信息,處理Http請求壓縮和解析;
  • CacheInterceptor 緩存攔截器,此攔截器藉助於Http協議的客戶端緩存定義,模擬瀏覽器的行爲,對接口內容提供緩存機制,提升客戶端的性能;
  • ConnectInterceptor 鏈接攔截器,負責根據配置信息,分配一個Connection實例對象,用於TCP/IP通訊。
  • CallServerInterceptor 調用服務端攔截器,該攔截器負責向Server發送Http請求報文,並解析報文。

CallServerInterceptor攔截器底層使用了高性能的okio(okhttp io components)子組件完成請求流的發送和返回流的解析。架構

3.3 OkHttp3的內部核心架構關係

做爲攔截器鏈的展開,下圖展現了OKHttp3的核心部件及其關係:併發

上述架構圖中,有以下幾個概念:

  • StreamAllocation 當一個請求發起時,會爲該請求建立一個StreamAllocation實例來表示其整個生命週期;
  • Call 該對象封裝了對某一個Http請求,相似於command命令模式;
  • RequestResponseCall被執行時,會轉換成Request對象, 執行結束以後,經過Response對象返回表示
  • HttpCodec 處理上述的RequestResponse,將數據基於Http協議解析轉換
  • Stream 這一層是okio高性能層進行io轉換處理,聚焦於SourceSink的處理
  • Address okhttp3對於調用服務的地址封裝,好比www.baidu.com則表示的百度服務的Address
  • Route 框架會對Address判斷是否DNS解析,若是解析,一個Address可能多個IP,每個IP被封裝成Route
  • RouteSelector 當存在多Route的狀況下,須要定義策略選擇Route
  • Connection 表示的是Http請求對應的一個佔用ConnectionConnection的分配時經過Connnection Pool獲取
  • Connection Pool 維護框架的鏈接池

3.4 OKhttp3的網絡鏈接的抽象


 

 

 

 

OKHttp3對網絡鏈接過程當中,涉及到的幾種概念:

  • 請求URL:OKHttp3 是處理URL請求的HTTP請求的基礎,URL的格式遵循標準的HTTP協議。對於某個HTTP服務器而言,會提供多個URL地址連接。URL協議中,基本格式爲http(s)://<domain-or-ip>:<port>/path/to/service,其中<domain-or-ip>則表示的是服務器的地址 Adress
  • Address(地址): 即上述的<domain-or-ip>,表示服務的域名或者IP
  • Route (路由) :當URL中的<domain-or-ip>是domain時,表示的是服務的域名,而域名經過DNS解析時,可能會解析出多個IP,也就是說一個Address能夠映射到多個Route,一個Route 表示的是一個機器IP,用於創建TCP/IP網絡鏈接
  • Connection:Connection表示的是一個Socket鏈接通訊實例
  • Connection Pool: 對於Connection實例,統一維護在鏈接池中, OKHttp的鏈接池比較特殊,詳情參考後續章節。

3.5 鏈接池的工做原理


在OKHttp3內部使用了雙端隊列管理鏈接池,也就是說 鏈接池沒有數量的限制
那既鏈接數量的限制,OKHttp3是怎麼保證隊列內存不溢出呢?
3.5.1 鏈接池的鏈接清空機制

鏈接池經過最大閒置鏈接數(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;
  }

 

默認狀況下:

  • 最大閒置鏈接數(maxIdleConnections):5
  • 保持存活時間(keepAliveDuration):5(mins)

鏈接池(Connection Pool)的工做原理

  1. 當某一個Http請求結束後,對應的Connection實例將會標識成idle狀態,而後鏈接池會立馬判斷當前鏈接池中的處於idle狀態的Connection實例是否已經超過 maxIdleConnections 閾值,若是超過,則此Connection實例 將會被釋放,即對應的TCP/ IP Socket通訊也會被關閉。
  2. 鏈接池內部有一個異步線程,會檢查鏈接池中處於idle實例的時長,若是Connection實例時長超過了keepAliveDuration,則此Connection實例將會被剔除,即對應的TCP/ IP Socket通訊也會被關閉。
3.5.2 鏈接池使用注意事項

對於瞬時併發很高的狀況下,okhttp鏈接池中的TCP/IP鏈接將會衝的很高,可能和併發數量基本一致。可是,當http請求處理完成以後,鏈接池會根據maxIdleConnections來保留Connection實例數量。maxIdleConnections的設置,應當根據實際場景請求頻次來定,才能發揮最大的性能。

假設咱們的鏈接池配置是默認配置,即:最大閒置鏈接數(maxIdleConnections):5,保持存活時間(keepAliveDuration):5(mins);
當前瞬時併發有100個線程同時請求,那麼,在okhttp內建立100個 tcp/ip鏈接,假設這100個線程在1s內所有完成,那麼鏈接池內只有5tcp/ip鏈接,其他的都將釋放;在下一波50個併發請求過來時,鏈接池只有5個能夠複用,剩下的95個將會從新建立tcp/ip鏈接,對於這種併發能力較高的場景下,最大閒置鏈接數(maxIdleConnections)的設置就不太合適,這樣鏈接池的利用率只有5 /50 *100% = 10%,因此這種模式下,okhttp的性能並不高。
因此,綜上所述,能夠簡單地衡量鏈接池的指標:

鏈接池的利用率 = maxIdleConnections / 系統平均併發數
說明:根據上述公式能夠看出,利用率越高, maxIdleConnections系統平均併發數 這兩個值就越接近,即:maxIdleConnections 應當儘量和系統平均併發數相等。

3.6 spring cloud對鏈接池的設置

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

在設置上,共有兩個地方能夠指定鏈接參數:

  • 基於ribbon的 maxTotalConnections值,默認爲 :200;
  • 基於feign的 getMaxConnections 值,默認爲:200
3.6.1 基於ribbon和okhttp的配置(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;
        }
    }
}
3.6.2 基於feign的OKHttp配置(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);
        }
    }
相關文章
相關標籤/搜索