Spring Cloud OkHttp設計原理


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

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

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

Spring Cloud作為組合式的分布式微服務解決方案,再服務調用上,至少需要解決如下幾個環節:

  • 面向接口的編程形式
    接口調用過程,除了拼裝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有如下幾個特性:

  • 支持Http1.1、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);
        }
    }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM