如何使用 OkHttp 实现请求重试、超时控制和连接池复用?

解读

国内面试中,OkHttp 几乎是“网络必考题”。面试官想确认三件事:

  1. 你是否只会“调库”,还是能根据业务场景把参数配到“刚刚好”;
  2. 遇到弱网、CDN 抖动、运营商劫持等中国特色场景,能否用重试/超时策略兜底;
  3. 高并发业务(如电商秒杀、直播礼物)下,连接池配错直接导致端口耗尽或 SYN 风暴,你有没有踩过坑。
    因此,回答时要“落地”:给出代码模板、参数含义、踩坑案例,以及监控手段。

知识点

  1. 超时四兄弟:callTimeout、connectTimeout、readTimeout、writeTimeout;
  2. 重试两层次:OkHttp 内置重试(RetryOnConnectionFailure)与业务重试(Interceptor);
  3. 连接池:ConnectionPool 内部用 ConcurrentLinkedQueue + 线程清理器,keep-alive 时间默认 5 min,最大空闲连接数默认 5;
  4. 国内特色:
    – 域名被运营商 TTL 缓存,DNS 轮询失效,需结合 HttpDns;
    – 弱网下 TCP 握手 3 s 才失败,需把 connectTimeout 降到 2 s 以内,快速换 IP;
    – 直播场景上传日志,writeTimeout 必须大于“最大分片时间”,否则 408 频发;
  5. 监控:EventListener 统计重试次数、连接复用率、TCP 握手耗时,接入阿里 SLS 或腾讯 RUM。

答案

“我在上一个电商项目里把网络模块拆成独立组件,核心思路是‘三件套’:超时配准、重试分层、连接池可观测。”

  1. 超时控制
val client = OkHttpClient.Builder()
    .callTimeout(10, TimeUnit.SECONDS)      // 整次调用硬上限,防止“伪死”
    .connectTimeout(2, TimeUnit.SECONDS)    // 国内 4G/5G 切换场景,2 s 足够
    .readTimeout(8, TimeUnit.SECONDS)       // 下载商品大图,8 s 兜底
    .writeTimeout(5, TimeUnit.SECONDS)      // 上传埋点,5 s 容错
    .build()

注意:callTimeout 会覆盖单个请求的总耗时,出现“慢接口”时需后台配合拆分。

  1. 请求重试
    内置开关默认开启,但只重试“幂等”请求(GET、HEAD)。对 POST/PUT 支付接口,必须自定义 Interceptor 做“业务重试”:
class BusinessRetryInterceptor(
    private val maxRetry: Int = 2,
    private val retryCodes: List<Int> = listOf(408, 502, 503, 504)
) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var response: Response? = null
        var exception: IOException? = null
        for (i in 0..maxRetry) {
            try {
                response = chain.proceed(chain.request())
                if (!response.isSuccessful && response.code in retryCodes) {
                    response.close()
                    if (i < maxRetry) continue
                }
                return response
            } catch (e: IOException) {
                exception = e
                if (i < maxRetry) continue else throw exception
            }
        }
        throw exception!!
    }
}

接入时放在普通拦截器之后、网络拦截器之前,避免重复添加公共头。

  1. 连接池复用
val pool = ConnectionPool(8, 3, TimeUnit.MINUTES)
val client = OkHttpClient.Builder()
    .connectionPool(pool)
    .build()

参数解释:
– 8:秒杀高峰并发接口 200 QPS,经验值 8 条连接可把 3 次握手耗时从 120 ms 降到 20 ms;
– 3 min:短于运营商 NAT 超时(一般 5 min),防止 FIN_WAIT2 堆积。
配合“同一 host 复用”策略,所有图片域名收敛到 cdn.xxx.com,减少 TLS 握手次数 30%+。

  1. 可观测
    自定义 EventListener,把“connectionAcquired”“callFailed”事件打到美团 Logan 日志,灰度期间发现重试率 >5% 的接口,立即回退后台或扩容 CDN。

这样配置后,线上网络请求失败率从 1.2% 降到 0.3%,GC 抖动减少 15%,用户支付取消率下降 0.8 个百分点。

拓展思考

  1. HTTP/2 多路复用 vs. 连接池:如果服务端已开启 HTTP/2,理论上一条 TCP 连接即可多路并发,但国内部分高防 CDN 会强制降级到 HTTP/1.1,此时仍依赖连接池。
  2. 预连接(preconnect):利用 client.newCall(Request.Builder().url("https://cdn.xxx.com").build()).enqueue(object : Callback{ override fun onResponse(call, response) { response.close() } override fun onFailure(call, e) {} }) 提前握手,把耗时隐藏在首屏渲染前。
  3. 与 Retrofit 结合:Retrofit 默认使用全局 OkHttpClient,但不同模块需要不同超时策略时,可用 @OkHttpClientWithTimeout 自定义注解,配合 Retrofit 的 callFactory 注入,实现“一个接口一套策略”。
  4. 弱网模拟:国内主流做法是接入腾讯 QNET 或阿里 ATC,在地铁、电梯场景做 2G/3G 抖动压测,验证重试次数与超时阈值是否合理。