跳转至

连接池复用

预先把连接建好放着,谁要用谁借,用完别关——高频场景下最简单有效的优化


核心概念

  1. 连接池 — 一批预先建好的连接集合,用时借出,用完归还而非关闭
  2. HTTP/1.1 Keep-Alive 复用 — 同一 TCP 连接上串行处理多个请求
  3. HTTP/2 多路复用 — 一个 TCP 连接上并行处理多个请求/响应
  4. 池化参数 — 最大连接数、最小空闲数、超时时间等关键配置

详解

没有连接池的世界

每次请求都要:建 TCP 连接 → TLS 握手 → 发数据 → 四次挥手关掉。

sequenceDiagram
    participant C as 客户端
    participant S as 服务器
    loop 每次请求
        C->>S: TCP 三次握手
        C->>S: TLS 握手(HTTPS)
        C->>S: GET /api/data
        S->>C: 200 OK + 数据
        C->>S: TCP 四次挥手
    end

就像每次出门买菜都要:解锁 → 开门 → 走出去 → 锁门,回来再解锁 → 开门 → 走进去 → 锁门。一天买十次菜,光开关门就累死了。

有连接池的世界

sequenceDiagram
    participant P as 连接池
    participant S as 服务器
    Note over P: 启动时建好 50 个连接
    participant C as 请求
    C->>P: 借连接 A
    C->>S: GET /api/data(复用连接 A)
    S->>C: 200 OK
    C->>P: 还连接 A
    C->>P: 借连接 B
    C->>S: GET /api/user(复用连接 B)
    S->>C: 200 OK
    C->>P: 还连接 B

两个层次的复用

HTTP/1.1 连接复用(Keep-Alive)

sequenceDiagram
    participant C as 客户端
    participant S as 服务器
    Note over C,S: TCP 连接(建一次)
    C->>S: 请求1
    S->>C: 响应1
    C->>S: 请求2(同一个连接)
    S->>C: 响应2
    C->>S: 请求3(同一个连接)
    S->>C: 响应3

但同一时刻只能有一个请求在跑(队头阻塞)。浏览器默认开 6 个连接弥补:

连接1: 请求1 → 响应1   (串行)
连接2: 请求2 → 响应2   (串行)
连接3: 请求3 → 响应3   (串行)
连接4: 请求4 → 响应4   (串行)
连接5: 请求5 → 响应5   (串行)
连接6: 请求6 → 响应6   (串行)
          ↑
    6 个连接之间是并行的

HTTP/2 多路复用

一个 TCP 连接上同时跑多个请求/响应,通过帧编号区分:

TCP 连接(就一个)
├── 请求1 的第1帧 ──┐
├── 请求2 的第1帧 ──┼── 混在一起发,到了对端自动分拣
├── 请求1 的第2帧 ──┤
├── 请求3 的第1帧 ──┤
├── 请求2 的第2帧 ──┘

不再需要多个 TCP 连接来并行,一根连接全部搞定。

关键配置参数

参数 说明 过大 过小
最大连接数 池子最多同时借出多少个 内存和 fd 浪费 请求排队等连接
最小空闲 池子里至少保留多少个待命 空闲资源浪费 高并发时来不及建连
连接超时 借不到连接最多等多久 请求长时间挂起 高峰期大量失败
空闲回收 超过多久没用就释放 占着不用浪费资源 频繁重建连接

代码示例

Python requests

from requests.adapters import HTTPAdapter

adapter = HTTPAdapter(
    pool_connections=10,   # 连接到不同目标服务器的池子数
    pool_maxsize=20,       # 每个池子最多保持 20 个连接
)
session.mount('https://', adapter)

Go

client := &http.Client{
    Transport: &http.Transport{
        MaxIdleConns:        100,  // 最大空闲连接
        MaxIdleConnsPerHost: 10,   // 每个主机最多保留 10 个空闲连接
        IdleConnTimeout:     90 * time.Second,
    },
}

Java Apache HttpClient

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(200);           // 总连接池大小
cm.setDefaultMaxPerRoute(20);  // 每个目标服务器最多 20 个连接

常见陷阱

连接池打满导致延迟飙升

池子大小为 10,但同时来了 50 个请求,40 个在排队等连接。解决方案:合理调大池子,或用 HTTP/2 多路复用减少对连接数的需求。

僵尸连接

服务器端已经关闭了连接,但客户端不知道,下次复用时发现连接已死 → 报错。解决方案:开启连接健康检查、设置合理的空闲超时、使用 TCP keepalive。


练习题

连接池大小设为多少合适?
答案

没有万能值,取决于场景。一般经验值:最大连接数 = 服务端能承受的并发数,空闲连接数 = 平均并发请求数的 20-50%。高 IO 等待的场景(如调外部 API)可以设大一些,CPU 密集的场景设小一些。监控连接池的借出/归还速率来动态调整。

HTTP/2 的多路复用还需要连接池吗?
答案

连接池仍然有用。HTTP/2 解决了单个连接上的并行问题,但连接池提供了**容错**(一个连接挂了可以切到另一个)和**负载均衡**(多连接分散压力)的能力。此外,Go 的 http.Transport 本身就是连接池实现。