1. 问题背景
- OkHttp版本:3.14.9
- 问题描述
在做网络请求优化过程中,首先根据耗时分析,发现接口在建立连接进行握手阶段耗时比较久,且每次都要进行建连过程。我通过修复实际项目中OKHttp的Keep-Alive失效问题,让短时间内(keep-alive有一个默认的timeout时间,详情可以看ConnectionPool类的构造方法)对同一域名的网络请求连复用,对于降低网络数据刷新延迟效果明显,详情可以看:OkHttp请求时Keep-Alive无法生效问题修复记录
但增加了"Connection":“Keep-Alive”之后,测试反馈有时候会遇到无网络的错误toast提示,然后再手动触发一次网络请求就能成功,随后抓取线上网络请求埋点,确实存在大量类似的错误。
2. bug触发点定位
问题发生在网路连接阶段,可以直接跟踪网络连接拦截器CallServerInterceptor:
2.1 okhttp3.internal.http.CallServerInterceptor#intercept
@Override public Response intercept(Chain chain) throws IOException {
...
responseBuilder = exchange.readResponseHeaders(true); //(1)
...
}
2.2 okhttp3.internal.http1.Http1ExchangeCodec#readResponseHeaders
- exchange.readResponseHeaders()最后会转调到Http1ExchangeCodec.readResponseHeaders()方法,关键代码如下:
//okhttp3.internal.http1.Http1ExchangeCodec#readResponseHeaders():
@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
...
try {
StatusLine statusLine = StatusLine.parse(readHeaderLine());
Response.Builder responseBuilder = new Response.Builder()
.protocol(statusLine.protocol)
.code(statusLine.code)
.message(statusLine.message)
.headers(readHeaders());
...
return responseBuilder;
} catch (EOFException e) {
...
//(1)找到了抛出发生错误的位置
throw new IOException("unexpected end of stream on "
+ address, e);
}
}
- (1)ExchangeCodec有两个实现类,分别是Http1ExchangeCodec和Http2ExchangeCodec,这里直接在两个实现类中搜索报错字符串,发现只有Http1ExchangeCodec实现类中存在该异常抛出逻辑。
- (2)通过跟进源码可以知道Http2ExchangeCodec是给HTTP2使用的,发生该错误的http请求是HTTP1.1,从这个线索也可以直接看Http2ExchangeCodec这个实现类。
- (3)Http1.1协议本身是支持链接复用的,同一个服务ip的tcp链接会在给定的Keep-Alive保持超时时间内复用,不用每次都重建连接。
2.23 判断keep-alive超时逻辑
首先是连接复用问题,可以聚焦到OkHttp的连接池ConnectionPool上,而ConnectionPool的实现类是RealConnectionPool,通过跟进连接池中Connection的放出和移出逻辑发现判断时机在cleanup()方法中:
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) {
//(1)
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;
}
}
//(2)
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
- 上述是连接池的维护逻辑,如果超过保持连接超时或空闲连接限制,则移除空闲时间最长的连接。
- 虽然有超过链接复用的超时时间移除连接池逻辑,但是如果客户端不去请求并不能知道服务端已经单方面断开连接了,所以需要针对此类情况做兼容处理,当发现连接失败时触发重连等。
- 触发判断移除连接的逻辑是在每次建立链接,将链接put进ConnectionPool时先触发一次cleanup逻辑
- 将不符合继续缓存的连接移除后会同步阻塞的进行连接关闭逻辑
配置Keep-Alive超时的位置:
在构建OKHttpClient时,Builder有开放connectPool()接口让使用方自己配置:
举例:
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.readTimeout(1000, TimeUnit.SECONDS)
.writeTimeout(1000, TimeUnit.SECONDS)
//配置自定义连接池参数
.connectionPool(new ConnectionPool(5, 60, TimeUnit.SECONDS))
.build();
3. 本地还原现场
为了验证该问题,抛开实际项目中的额外逻辑(OkHttp的客制化逻辑等),我们采用本地模拟该条件进行还原,摸索是否能解决该问题。
- 服务端:apache-tomcat-7.0.73
- OkHttp版本:3.14.9
4. 验证有效的解决方案
在构建OKHttpClient的时候开启连接失败重试开关:
OkHttpClient client = new OkHttpClient.Builder()
...
.retryOnConnectionFailure(true) //开启连接失败时重连逻辑
.build();
修改后本地测试未复现,但因线上环境复杂,可能不同地区网络状态差异较大,该问题需要进一步分析线上埋点数据分析改善效果。
!!开启了失败重连务必测试重连带来的服务器压力和重试次数。