当使用 requests 或 urllib3 发起 HTTPS 请求时,第一次请求通常比后续慢 100~300ms,这是因为冷连接需要经历完整的 TCP 三次握手(1 RTT)和 TLS 握手(2 RTT),加上请求/响应(1 RTT),总共 4 RTT。而热连接只需 1 RTT,省下的 3 RTT 就是预热的优化空间。
很多开发者误以为使用 requests.Session() 或 urllib3.PoolManager() 后连接池会自动预热,但实际上连接是懒加载的——只有第一次请求时才会建立连接。这意味着第一个用户的请求仍然要承受完整的握手延迟。
预热的核心思路是在用户请求到来之前,先把连接建好并放入池中,让“第一次请求”也能享受到复用连接的加速。下面介绍三种由简到难的预热方式,并对比实测数据。
一、最简单的 HEAD 请求预热
HEAD 请求只返回响应头,不下载 body,开销极小,但能触发完整的 TCP + TLS 握手。- import requests
- s = requests.Session()
- s.head('https://api.example.com/health') # 预热握手,连接留在池中
- resp = s.get('https://api.example.com/data') # 直接复用
复制代码 优点是一行代码,不依赖内部 API;缺点是多了一次无用请求,服务端会看到这个 HEAD 记录。
二、推荐方式:connection_from_host 强制建连
urllib3 提供了官方方法 connection_from_host,直接创建连接放入池中,不发任何请求。- import urllib3
- pool = urllib3.PoolManager(num_pools=50, maxsize=20)
- pool.connection_from_host('api.example.com', port=443, scheme='https')
- resp = pool.request('GET', 'https://api.example.com/data')
复制代码 如果使用 requests,需要先获取底层的 urllib3 连接池对象:- import requests
- s = requests.Session()
- pool = s.mount('https://', requests.adapters.HTTPAdapter(pool_connections=20, pool_maxsize=20))
- pool.poolmanager.connection_from_host('api.example.com', port=443, scheme='https')
- resp = s.get('https://api.example.com/data')
复制代码 这种方法零额外网络开销,服务端完全无感知,是生产环境的首选。
三、生产环境推荐:启动时批量预热多个主机
对于微服务或涉及多个 API 网关的场景,可以在应用启动时遍历核心服务列表,逐一预热:- import urllib3
- from urllib3.util.retry import Retry
- pool = urllib3.PoolManager(
- num_pools=100,
- maxsize=50,
- timeout=3.0,
- retries=Retry(total=3, backoff_factor=0.5),
- block=True,
- )
- core_hosts = [('api.example.com', 443), ('auth.example.com', 443)]
- for host, port in core_hosts:
- try:
- pool.connection_from_host(host, port=port, scheme='https')
- print(f'✅ {host}')
- except Exception as e:
- print(f'❌ {host}: {e}')
复制代码 注意要处理异常,避免目标服务未启动时导致程序崩溃。
四、实测对比
测试数据:不预热时第一次请求约 280ms;使用 HEAD 预热约 120ms;使用 connection_from_host 预热约 110ms,与第二次复用的耗时几乎一致(约 110ms)。因此,connection_from_host 是最优解,它让“第一次”直接达到“第二次”的速度。
五、三个常见坑
1. TLS Session Resumption 才是真正的省时利器。urllib3 默认开启,第二次请求同一主机时 TLS 握手可从 2 RTT 降到 1 RTT。预热的真正价值是让“第一次”就享受到“第二次”的提速。
2. 预热失败必须处理异常。用 try/except 捕获 urllib3.exceptions.NewConnectionError,避免启动时崩溃。
3. 预热不是银弹。仅在首次请求必须低延迟(如健康检查)、多主机轮询等场景有意义;对于高频重复调用或长连接保持,连接池已足够,无需额外预热。
六、最佳实践:启动预热 + 连接池复用
在应用初始化时创建全局连接池,并立即预热核心服务列表。后续所有请求都会从池中取出已就绪的连接,大幅降低首请求延迟。代码模板可参考第三节的批量预热写法。
总结:冷连接预热通过提前完成 TCP+TLS 握手,将首请求耗时从 4 RTT 降至 1 RTT。一行代码 pool.connection_from_host() 即可实现,剩下的连接池会自动管理复用。如果你的应用对首次请求的延迟敏感,是时候加上预热了。 |