第33章:源码剖析——sessions.py(Session核心调度与状态管理)
1. 项目背景业务场景某微服务网关团队使用 requests 构建了内部 API 调用层。在排查一个请求偶尔不带 Authorization 头的 bug 时他们发现 Session 的全局auth属性在某些路径下被意外清除了。通过阅读sessions.py源码他们找到了根因merge_setting()函数在合并 Session 设置和单次请求设置时的优先级逻辑——当单次请求传了authNone时它会覆盖 Session 的全局 auth而不是忽略 None 使用默认值。另一个问题更致命他们创建了一个 Session 并在全局使用但没有调用close()。代码在持续运行 24 小时后开始报ConnectionPool is full错误。阅读源码后发现Session 的close()负责清理底层 urllib3 连接池而 Python 的 GC 不会自动触发——必须显式调用。痛点问题一Session.request() 是核心调度器但知其然不知其所以然。几乎所有 requests 调用最终都走到了Session.request(method, url, **kwargs)方法。这个方法内部做了参数规范化、环境变量合并、Request 构建、PreparedRequest 准备、Auth 应用、Redirect 处理——一套完整的流水线。不理解这套流程就无法精准控制请求行为。问题二merge_setting 的优先级逻辑不透明。Session 级别有默认headers、auth、hooks、proxies单次请求也可以传这些参数。两者的合并规则是什么源码中的merge_setting()使用有则覆盖策略——意味着单次请求传一个空值会覆盖 Session 的非空默认值。这不是简单的默认值语义。问题三环境变量合并的隐式行为。merge_environment_settings()会从环境变量读取HTTP_PROXY、HTTPS_PROXY、NO_PROXY并自动注入到请求中。如果你的系统环境变量配置了一个不可用的代理所有请求都会静默失败——而你根本没意识到是环境变量在作祟。sessions.py 核心流程session.get(url, **kwargs) └─ session.request(GET, url, **kwargs) ├─ 构建 Request(GET, url, **kwargs) ├─ merge_environment_settings(url, proxies, stream, verify, cert) │ └─ 读取 HTTP_PROXY / NO_PROXY 环境变量 ├─ session.prepare_request(Request) │ └─ 调用 PreparedRequest 的五个 prepare_* 方法 ├─ merge_hooks() 合并 Session hooks 和单次请求 hooks ├─ session.send(PreparedRequest, **kwargs) │ ├─ 获取适配器: adapter self.get_adapter(url) │ ├─ 应用 auth: request self.auth(request) │ ├─ 发送: adapter.send(request, **kwargs) │ └─ 处理重定向 (循环直到非 3xx) └─ 返回 Response2. 项目设计小胖迷惑地盯着服务器日志“大师我们的网关服务跑了 24 小时后就报ConnectionPool is full然后所有 API 调用都失败了。重启之后又好了。我明明创建了 Session 啊——应该会自动复用连接才对”大师查看代码“你创建了 Session但你没调用session.close()。Session 的底层连接池不会自动释放——即使在 Python 垃圾回收时也不会。如果你不断创建 Session 而不关闭每个 Session 都会留下几个 TCP 连接处于CLOSE_WAIT状态。24 小时后自然是连接池溢出。”小白追问“那session.request()的内部流程是怎样的它跟requests.get()是什么关系”大师“requests.get(url)是session.request(GET, url)的快捷方式。它们的区别在于requests.get()每次创建一个临时 Session用完即弃而session.get()用的是同一个 Session连接复用、Cookie 保持。但两者最终都调用同一个核心方法——Session.request()。”# sessions.py 核心逻辑 (简化版)classSession:defrequest(self,method,url,**kwargs):# 1. 创建 Request 对象reqRequest(method,url,**kwargs)# 2. 准备 (prepare)prepself.prepare_request(req)# 3. 合并环境变量 (proxies, SSL)proxieskwargs.get(proxies,{})settingsself.merge_environment_settings(prep.url,proxies,None,None,None)# 4. 发送respself.send(prep,**settings)# 5. 返回returnresp小胖“那merge_environment_settings是什么我发现我的服务在 K8s 里跑得好好的但到某台开发机就跑不动了——全报 ProxyError。”大师“这就是环境变量的坑。merge_environment_settings()会读取HTTP_PROXY、HTTPS_PROXY、NO_PROXY环境变量并自动应用到请求中。你那台开发机上可能 IT 部门设了全局代理但代理不可用。”“你可以通过trust_envFalse来禁用这个行为——这是排查代理相关问题的第一步。”# 禁用环境变量代理sessionrequests.Session()session.trust_envFalse# 或单次请求requests.get(url,trust_envFalse)小白“那merge_setting呢我设了session.headers {X-Key: session}但有些请求不需要这个头——我怎么覆盖它”大师“merge_setting的逻辑很简单——单次请求的值覆盖 Session 默认值。但如果你传headers{X-Key: None}它会把 X-Key 从头中删除。如果你想保持 Session 的值不变就不要在单次请求中传这个 key。这不是’默认值覆盖’的模式而是’全量替换合并’。”生活比喻技术映射交通指挥中心调度车辆Session.request() 核心调度车辆出厂默认配置Session 级别的 headers/auth/proxies单次出车特殊配置单次请求参数覆盖默认值GPS 自动选择路线get_adapter() 按 URL 前缀匹配车辆到站后入库保养session.close() 清理资源3. 项目实战环境准备# 找到 sessions.py 源码python-cimport requests; import os; print(os.path.dirname(requests.__file__))分步实现步骤一Session.request() 完整链路追踪目标通过 Monkey-Patch 打印 Session.request() 内部流程。importrequestsfromrequestsimportSession,Requestdeftrace_session_request():追踪 Session.request() 的完整调用链sSession()s.headers[X-Default]from-session# Monkey-patch 各个步骤来追踪original_prepares.prepare_request original_sends.senddeftraced_prepare(req):print(f[prepare]{req.method}{req.url})preporiginal_prepare(req)print(f - headers:{dict(prep.headers)})returnprepdeftraced_send(prep,**kwargs):print(f[send]{prep.method}{prep.url})print(f - timeout:{kwargs.get(timeout)})print(f - proxies:{kwargs.get(proxies)})resporiginal_send(prep,**kwargs)print(f -{resp.status_code}({resp.elapsed.total_seconds():.3f}s))returnresp s.prepare_requesttraced_prepare s.sendtraced_send# 发起请求——观察流程print( Session.request 链路追踪 )resps.get(https://httpbin.org/get,params{q:test},headers{X-Custom:from-request},timeout5)print(f\n最终响应:{resp.status_code})s.close()trace_session_request()步骤二merge_environment_settings 实验目标验证环境变量对代理行为的影响。importrequestsimportosdefdemo_environment_merge():演示环境变量如何影响请求行为# 场景1: 不设置环境变量 print( 场景1: 无环境变量 )sessionrequests.Session()settingssession.merge_environment_settings(https://httpbin.org/get,{},None,None,None)print(f proxies:{settings.get(proxies,未设置)})# 场景2: 设置 HTTP_PROXY print(f\n 场景2: 设置 HTTP_PROXY )os.environ[HTTP_PROXY]http://fake-proxy:8888settingssession.merge_environment_settings(https://httpbin.org/get,{},None,None,None)print(f proxies:{settings.get(proxies)})# 场景3: trust_envFalse 禁用 print(f\n 场景3: trust_envFalse )session.trust_envFalsesettingssession.merge_environment_settings(https://httpbin.org/get,{},None,None,None)print(f proxies:{settings.get(proxies,未设置(已禁用))})# 清理os.environ.pop(HTTP_PROXY,None)session.close()demo_environment_merge()步骤三从源码角度理解 redirect 处理目标理解 Session.send() 如何循环处理重定向。importrequestsdefdemo_redirect_handling():演示 Session.send() 的重定向循环逻辑# 源码简化版:# while True:# resp adapter.send(request, **kwargs)# if not allow_redirects:# break# if resp.status_code not in (301,302,303,307,308):# break# if len(history) max_redirects:# raise TooManyRedirects# request rebuild_request_for_redirect(resp)# history.append(resp)# return build_response(req, resp)sessionrequests.Session()# 正常重定向respsession.get(https://httpbin.org/redirect/3,timeout10)print(f重定向{len(resp.history)}次 -{resp.status_code})fori,hinenumerate(resp.history):print(f [{i}]{h.status_code}-{h.headers.get(Location,N/A)[:50]})# 禁用重定向respsession.get(https://httpbin.org/redirect/1,allow_redirectsFalse,timeout10)print(f\n禁用重定向:{resp.status_code})# 限制最大重定向try:session.get(https://httpbin.org/redirect/10,max_redirects3,timeout10)exceptrequests.exceptions.TooManyRedirectsase:print(f\n重定向超出限制:{e})session.close()demo_redirect_handling()可能遇到的坑及解决方法坑1Session.auth 被单次请求 authNone 覆盖# session.auth HTTPBasicAuth(u,p)# resp session.get(url, authNone) # 覆盖了全局 auth!# merge_setting 不会把 None 当作使用默认值坑2trust_envFalse 不影响显式传递的 proxies# trust_envFalse 只屏蔽环境变量代理不影响显式 proxies参数session.trust_envFalsesession.get(url,proxies{https:http://explicit-proxy:8080})# 仍生效坑3Session 不 close 导致文件描述符泄露# 建议使用 with 语句或 try/finallywithrequests.Session()ass:s.get(url)# 自动调用 close()测试验证importpytestimportrequestsclassTestSessionInternals:验证 Session 内部机制deftest_session_request_method(self):srequests.Session()# session.request 是核心调度方法resps.request(GET,https://httpbin.org/get)assertresp.status_code200s.close()deftest_trust_env_disables_env_proxy(self):importos srequests.Session()s.trust_envFalseos.environ[HTTP_PROXY]http://fake:9999# trust_envFalse 应该忽略环境变量try:resps.get(https://httpbin.org/get,timeout5)assertresp.status_code200finally:os.environ.pop(HTTP_PROXY,None)s.close()deftest_session_headers_overridden_by_request(self):srequests.Session()s.headers[X-Def]sessionresps.get(https://httpbin.org/headers,headers{X-Def:request})hresp.json()[headers]# 单次请求的值覆盖 Session 默认值asserth[X-Def]requests.close()4. 项目总结核心源码要点方法关键逻辑常见问题Session.request()构建Request-prepare-merge_settings-send核心调度入口merge_environment_settings()读取环境变量代理/SSL环境变量干扰请求merge_setting()单次请求值覆盖Session默认None 值也会覆盖get_adapter()按URL前缀匹配适配器mount 的匹配规则Session.send()应用auth-发送-处理重定向重定向循环控制Session.close()清理所有适配器的连接池未 close 导致资源泄漏适用场景排查代理莫名其妙生效的问题理解 Session 默认值与请求参数的优先级优化连接池生命周期管理自定义中间件时需要理解 Session 的调度流程注意事项Session 用完后必须调用close()或使用with语句trust_envFalse可禁用环境变量代理干扰merge_setting的覆盖语义不同于默认值Session.send() 内部有重定向循环非单次发送常见踩坑经验案例一环境变量代理导致全站瘫痪。K8s 集群中设置了一个全局HTTP_PROXY环境变量指向公司代理。某天代理故障所有微服务之间的 HTTP 调用全部失败。运维排查了 2 小时才发现是代理问题——因为没有人显式配置过代理。根因环境变量被 requests 静默读取。修复所有服务设置session.trust_env False显式使用网络策略。案例二Session.close() 未调用导致 FD 耗尽。某定时任务每分钟创建一次 Session 发请求没有调用 close()。运行一周后报 “Too many open files”。排查发现 10080 个文件描述符未被释放——每个 Session 至少占用 1 个 socket FD。根因Session 未关闭。修复使用with Session() as s:或任务结束时s.close()。思考题源码题阅读sessions.py中rebuild_auth()相关的源码在Session.send()中找出 requests 是如何处理 HTTP 401 挑战和 Digest Auth 的 auto-retry 的设计题基于对 Session 内部机制的理解设计一个Session 池——类似数据库连接池预创建 N 个 Session 对象Worker 线程从中获取、使用、归还而不是每次都创建和销毁。推广计划提示开发团队建议通读sessions.py的Session.request()和Session.send()两个方法理解请求的完整生命周期运维团队关注HTTP_PROXY/HTTPS_PROXY环境变量的全局影响范围步骤四Session 生命周期与资源管理目标理解 Session.init到 close() 的完整生命周期。importrequestsfromrequests.adaptersimportHTTPAdapterimportgcdefdemo_session_lifecycle():演示 Session 的完整生命周期# ---- 创建 ----print( Session 生命周期 )srequests.Session()print(f1. 创建 Session)print(f 默认 headers:{dict(s.headers)})print(f 默认适配器:{list(s.adapters.keys())})print(f trust_env:{s.trust_env})# ---- 配置 ----s.headers.update({X-App:demo})s.auth(user,pass)print(f\n2. 配置后)print(f headers:{dict(s.headers)})print(f auth:{s.auth})# ---- 使用 ----resps.get(https://httpbin.org/get,timeout10)print(f\n3. 发出请求:{resp.status_code})# ---- 查看状态 ----print(f\n4. 会话状态)print(f cookies:{len(s.cookies)}个)adapters.get_adapter(https://httpbin.org/get)pooladapter.poolmanagerprint(f 连接池类型:{type(pool).__name__})# ---- 关闭 ----s.close()print(f\n5. 已关闭)# 验证: 再次使用会怎样try:s.get(https://httpbin.org/get,timeout5)exceptExceptionase:print(f 关闭后请求:{type(e).__name__})demo_session_lifecycle()步骤五merge_setting 的完整优先级实验目标通过实验验证 Session 设置和单次请求参数的合并逻辑。importrequestsfromrequests.authimportHTTPBasicAuthdefdemo_merge_setting_priority():验证 merge_setting 的优先级srequests.Session()s.headers[X-From-Session]session-headers.headers[X-Common]from-sessions.authHTTPBasicAuth(session_user,session_pass)# 场景1: 使用 Session 默认值 resps.get(https://httpbin.org/headers)hresp.json()[headers]print(f场景1 - Session 默认头:)print(f X-From-Session:{h.get(X-From-Session,N/A)})# 场景2: 单次请求覆盖 resps.get(https://httpbin.org/headers,headers{X-Common:from-request})hresp.json()[headers]print(f\n场景2 - 单次请求覆盖:)print(f X-Common:{h.get(X-Common,N/A)})# from-request# 场景3: headersNone 不会清除默认头 # (merge_setting 判断 if value is not None)# 场景4: auth 的合并规则 resp1s.get(https://httpbin.org/basic-auth/session_user/session_pass)print(f\n场景4 - Session auth:{resp1.status_code})resp2s.get(https://httpbin.org/basic-auth/override/override,authHTTPBasicAuth(override,override))print(f场景4 - 覆盖 auth:{resp2.status_code})s.close()demo_merge_setting_priority()补充踩坑经验案例三Session 还原点——Cookie 持久化。某长生命周期服务在内存中持有 Session。运维重启服务后所有用户的登录状态丢失Session.cookies 丢失导致后续请求全部 401。团队花了半天时间才定位到不是代码 bug 而是重启导致的 Cookie 丢失。根因Session 状态仅在内存中。修复服务启动时从 Redis/文件加载持久化的 Cookie服务关闭时保存 Cookie 到持久化存储。案例四trust_env 的全局影响。开发在本地调试时通过export HTTP_PROXYhttp://localhost:8888设置代理抓包。忘了清除环境变量就提交了代码CI 环境中没有这个代理——所有请求全报 ProxyError。根因环境变量的全局隐式影响。修复代码中显式设置session.trust_env False仅通过代码配置代理。思考题补充设计题基于对 Session 内部机制的理解设计一个Session 池——类似数据库连接池预创建 N 个 Session 对象Worker 线程从中获取、使用、归还而不是每次都创建和销毁。需要考虑连接池参数调优、Session 状态清理Cookie/auth、连接有效性检查。延伸阅读与资源Milvus向量数据库实战修炼从 0 到 1精通向量检索与生产落地后端工程师的 AI 转型第一课Ollama 与私有化大模型实战10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析