Spring Cloud内核Spring Cloud SpringCloud

Spring Cloud OkHttp设计原理

2019-05-22  本文已影响95人  亦山札记

Spring Cloud 框架最底层核心的组件就是服务调用方式,一般Spring Cloud框架采用的是HTTP的调用框架,本文将在 Spring Cloud应用场景下,介绍组件OkHttp3的设计原理。

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

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

Spring Cloud作为组合式的分布式微服务解决方案,再服务调用上,至少需要解决如下几个环节:

2. 什么是OkHttp,它有什么特点?

OkHttp是square公司开发的一个同时支持Http和Http2协议的Java客户端,可用于Android和Java应用中。
OKHttp有如下几个特性:

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

根据上述的流程,其内部请求主要主体如下所示:


2019-05-21_231457.png

OkHttp3在请求处理上,采用了拦截器链的模式来处理请求,拦截器链中,负责通过http请求调用服务方,然后将结果返回。

3.2 okHttp3的拦截器链

2019-05-21_233339.png

OkHttp3的核心是拦截器链,通过拦截器链,处理Http请求:

CallServerInterceptor拦截器底层使用了高性能的okio(okhttp io components)子组件完成请求流的发送和返回流的解析。

3.3 OkHttp3的内部核心架构关系

作为拦截器链的展开,下图展示了OKHttp3的核心部件及其关系:


2019-05-21_232649.png

上述架构图中,有如下几个概念:

3.4 OKhttp3的网络连接的抽象

image.png

OKHttp3对网络连接过程中,涉及到的几种概念:

3.5 连接池的工作原理

image.png
在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;
  }

默认情况下:

连接池(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);
    }
}

在设置上,共有两个地方可以指定连接参数:

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

4.总结

以上关于OkHttp3的论述,仅代表个人观点,作者水平有限,如有错误,欢迎批评指正。

上一篇 下一篇

猜你喜欢

热点阅读