本文最初写于2020年,起因是一次连接没有复用的问题。2026年重新整理时删除了旧版本源码和官网示例,保留仍然有效的判断方法。

HTTP客户端的使用方式很简单,运行行为却涉及DNS、连接建立、TLS、连接池、并发队列、超时和重试。只看一次请求是否成功,很难判断客户端在压力下会发生什么。

我当时遇到的问题是:明明配置了连接池,请求之间却仍然反复建立连接。最后发现服务端响应明确要求关闭连接。这个细节让我意识到,连接复用不是客户端单方面的配置结果,而是协议、响应是否完整消费以及连接状态共同决定的。

复用一个客户端

OkHttpClient内部持有连接池和异步调用调度器。为每次请求创建新客户端,会拆散这些可以共享的资源,连接复用也就失去了意义。

通常应在应用范围内复用一个客户端;某次调用需要不同配置时,可以从共享客户端使用newBuilder()派生。派生客户端能够共享连接池、调度器和已有配置,而不必重新建立一套网络资源。

连接什么时候能够复用

建立TCP和TLS连接需要额外往返,连接池会尽量复用符合条件的现有连接。HTTP/2还可以让同一连接承载多个并发流。

但连接复用有前提:

  • 响应体被正确读取或关闭,连接才能及时归还;
  • 服务端或代理没有要求关闭连接;
  • 地址、代理、TLS配置等条件允许共享;
  • 连接仍然健康,没有被对端或网络设备回收。

因此排查“为什么没有复用”时,不能只看ConnectionPool参数。更有效的方法是通过EventListener观察DNS、连接建立、连接获取和释放事件,确认请求到底在哪个阶段创建了新连接。

Dispatcher管理的是异步调用

异步请求通过Dispatcher进入执行器。Dispatcher限制同时运行的调用总数和单个主机的并发数,超过限制的调用进入队列。

这些限制主要约束异步调用,不等于整个系统的完整流量保护。同步调用由调用线程直接执行;即使客户端并发受控,下游服务、线程池和业务重试仍然可能放大流量。

自定义执行器时也不能只把最大线程数调大。需要同时观察队列、单主机并发、请求耗时和下游容量。

四类超时

OkHttp提供几个容易混淆的超时维度:

  • connectTimeout:建立到目标地址连接的时间限制;
  • readTimeout:一次读取操作等待数据的时间限制;
  • writeTimeout:一次写入操作的时间限制;
  • callTimeout:一次完整调用从开始到结束的总时间限制,包括连接、写入、服务端处理、读取以及可能发生的重试和跳转。

它们解决的是不同问题。只设置读取超时,不能约束一次调用经历多个阶段后的总耗时;把所有超时设成同一个数字,也不代表符合实际链路。

参数应来自调用目标、接口SLA和失败后的处理方式。对于不可安全重复的写操作,还要谨慎评估自动重试可能造成的副作用。

从结果转向过程

网络调用的不同问题经常被统一描述为“请求超时”。DNS慢、连接失败、TLS慢、连接池没有命中、服务端迟迟不返回数据,最终都可能表现为调用失败。

理解OkHttp时,与其记住某个版本的默认参数,不如把一次调用拆成可观察的阶段,再判断连接、并发和超时分别在控制什么。这样得到的客户端配置,也更容易与实际链路对应。

参考