OkHttp作为常用的网络通讯组件,其中大部分的功能点需要我们深入了解,本系列的文章将以源码角度解析组件背后的运行原理,避免踩坑。

本篇幅通过源码角度解释一些使用不当导致的”血案“。Okhttp使用连接池复用连接,在HTTP1.1的版本复用的连接尤为重要,它避免了每次创建TCP连接的开销。如果你不熟悉Okhttp的连接池,在大量请求的情况下,它会拖慢你的性能,严重时会导致一段时间机器无端口个可用。
一、先看现象,线上请求并发量上来后,大量的端口TIME_WAIT。请求创建连接无法申请到端口,等待操作系统释放端口(net.ipv4.tcp_fin_timeout 默认60s)。
捕获到大量异常:Cannot assign requested address
排查过程:
- 通过 netstat | grep 'WAIT' |wc -l 观察占用端口状态,发现占用的端口大于2个。
- 查看系统端口回收时间 /sbin/sysctl -a | grep 'fin'
- 通过调整 操作系统回收端口时间net.ipv4.tcp_fin_timeout=30,问题明显改善。

二、怀疑okhttp每次请求都是短链接,所以我们review一下代码
第一我们的请求带有keep-alive头
request.newBuilder().addHeader("Connection", "keep-alive")
第二Okhttp默认会加上Keep-Alive,使用长链接,这段代码在【BridgeInterceptor.kt】
class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
// 省略部分代码
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive")
}
// 省略部分代码
}
}
三、通过tcpdump+WireShark观察线上流量,dump出来的请求也证实了之前的代码没问题
// 请求头header:
POST url HTTP/1.1
Content-Type: application/json;charset=UTF-8
Content-Length: 269
Host: 127.0.0.1:30002
Connection: Keep-Alive
Accept-Encoding: gzip
// 响应头 header:
HTTP/1.1 200 OK
Content-Length: 109
Content-Type: application/json;charset=UTF-8
Date: Tue, 09 Nov 2021 07:59:43 GMT
另外通过dump观察到一个连接只处理了几个HTTP请求,客户端主动FIN关闭了链接

从这里开始我们觉得OkHttp的连接池应该有问题。
四、原因解析
首先我们需要了解他的连接池

复用连接的细节

何时复用连接
- 连接池中存在空闲的连接(calls引用=0,http1.1最多允许一个calls)
- 连接的是健康的
- 复用的连接和当前的请求的host、port等请求信息相同
连接池的参数
- 常驻的存活连接数量(maxIdleConnections),默认5个
- 连接存活的时间(keepAliveDuration,timeUnit),默认5分钟
何时触发释放连接
- 新的连接创建时
- 一个请求完成的时候
- 复用RealCall的引用连接不健康时
这种故障的问题在线上多服务的场景尤为明细,通过okhttp实现请求,服务的调用是负载均衡的,通常需要请求多个服务,这个也是问题触发的根据原因。以下通过示意图解释为什么会发生该问题。

A连接与F连接不符合连接复用的条件(host不相同),所以被A连接被回收销毁。如果这时候再创建一个G连接,B连接会被销毁掉,所以这个就是问题所在,我们不能直接使用默认的连接池参数
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
其中maxIdleConnections参数的大小应该为:
maxIdleConnections = 请求服务的Host个数 * 请求的服务个数
例如我的应用需要请求3个服务,3个服务存在6个实例(例子host以ip为例)那数量应该为18个;
结尾
至此,我们找到了具体原因,OkHttp众多构造参数都为我们提供了默认值,一个很小的参数都会导致我们的服务性能下降。
后面我们将深入分析OkHttp 4.10.0版本连接的创建、复用以及销毁,感兴趣的同学欢迎继续关注!
-END-