HTTP 连接池与 Keep-Alive¶
30 秒版(开场)¶
Go
http.Transport默认会缓存连接;对 HTTP/1.x,DefaultMaxIdleConnsPerHost为 2,固定高并发下游可能需要调大。Keep-Alive 复用 TCP/TLS 握手。生产应复用Client/Transport,并按容量设置MaxIdleConnsPerHost、MaxConnsPerHost、IdleConnTimeout和各阶段超时。关键词:TIME_WAIT、FD 耗尽、HTTP/2 多路复用。
3 分钟版(精讲深度)¶
- 是什么:
http.Transport维护 idle 连接缓存;请求完连接放回池供复用;Keep-Alive 是 HTTP/1.1 默认持久连接。 - 为什么:每次新建 TCP+TLS 握手有额外延迟与 CPU 成本;HTTP/1.x 高并发出站调用若每 host 只保留 2 条空闲连接,波峰时会频繁重建连接。
- 怎么做:按相同 TLS、代理和超时策略复用
http.Client/Transport;容量值由下游实例数、并发和 HTTP 版本压测确定;同时设置请求总 deadline 与连接、TLS、响应头等阶段超时。
10 分钟版(原理 + 图示)¶
Transport 关键字段
| 字段 | 默认 | 建议 |
|---|---|---|
| MaxIdleConns | 100 | 按总 outbound 调 |
| MaxIdleConnsPerHost | 2 | 20~100 |
| MaxConnsPerHost | 0(不限) | 需要保护下游时设置 |
| IdleConnTimeout | 90s | 对齐 LB idle |
| TLSHandshakeTimeout | 10s | 防挂死 |
| ExpectContinueTimeout | 1s | 大 body 上传 |
flowchart TB
Client[http.Client 单例] --> T[Transport 连接池]
T --> H1[host A idle conn 1]
T --> H2[host A idle conn 2]
T --> H3[host B idle conn]
Req[并发请求] --> T
T -->|无可复用连接且未达上限| New[新建 TCP+TLS]
New --> T
HTTP/1.1 vs HTTP/2:HTTP/2 在一条连接上并发多个 stream,达到服务端 MAX_CONCURRENT_STREAMS 或连接异常时也可能建立更多连接;HTTP/1.1 并行请求通常需要多条连接。DefaultTransport 会尝试 HTTP/2;若自定义 DialContext、DialTLS 或 TLSClientConfig,应显式设置 ForceAttemptHTTP2: true,否则可能保守地关闭自动 HTTP/2。
陷阱:http.Get 使用 DefaultClient,没有应用级总超时;多个零值 http.Client{} 仍会共享 DefaultTransport,真正破坏复用的是每次新建自定义 Transport。响应体必须 Close;对 HTTP/1.x,通常还应读到 EOF 才能稳定复用该连接。
生产场景¶
- 调用支付网关高 QPS:先按协议版本、并发度和下游承载能力压测,再调
MaxIdleConnsPerHost/MaxConnsPerHost;不要背固定参数或固定收益。 - K8s 滚动发布:旧 Pod 连接仍被 client 池持有 → 502;缩短
IdleConnTimeout+ 优雅 drain。 - Sidecar Envoy:客户端 HTTP/2 单连接,注意 max concurrent streams。
排查与工具¶
| 工具 | 用途 |
|---|---|
ss -s / lsof -p |
FD 与连接状态 |
| pprof goroutine | 阻塞在 dial |
| APM outbound span | 连接 vs TTFB |
GODEBUG=http2debug=2 |
HTTP/2 帧调试 |
路径:出站 RT 周期性尖刺 → 是否每请求 NewClient → TIME_WAIT 堆积 → 调池参数与 LB idle 对齐。
架构取舍¶
| 方案 | 适用 | 不适用 |
|---|---|---|
| 共享 Transport | 所有出站 HTTP | 完全不同 TLS 需求 |
| 连接池调优 | 高 QPS 固定下游 | 单次调用 |
| HTTP/2 | 同 host 高并发 | 老中间盒不兼容 |
| 短连接 | 极低频 | 性能敏感 |
| 专用 proxy | 统一 mTLS/限流 | 简单两服务 |
深挖问答¶
- 为什么要读完并关闭 Body? → HTTP/1.x Transport 只有在响应体读到 EOF 后,才通常能把连接放回复用池;无论是否读完都必须
Close。请求取消用 context,不再推荐CancelRequest。 - Client.Timeout 包含什么? → 从 Dial 到读完 body 的总时间。
- MaxConnsPerHost? → Go 1.11+ 限制总连接,防打爆下游。
- Keep-Alive 谁关? → 空闲超时任一侧可关;下次请求重建。
- 和数据库连接池区别? → 语义类似;HTTP 还有 TLS 会话复用。
反模式与事故¶
- 每个请求新建一个自定义
http.Transport——连接池彼此隔离,TLS/TCP 频繁重建。 - 忽略
resp.Body.Close()——泄漏 FD。 - 只调大空闲池却不设
MaxConnsPerHost——突发流量仍可建立大量连接打爆下游。 - 客户端 idle timeout 明显长于 LB——增加陈旧连接、重连和偶发网络错误风险;是否自动重试还取决于请求是否幂等、body 是否可重放等条件。
代码示例¶
var defaultHTTPClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 50,
MaxConnsPerHost: 100,
IdleConnTimeout: 60 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 5 * time.Second,
ForceAttemptHTTP2: true,
},
}
func callAPI(ctx context.Context, url string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := defaultHTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
_, err = io.Copy(io.Discard, resp.Body) // HTTP/1.x 尽量读到 EOF
return err
}