OkHttpClient3 连接池模型
OkHttp 的连接池与 JDBC 连接池有所不同。JDBC 的连接池往往面向单一目标服务器,而 OkHttp 中,多数情况下 HTTP 请求需要面向多台不同的服务器,因此 OkHttp 的连接池需要面向多目标。
获取连接
StreamAllocation#findConnections()
方法用于获取一个连接(RealConnection,本文简称 cnn),cnn 可能来自线程池,创建新连接。
HTTP 2 连接复用
OkHttp 的连接复用与连接的创建和回收过程关联紧密,并且还涉及到了一个大角色——
StreamAllocation
HTTP 2.0 中,一个连接可以同时发送多个请求。所以 OkHttp 需要解决:如何让一个连接被多个线程使用,同时要确保线程安全,以及准确及时的回收空闲连接。
cnn 的复用是通过 StreamAllocation
实现的。StreamAllocation
可以看作是 cnn 的一个“分身”,一个 cnn 拥有众多的 StreamAllocation
。用户使用连接的时,会获取 StreamAllocation
,从而获取它背后真正的 cnn与服务器通讯。
下面从 Java 层面展示了两者的关系。
1 | /* RealConnection class */ |
一个 cnn 拥有一组 StreamAllocation
软连接集合, StreamAllocation
持有唯一一个 cnn。当用户获取一个连接时,会向特定的一个 StreamAllocation
发出申请,然后返回一个真正当连接。多个线程间可能会从不同的 StreamAllocation
获取同一个 cnn。一个线程持有一个唯一 StreamAllocation
。
这里的用户是广义的,泛指连接的需求方,通常是一个线程。而并非真实的一个用户。
连接销毁
我们采用“倒叙”的方式铺开连接池的管理,从销毁讲起,后面还包括:连接的获取、连接的创建和线程安全保障
cnn 维护在 ConnectionPool#connections
中,数据结构如下:
1 | public final class ConnectionPool { |
完成这个清理工作的,是一个线程,它的处理方式很简单:
connection pool 中没有任何空闲连接时,线程关闭
connection pool 中没有待清理的连接时,线程等待(waiting,默认等待 5 min)
connection pool 中存在需要清理的连接时,执行清理任务
当有新的 cnn 加入到 connection pool 时,清理线程开启。
1 | public final class ConnectionPool { |
连接池清理工作包括:寻找无效连接和销毁无效连接,空闲连接的判断是第一步的核心。
cnn 维护了一个 StreamAllocation
软连接集合用于对连接的使用情况进行追踪与计数。当没有任何线程使用 cnn 时,无法从集合中获取未被回收的 StreamAllocation
对象 。通过这样当方式,我们可以轻松的判断:”当前连接是否是处于空闲状态?“。
OkHttp 是允许部分空闲连接的存在的,只有超过最大空闲连接数量(maxIdleConnections)或者空闲时间过长的连接(keepAliveDurationNs),才被定义为“无效连接“,然后被清理线程销毁。
如同我们猜想的一样,销毁的最后一步,一定是将 cnn 从 connections
移除,并关闭 socket。
1 | public final class ConnectionPool { |
结束了上面过程,简单梳理一下。清理连接的整体思路是:通过 cleanupRunnable
线程来执行清理任务,通过线程等待的方式不断的执行。StreamAllocation
软连接集合的引入,追踪了连接的被使用情况,解决了“空闲连接”定义的问题。该思路与 gc 回收算法中的“引用计数”算法大致相同。清理线程将无效的连接销毁,完成清理任务。
获取连接 & 创建连接
获取连接的过程包含了连接的创建。获取连接最初是由 ConnectInterceptor
拦截器发起的。拦截器模式是 OkHttp 整体流程的主干,贯穿了整体请求与响应流程,OkHttpClient3 架构简介 包含了 OkHttp 拦截器在整体架构中的地位与应用。
1 | /* ConnectInterceptor class */ |
OkHttp 优先尝试从 ConnectionPool
中获取连接,获取成功后计数加 1;如果获取失败,会创建一个新的连接,并将新连接加入到 connections 中,计数加 1。
计数过程是把 StreamAllocation
对象加入到 cnn 的“影子”集合中,StreamAllocation#acquire()
完成了这项工作。
获取连接
获取 cnn 就是遍历 connections ,找到一个“合适”的连接返回。OkHttp 要求 HTTP 1.x 中,一个 cnn 最多拥有 1 个 Stream,HTTP 2.0 中可以拥有多个(默认为 Integer.MAX_VALUE)。合适连接要求如下:
- steam 没有达到创建的上限
- host 相同
计数加 1,并将合适的 cnn 成功返回给用户,完成 HTTP 请求。
这里合适连接的要求,仅停留在 HTTP 1.x,HTTP 2.0 获取连接做了一些协议上的处理,不是本篇重点,这里不详述。
创建连接
当从 ConnectionPool
获取连接失败后,会选择创建新的连接。并将新的连接加入到 connections
中,计数加 1。
1 | public final class StreamAllocation { |
连接的获取与创建过程,通过创建对象,并将新创建的对象加入到连接池中,计数加 1。本质上来看是连接池的连接容器(Deque<RealConnection>
)的 add()
和计数器计数。
线程安全
本节所介绍的线程安全仅仅是有关连接池的线程安全。更多线程模型介绍在 OkHttpClient3 线程模型。
线程安全的核心是解决 共享资源在竞争条件下的状态不确定 问题。解决过程中的关键一步在于:找出可能处于竞争条件的共享变量。
其中,被 final
修饰的基础变量、Unmofied
容器、无访问通路的私有变量和局部变量,任何情况下都处于非竞争条件。更多关于线程的介绍在 操作系统的线程管理。
竞争条件(Race Condition, 又称竞太条件):多个进程读写共享资源,最终的结果取决于进程运行的一个精确的时序,这样的情形称之为竞争条件,例如:缓冲区的并发访问问题。
Connection Pool 的共享变量
ConnectionPool
作为 OkHttpClient
的一个 final
成员,随着 OkHttpClient
的创建而创建。一个 OkHttpClient
持有唯一一个 ConnectionPool
,即 ConnectionPool
相对于 OkHttpClient
是单例模式。
OkHttp 的连接池类中,成员信息如下:
1 | public final class ConnectionPool { |
其中,共享变量有三个:
connections
:缓存连接,在线程创建、获取、销毁时使用routeDatabase
:缓存失败的链路,监视器类cleanupRunning
:清理线程开启的标志
Connctons
OkHttp 在任何地方都使用了对象锁来保证访问 connections 的正确性,由于 ConnetionPool
对于 OkHttpClient
而言是单例,对象锁可以解决并发冲突。
RouteDataBase
一个 HTTP URL 根据 DNS 解析的结果往往会存在多条 HTTP 链路(Route),例如:
1 | 1.1.1.1 www.abc.com |
OkHttp 维护来一个失败链路的黑明单,用于记录和规避连接失败的场景,提升连接的成功率。为了保证并发安全,OkHttp 采用了监视器模式实现该类。
cleanupRunning
cleanupRuning 变量只在 put()
和 cleanup()
方法中用到,这两方法均使用了对象锁来保证。
Tick, Tick
本篇文档偏重于讲解 OkHttp 线程模型的“骨骼”,其中的细节没有过多的涉及。目的是了解连接池的核心——安全高效的获取连接和回收连接,这是最有“营养”的部分。