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

Spring Cloud OkHttp设计原理的更多相关文章

  1. Spring Cloud Feign设计原理

    什么是Feign? Feign 的英文表意为“假装,伪装,变形”, 是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直 ...

  2. 撸一撸Spring Cloud Ribbon的原理-负载均衡器

    在上一篇<撸一撸Spring Cloud Ribbon的原理>中整理发现,RestTemplate内部调用负载均衡拦截器,拦截器内最终是调用了负载均衡器来选择服务实例. 接下来撸一撸负载均 ...

  3. 撸一撸Spring Cloud Ribbon的原理-负载均衡策略

    在前两篇<撸一撸Spring Cloud Ribbon的原理>,<撸一撸Spring Cloud Ribbon的原理-负载均衡器>中,整理了Ribbon如何通过负载均衡拦截器植 ...

  4. Eureka 系列(03)Spring Cloud 自动装配原理

    Eureka 系列(03)Spring Cloud 自动装配原理 [TOC] 0. Spring Cloud 系列目录 - Eureka 篇 本文主要是分析 Spring Cloud 是如何整合 Eu ...

  5. Spring技术内幕——深入解析Spring架构与设计原理(一)IOC实现原理

    IOC的基础 下面我们从IOC/AOP开始,它们是Spring平台实现的核心部分:虽然,我们一开始大多只是在这个层面上,做一些配置和外部特性的使用工作,但对这两个核心模块工作原理和运作机制的理解,对深 ...

  6. Spring Could Feign 设计原理

    什么是Feign? Feign 的英文表意为"假装,伪装,变形", 是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HT ...

  7. 撸一撸Spring Cloud Ribbon的原理

    说起负载均衡一般都会想到服务端的负载均衡,常用产品包括LBS硬件或云服务.Nginx等,都是耳熟能详的产品. 而Spring Cloud提供了让服务调用端具备负载均衡能力的Ribbon,通过和Eure ...

  8. Spring Cloud OpenFeign的原理(六)

    通过上篇我们了解OpenFeign他也可以完成远程通信,但是它并不是真正义意上的RPC通信,因为他是通过封装代理来实现的,下面和以前一样,知道了怎么用就来看下他是怎么实现的. 一.思考Feign要做的 ...

  9. Spring Cloud个组件原理

    引言 面试中面试官喜欢问组件的实现原理,尤其是常用技术,我们平时使用了SpringCloud还需要了解它的实现原理,这样不仅起到举一反三的作用,还能帮助轻松应对各种问题及有针对的进行扩展.以下是 课程 ...

随机推荐

  1. Lucene PriorityQueue & JDK PriorityQueue

    麻蛋,原来是最小堆呀!  数据结构不熟害死人呀! 看来待复习复习数据结构了 在lucene源码中对多个段合并的时候,会先将多个段放到一个PriorityQueue中,不要被这个名字迷惑,这个Prior ...

  2. Java开发环境系列:一篇能解决你99%问题的排雷日记

      安装 https://archive.apache.org/dist/tomcat/ 推荐使用免安装版的Tomcat(放在没有中文和空格的目录下),前提是已经安装了JDK并配置了环境变量.Linu ...

  3. Linux自有服务(1)-Linux从入门到精通第五天(非原创)

    文章大纲 一.运行模式二.用户与用户组管理(重点)三.网络设置四.ssh服务(重点)五.学习资料下载六.参考文章   自有服务,即不需要用户独立去安装的软件的服务,而是当系统安装好之后就可以直接使用的 ...

  4. notepad++ 设置运行python脚本

    按F5 在输入框中输入: cmd /k python “$(FULL_CURRENT_PATH)” &PAUSE & EXIT python路径必须在环境变量中. 否则需要输入完整的p ...

  5. windows下使用ssh(利用paramiko库)

    环境:python3.7.3 win7 or win10 1.首先下载paramiko库 命令:pip install paramiko 2.代码: import paramiko 创建一个 ssh ...

  6. 大众点评评论数据抓取 反爬虫措施有css文字映射和字体库反爬虫

    大众点评评论数据抓取  反爬虫措施有css文字映射和字体库反爬虫 大众点评的反爬虫手段有那些: 封ip,封账号,字体库反爬虫,css文字映射,图形滑动验证码 这个图片是滑动验证码,访问频率高的话,会出 ...

  7. iOS-快速开发直播APP所需第三方SDK

    直播SDK 金山云, 推荐 七牛云, 推荐 阿里云(收费) 网易云(收费) 腾讯云(收费) 又拍云 播放SDK IJKPlayer 自定义IJKPlayer,进度条.音量.亮度 短视频SDK 七牛云( ...

  8. The run destination "设备名称" is not valid for Running the scheme '项目名称'.

    之前运行好好的,怎么会突然出现这个呢?开始百度发现都解决不了. 最后发现,这是XCode的一个bug.遇到这种情况只要Command+Q,退去XCode,然后再重新登入即可.

  9. LCD编程_LCD控制器

    CLKVAL : VCLK = HCLK / [(CLKVAL+1) x 2]--------> CLKVAL = HCLK/VCLK/2-1 在这个地方HCLK=100M,那么VLCK等于多少 ...

  10. C#中Equals 与== 的区别

    这个问题听说是大公司面试都会问的问题,以前不怎么了解,好奇心勾引我来研究一下 首先从值类型分析,先写几句简单的代码供测试用,二行语句输出的都是true, 说明==与Equals功能是相同的, 判断的都 ...