1. 课程导入与全景认知
说实话,我刚入行那会儿,也干过一件蠢事——每次发 HTTP 请求都新建一个连接,用完就关。那时候项目规模小,感觉没啥问题。直到有一次线上告警,说服务响应时间从 50ms 飙到了 2s,我一看监控,好家伙,TCP 连接数像过山车一样忽高忽低。
后来我才明白,每次请求都新建连接,代价太大了。你想想看,一个 TCP 连接从建立到关闭,要经历三次握手、四次挥手,再加上 TLS 握手(如果用了 HTTPS),光这些开销就能吃掉几十毫秒。在高并发场景下,这简直就是灾难。
核心结论:连接池的本质,就是用空间换时间。把建立好的连接缓存起来,下次请求直接复用,避免重复握手开销。
HTTP 连接复用原理
HTTP 协议本身是支持连接复用的。从 HTTP/1.1 开始,默认就开启了 Connection: keep-alive。什么意思呢?就是同一个 TCP 连接,可以发送多个 HTTP 请求和响应,不用每次都重新建立。
但这里有个坑——浏览器和服务器之间的 keep-alive 是有限制的。我记得有一次排查问题,发现客户端明明设置了 keep-alive,但服务端那边连接还是频繁断开。后来一查,是 Nginx 的 keepalive_timeout 设得太短了。
连接复用的流程,说白了就是三步:
- 建立连接:第一次请求时,创建 TCP 连接,完成握手。
- 复用连接:后续请求直接走已有连接,省去握手时间。
- 归还连接:请求完成后,连接不关闭,放回池子里等下次用。
我画了一张图,帮你理清这个逻辑:
连接池的核心参数
连接池好不好用,关键看参数配得对不对。我见过太多人直接用默认值,结果线上出问题。下面这三个参数,是必须搞明白的。
1. maxsize(最大连接数)
这个参数控制的是单个连接池实例能持有的最大连接数。比如你用 requests.Session(),每个 Session 都有自己的连接池。
我个人的习惯是,maxsize 不要设太大。为什么?因为连接数越多,服务端压力越大,而且客户端自己也会消耗文件描述符。我曾经接手过一个项目,maxsize 设了 1000,结果服务端直接扛不住,CPU 打满。
经验值:一般建议 maxsize 设为 10-50,具体看你的并发量和服务端承载能力。如果服务端是 Nginx + 多进程,可以适当放大。
2. max_connections(全局最大连接数)
这个参数和 maxsize 容易搞混。max_connections 是整个进程能打开的最大连接数,不是单个池子的。比如你用 urllib3 的 PoolManager,它内部会维护多个连接池(按 host:port 区分),max_connections 就是所有池子加起来的上限。
| 参数 | 作用范围 | 典型值 | 注意事项 |
|---|---|---|---|
| maxsize | 单个连接池 | 10-50 | 按目标服务独立配置 |
| max_connections | 全局(所有池子) | 100-500 | 受系统 fd 限制 |
| ttl | 单个连接 | 30-300 秒 | 避免连接长时间空闲 |
注意:max_connections 设得太大,可能会耗尽系统的文件描述符。Linux 默认是 1024,你可以用 ulimit -n 查看。我曾经遇到过因为 fd 耗尽导致服务完全不可用的情况,排查了半天才发现是连接池参数配错了。
3. ttl(连接存活时间)
ttl 是 Time To Live 的缩写,控制的是连接在池子里最多能活多久。超过这个时间,连接就会被关闭并移除。
为什么要设 ttl?因为连接不是永生的。长时间不用的连接,可能被中间设备(比如防火墙、负载均衡器)给掐断。你想想看,如果池子里全是死连接,那复用还有什么意义?
我建议 ttl 设成 30-60 秒。太短了频繁重建连接,太长了容易积累死连接。有一次我调优一个爬虫系统,把 ttl 从 300 秒改成 60 秒,成功率直接提升了 5%。
一个简单的连接池配置示例
下面是我常用的配置模板,用 urllib3 举例:
import urllib3
from urllib3.util import Retry
# 创建连接池管理器
http = urllib3.PoolManager(
num_pools=10, # 最多维护 10 个连接池
maxsize=20, # 每个池子最多 20 个连接
block=True, # 连接池满时是否阻塞等待
timeout=urllib3.Timeout(
connect=5.0, # 连接超时
read=10.0 # 读取超时
),
retries=Retry(
total=3, # 总重试次数
backoff_factor=0.5 # 退避因子
)
)
# 发送请求
response = http.request('GET', 'https://api.example.com/data')
print(response.status)
小技巧:用 block=True 可以让请求在连接池满时排队等待,而不是直接报错。但要注意,排队时间不能太长,否则会拖垮整个系统。我一般配合 timeout 一起用。
避坑指南
最后,分享几个我踩过的坑:
- 连接泄漏:用完连接不归还,池子很快被耗尽。记得用
with语句或者显式调用release()。 - 参数一刀切:不同服务的连接特性不一样。比如内部 RPC 服务延迟低,可以设小一点的 maxsize;外部 API 延迟高,可以适当放大。
- 忽略监控:连接池的命中率、等待时间、创建速率,这些指标一定要监控。我习惯在 Grafana 上画一张连接池大盘,一眼就能看出问题。
嗯,连接池的基础就这些。说白了,它就是帮你省掉重复握手的开销,让高并发场景下的 HTTP 请求更高效。参数调优没有银弹,多观察、多测试,找到最适合你业务的配置。