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);
        }
    }
相关文章
相关标签/搜索