OkHttp 连接池与超时配置

讲真,很多同学把 OkHttp 当成一个简单的 HTTP 客户端来用,配个超时时间就完了。但实际在 CI 测试和线上环境中,连接池和重试机制才是性能的关键。我早期维护过一个支付 SDK,就因为连接池配置不当,导致高并发下大量 TIME_WAIT 连接,最后被运维找上门……嗯,从那之后我对这块儿再不敢马虎。

连接池原理

OkHttp 的 ConnectionPool 核心思路很简单:复用已经建立的 TCP 连接,避免三次握手和 TLS 握手的开销。你想想看,一个 HTTPS 请求从建立连接到发送数据,光是握手就可能消耗 2~3 个 RTT。如果每次请求都新建连接,延迟和服务器压力都会飙升。

连接池内部维护了一个 Deque<RealConnection>,每个连接都有空闲时间戳。当一个请求完成,连接不会立刻关闭,而是放回池中供后续复用。默认情况下,每个连接最多空闲 5 分钟,池中最多保留 5 个空闲连接。

提示: 连接池是线程安全的。我在项目里遇到过并发环境下连接泄漏的问题,后来发现是多线程共用同一个 OkHttpClient 实例,但连接池的清理线程没正确启动。解决方案:确保你的 OkHttpClient 全局单例,别每次创建新的。

超时设置

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秒整体超时
注意: 不要为了省事把超时设得特别大。我见过有人设 120 秒 readTimeout,结果服务器挂了,客户端还在傻等。合理的做法是针对不同接口设置不同超时——比如登录接口 10s,上传接口 60s。

重试机制

OkHttp 默认自带重试逻辑,由 RetryAndFollowUpInterceptor 实现。它会自动处理两类场景:

  • 路由重试: 如果当前 IP 连接失败,尝试下一个 DNS 解析出的 IP(如果有多个 IP)
  • 自动重定向: 响应码 3xx 时跟随跳转(默认最多 20 次)

但注意,不是所有失败都会重试。只有以下情况会触发:

  1. 连接超时或连接被拒绝
  2. TLS 握手失败(且可以回退到非加密连接?其实不会,只是换 IP)
  3. 服务端返回 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),避免雪崩。

知识结构总览

下面这张图梳理了连接池、超时、重试之间的协作关系,帮你快速建立整体认知:

OkHttp 连接池与超时配置核心流程 OkHttpClient 配置 连接池 ConnectionPool 超时设置 请求执行与拦截器链 RetryAndFollowUp 重试 关键配置建议 • 连接池:maxIdleConnections=5,keepAliveDuration=5min(默认适合大多数场景) • 超时:connectTimeout=15s,readTimeout=10s,writeTimeout=30s(根据业务调整) • 重试:retryOnConnectionFailure=false(CI 测试),true(生产环境需保证幂等) 图 3-1 OkHttp 核心参数与流程关系
避坑指南: 我曾经在微服务架构中,因为每个服务都单独创建 OkHttpClient 实例,导致连接池资源浪费。后来改用 OkHttpClient.Builder 复用同一个配置,内存占用下降 40%。记住:一个 JVM 进程,一个 OkHttpClient 单例就够了。