3. requests.Session 源码分析:Session 对象如何管理连接池,适配器(Adapter)的作用
上一章我们聊了 urllib3 的连接池原理。那 requests 库是怎么把这些能力封装起来的?
说白了,Session 对象就是 requests 对 urllib3 连接池的一层高级封装。它让你不用手动创建 PoolManager,不用操心连接复用,甚至不用管 HTTP 和 HTTPS 走不同的端口。
我刚开始用 requests 时,也以为 Session 就是个“保持 cookie 的容器”。后来线上压测发现 QPS 上不去,才意识到——Session 真正的核心,是连接池管理。
3.1 Session 对象的初始化
先看一段最简代码:
import requests
s = requests.Session()
resp = s.get('https://api.example.com/data')
这背后发生了什么?我们打开 requests 的源码(以 2.31.0 为例),看看 __init__ 方法:
class Session(SessionRedirectMixin):
def __init__(self):
self.headers = default_headers()
self.cookies = cookiejar_from_dict({})
self.auth = None
self.proxies = {}
self.hooks = default_hooks()
self.params = {}
self.stream = False
self.verify = True
self.cert = None
self.max_redirects = DEFAULT_REDIRECT_LIMIT
self.trust_env = True
# 核心:适配器字典
self.adapters = OrderedDict()
self.mount('https://', HTTPAdapter())
self.mount('http://', HTTPAdapter())
注意最后两行。Session 初始化时,会自动挂载两个适配器:一个处理 HTTPS,一个处理 HTTP。这两个适配器内部各自维护了一个连接池。
核心结论:Session 不直接管理连接池。它把连接池的创建、复用、清理全部委托给了适配器(Adapter)。
3.2 适配器(Adapter)到底是什么?
适配器是 requests 架构里一个很巧妙的设计。它把“发送请求”这个行为抽象成了一个接口。
看看 HTTPAdapter 的核心结构:
class HTTPAdapter(BaseAdapter):
def __init__(self, pool_connections=10, pool_maxsize=10,
max_retries=0, pool_block=False):
self.pool_connections = pool_connections
self.pool_maxsize = pool_maxsize
self.max_retries = max_retries
self.pool_block = pool_block
# 核心:初始化 urllib3 的 PoolManager
self.init_poolmanager(
connections=self.pool_connections,
maxsize=self.pool_maxsize,
block=self.pool_block
)
def init_poolmanager(self, connections, maxsize, block, **pool_kwargs):
self.poolmanager = PoolManager(
num_pools=connections,
maxsize=maxsize,
block=block,
**pool_kwargs
)
看到了吗?HTTPAdapter 内部持有一个 urllib3 的 PoolManager 实例。每个适配器都有自己的连接池。
我个人习惯把适配器理解为“连接池的管家”。你给它一个 URL 前缀,它就知道该用哪个连接池去处理。
3.3 Session 如何选择适配器?
当你调用 s.get(url) 时,请求链路是这样的:
Session.get()→Session.request()→Session.send()- 在
send()方法中,调用self.get_adapter(url)选择适配器 - 适配器调用
self.send()真正发送请求
关键就在 get_adapter 方法:
def get_adapter(self, url):
for (prefix, adapter) in self.adapters.items():
if url.lower().startswith(prefix.lower()):
return adapter
raise InvalidSchema(f"No connection adapters were found for {url}")
逻辑很简单:遍历 adapters 字典,找到匹配 URL 前缀的适配器。因为 https:// 比 http:// 先注册,所以 HTTPS 请求会优先匹配 HTTPS 适配器。
小技巧:你可以通过 s.get_adapter(url) 查看某个 URL 会被哪个适配器处理。这在调试自定义适配器时很有用。
3.4 适配器的核心作用
适配器不只是连接池的容器。它还负责:
| 职责 | 说明 | 对应方法 |
|---|---|---|
| 连接池管理 | 创建、复用、清理 HTTP 连接 | init_poolmanager() |
| 请求发送 | 将 requests 的 PreparedRequest 转为 urllib3 的请求 | send() |
| SSL 配置 | 证书验证、客户端证书、自定义 CA 包 | cert_verify() |
| 代理支持 | HTTP/HTTPS/SOCKS 代理 | proxy_headers() |
| 重试策略 | 连接重试、读取重试、状态码重试 | send() 内部 |
| 响应构建 | 将 urllib3 的 HTTPResponse 转为 requests 的 Response | build_response() |
嗯,这里要注意:适配器是 requests 扩展性的关键。你想支持自定义协议?写个适配器挂上去就行。
3.5 连接池参数怎么传?
默认情况下,每个适配器的连接池大小是 10。这在并发场景下远远不够。
我建议这样自定义:
import requests
from requests.adapters import HTTPAdapter
s = requests.Session()
# 为 HTTPS 创建专用适配器,连接池上限 50
adapter = HTTPAdapter(
pool_connections=50, # 连接池数量
pool_maxsize=50, # 每个连接池最大连接数
max_retries=3, # 最大重试次数
pool_block=False # 连接池满时是否阻塞
)
# 挂载到 Session
s.mount('https://', adapter)
s.mount('http://', adapter)
这里有个坑,我曾经踩过:如果你分别挂载 HTTP 和 HTTPS 适配器,它们各自维护独立的连接池。也就是说,上面的代码实际上创建了两个连接池,每个最大 50 个连接。
避坑指南:我曾经在一个微服务网关项目中,给每个目标服务都创建了独立的 Session。结果连接数暴涨,把服务器端口耗尽了。后来改成共享 Session + 按域名挂载适配器,问题才解决。
3.6 适配器的挂载机制
mount() 方法做了什么?源码很简单:
def mount(self, prefix, adapter):
self.adapters[prefix] = adapter
# 按前缀长度降序排序,确保更精确的匹配优先
keys_to_move = [k for k in self.adapters if len(k) < len(prefix)]
for key in keys_to_move:
self.adapters[key] = self.adapters.pop(key)
注意排序逻辑:前缀越长的适配器,优先级越高。比如你同时挂了 https://api.example.com/ 和 https://,请求 https://api.example.com/v1/data 时会优先使用前者。
这个设计很实用。你可以为不同的 API 端点配置不同的连接池策略:
# 核心 API:大连接池,低超时
core_adapter = HTTPAdapter(pool_maxsize=100, max_retries=2)
s.mount('https://core.example.com/', core_adapter)
# 日志 API:小连接池,高重试
log_adapter = HTTPAdapter(pool_maxsize=5, max_retries=5)
s.mount('https://log.example.com/', log_adapter)
# 兜底:默认配置
default_adapter = HTTPAdapter(pool_maxsize=20)
s.mount('https://', default_adapter)
3.7 适配器与连接池的生命周期
为了让你更直观地理解,我画了一张图:
3.8 适配器的扩展:自定义协议
适配器模式的好处是——你可以轻松扩展。比如你想支持 Unix Socket 通信:
from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection
class UnixSocketConnection(HTTPConnection):
def __init__(self, socket_path):
self.socket_path = socket_path
super().__init__('localhost')
def connect(self):
import socket
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect(self.socket_path)
class UnixSocketAdapter(HTTPAdapter):
def __init__(self, socket_path, **kwargs):
self.socket_path = socket_path
super().__init__(**kwargs)
def init_poolmanager(self, *args, **kwargs):
self.poolmanager = PoolManager(
socket_path=self.socket_path,
**kwargs
)
# 使用
s = requests.Session()
adapter = UnixSocketAdapter('/var/run/docker.sock')
s.mount('http+docker://', adapter)
resp = s.get('http+docker://containers/json')
你看,只要继承 HTTPAdapter,重写连接创建逻辑,就能支持任意协议。这就是适配器模式的威力。
3.9 性能调优建议
基于源码分析,我总结几个实战要点:
- 复用 Session:不要每次请求都创建新 Session。Session 内部维护连接池,复用能减少 TCP 握手开销。
- 按场景调参:高并发场景把
pool_maxsize调到 50-200,低并发场景保持默认即可。 - 区分协议:HTTP 和 HTTPS 的适配器可以独立配置。HTTPS 有 SSL 握手开销,连接池可以适当放大。
- 注意线程安全:Session 不是线程安全的。多线程场景下,要么每个线程用独立 Session,要么加锁。
- 及时清理:长时间不用的 Session 调用
s.close(),释放连接池资源。
个人经验:我在做爬虫框架时,给每个目标网站分配一个 Session,连接池大小设为 20。这样既避免了连接数爆炸,又保证了每个站点的连接复用率。效果比全局共享一个 Session 好很多。