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