症状
警告本身就是“丢弃凭证”:Connection pool is full, discarding connection: api.example.com. Pool size: 10——池大小 10 对上 32 个并发 worker,意味着每波请求里有 22 条连接被建立、用一次、然后扔掉,每条都付一遍 TCP 加 TLS 握手的成本。客户端侧的证据堆在套接字表里:TIME_WAIT 数以千计且与请求量成比例。并发越高延迟越涨而服务器却闲着,瓶颈在客户端主机的握手而不在服务容量。
诊断思路
先统计警告数量与 TIME_WAIT 规模,确认“连接被制造又丢弃”的churn 机制;再审计 Session 用法——每请求建 Session 或直接用模块级 requests.get 都会绕开连接池,共享 Session 才能让池的尺寸真正起作用;最后核对适配器配置,pool_maxsize 小于 worker 数即可精确预测丢弃率。pool_block 默认 False 意味着溢出连接被静默制造再拆除而不是排队。
根因
urllib3 每主机最多保留 pool_maxsize 条持久连接。并发请求超出时,多余的连接为满足需求被创建,用完即因池满而被丢弃——警告是丢弃通知,代价是每个溢出请求一次完整的 TCP 与 TLS 握手,外加客户端不断增长的 TIME_WAIT。
修复与验证
每进程共享一个 Session,挂载按真实并发放大尺寸的 HTTPAdapter:pool_maxsize 大于等于线程或任务数、pool_connections 等于调用的主机数、适配器上配 Retry。Session 以引用方式传递给 worker(模块级单例或依赖注入),绝不在请求里调工厂。想用排队代替churn 就加 pool_block=True,但必须先完成尺寸设定——尺寸不足的阻塞池只是把一种故障换成另一种。验证时重放产生警告的负载:日志零 pool-full 警告、TIME_WAIT 有界、p99 因握手消失而可测量地下降;再到预发把 pool_maxsize 压回并发之下,确认警告如期回归——证明你拧的正是支配该行为的旋钮。
代码示例
2026-10-22 10:02:11 WARNING urllib3.connectionpool
Connection pool is full, discarding connection: api.example.com.
Pool size: 10
ss -tan state time-wait | wc -l
# 4821 <- proportional to discarded connections
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retry = Retry(total=3, backoff_factor=0.2, status_forcelist=[502, 503, 504])
adapter = HTTPAdapter(
pool_connections=4, # distinct hosts you call
pool_maxsize=64, # >= your real concurrency
max_retries=retry,
)
session.mount("https://", adapter)
session.mount("http://", adapter)