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) 时,请求链路是这样的:

  1. Session.get() → Session.request() → Session.send()
  2. 在 send() 方法中,调用 self.get_adapter(url) 选择适配器
  3. 适配器调用 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 适配器与连接池的生命周期

为了让你更直观地理解,我画了一张图:

Session 与适配器、连接池的关系 requests.Session HTTPAdapter (http://) HTTPAdapter (https://) PoolManager (num_pools=10) PoolManager (num_pools=10) ConnectionPool 1 ConnectionPool 2 ConnectionPool 3 ConnectionPool N HTTPConnection HTTPConnection HTTPSConnection HTTPSConnection 关键点 每个适配器持有独立的 PoolManager → 每个 PoolManager 管理多个 ConnectionPool → 每个 ConnectionPool 维护多个连接实例

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 好很多。