OkHttp 连接池与超时配置
讲真,很多同学把 OkHttp 当成一个简单的 HTTP 客户端来用,配个超时时间就完了。但实际在 CI 测试和线上环境中,连接池和重试机制才是性能的关键。我早期维护过一个支付 SDK,就因为连接池配置不当,导致高并发下大量 TIME_WAIT 连接,最后被运维找上门……嗯,从那之后我对这块儿再不敢马虎。
连接池原理
OkHttp 的 ConnectionPool 核心思路很简单:复用已经建立的 TCP 连接,避免三次握手和 TLS 握手的开销。你想想看,一个 HTTPS 请求从建立连接到发送数据,光是握手就可能消耗 2~3 个 RTT。如果每次请求都新建连接,延迟和服务器压力都会飙升。
连接池内部维护了一个 Deque<RealConnection>,每个连接都有空闲时间戳。当一个请求完成,连接不会立刻关闭,而是放回池中供后续复用。默认情况下,每个连接最多空闲 5 分钟,池中最多保留 5 个空闲连接。
超时设置
OkHttp 提供了三个独立超时:
| 超时类型 | 默认值 | 作用时机 | 我的建议 |
|---|---|---|---|
connectTimeout |
10 秒 | TCP 连接建立 | 内网 5s,外网 15~30s |
readTimeout |
10 秒 | 从服务器读取数据 | 根据接口 95 分位耗时 + 2s 缓冲 |
writeTimeout |
10 秒 | 向服务器发送数据 | 文件上传场景建议 60s+ |
这里说个坑:readTimeout 不是请求总耗时,而是两次读取之间的间隔超时。如果你有一个大文件下载,读取间隔很小,但整体耗时很长,readTimeout 并不会触发。我曾经踩过这个雷——接口返回很慢,但每 5 秒会发一个 keep-alive 心跳,结果 readTimeout 永远不生效,业务一直卡住。解决方案是用 call.timeout() 设置总超时。
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build();
// 单个请求总超时控制(建议用于关键接口)
Request request = new Request.Builder()
.url("https://api.example.com/data")
.build();
Call call = client.newCall(request);
call.timeout().timeout(30, TimeUnit.SECONDS); // 30秒整体超时
重试机制
OkHttp 默认自带重试逻辑,由 RetryAndFollowUpInterceptor 实现。它会自动处理两类场景:
- 路由重试: 如果当前 IP 连接失败,尝试下一个 DNS 解析出的 IP(如果有多个 IP)
- 自动重定向: 响应码 3xx 时跟随跳转(默认最多 20 次)
但注意,不是所有失败都会重试。只有以下情况会触发:
- 连接超时或连接被拒绝
- TLS 握手失败(且可以回退到非加密连接?其实不会,只是换 IP)
- 服务端返回 503(需配合
retryOnConnectionFailure配置)
我个人习惯在 Mock 测试中将重试关闭,否则 CI 跑测试时,请求失败就会自动重试,导致测试用例超时。关闭方法:
OkHttpClient client = new OkHttpClient.Builder()
.retryOnConnectionFailure(false) // 关闭连接失败重试
.followRedirects(false) // 关闭跟随重定向
.followSslRedirects(false)
.build();
@Idempotent 注解在客户端控制重试次数。
再说个实战技巧:手动实现重试策略。OkHttp 允许通过 Interceptor 自定义重试逻辑:
public class RetryInterceptor implements Interceptor {
private final int maxRetries;
private final long retryIntervalMs;
public RetryInterceptor(int maxRetries, long retryIntervalMs) {
this.maxRetries = maxRetries;
this.retryIntervalMs = retryIntervalMs;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
IOException lastException = null;
for (int i = 0; i <= maxRetries; i++) {
try {
response = chain.proceed(request);
if (response.isSuccessful()) {
return response;
}
// 非成功响应(如 5xx)也触发重试
if (i < maxRetries) {
response.close();
Thread.sleep(retryIntervalMs);
}
} catch (IOException e) {
lastException = e;
if (i < maxRetries) {
Thread.sleep(retryIntervalMs);
}
}
}
throw lastException != null ? lastException : new IOException("Max retries exceeded");
}
}
添加这个拦截器后,就能对特定接口实现指数退避重试。不过我个人建议:重试次数不要超过 3 次,间隔递增(1s → 2s → 4s),避免雪崩。
知识结构总览
下面这张图梳理了连接池、超时、重试之间的协作关系,帮你快速建立整体认知:
OkHttpClient.Builder 复用同一个配置,内存占用下降 40%。记住:一个 JVM 进程,一个 OkHttpClient 单例就够了。