Python 3.14 增量 GC:从 3.14.0 上线到 3.14.5 撤回,半年的生产事故暴露了 GC 设计的权衡本质
Python 3.14.5 最引人注目的变更是把 3.14.0 引入的增量垃圾回收器(incremental GC)回滚掉(此次发布还包含了约 154 个其他 bug 修复和文档改进)。这个决定背后是生产环境的反馈——由于工作负载的不同,升级到 3.14 的部分服务内存持续攀升。一个旨在提升性能的改动,最终被完整撤销。Python 的传统 GC 是分代回收:对象被分为三代(0、1、2),绝大多数对象在第 0 代就消亡,只有少数晋升到第 2 代后才触发代价更高的完整扫描。这个机制的问题在于:对于大型堆,完整的第 2 代扫描会造成数百毫秒的停顿,在延迟敏感的 Web 服务里会直接影响请求响应时间。增量 GC 的改动是:把三代简化成两代(年轻代 young + 老年代 old),每次 GC 触发时只扫描年轻代,加上老年代的一个"增量片段"(increment)。CPython 的 PR 承诺最大停顿时间减少一个数量级以上(对大型堆而言)。增量方案的副作用之一是 GC 触发频率降低。传统方案一旦检测到第 0 代的循环引用积累,会立刻触发;增量方案会把这些对象留在那里等待下一个增量周期。对于每次请求都会创建大量短命对象的异步工作负载——比如 httpx 和 urllib3 在每次 TLS 握手时创建的 SSL 上下文、证书对象、读写缓冲区——这些对象会在老年代里堆积,等待一个迟到的增量来处理它们。最终结果是进程 RSS 持续增长,即使 Python 堆里的"存活对象"数量并不多。社区中发现的典型内存上涨场景通常涉及大量依赖异步请求的框架。共同特征都是:高频对象创建与销毁,加上对象间的隐式循环引用(例如 TLS/SSL 验证时的上下文与读写缓冲区)。Python 官方团队在发现问题后做出回退决定。这意味着增量 GC 方案在 3.14.5 发布前的旧版本(3.14.0–3.14.4)都属于"有已知内存风险"的状态。不少生产系统已在运行这些版本并面临内存压力。撤回的影响比单纯"内存稳定下来"更大。部分开发者在实测中发现:回退 GC 之后,内存占用不只是稳定,基准值甚至更为优秀——增量 GC 的内存压力此前掩盖了 3.14 系列其他性能改进的收益。使用 Python 3.14.0–3.14.4 的生产服务,尤其是使用 httpx、urllib3 或任何异步 SSL 客户端的服务,升级到 3.14.5 是当前的直接行动项。增量 GC 的撤回不是增量回收这个概念本身的失败。Go 的并发三色标记 GC、JVM 的 ZGC、Rust 基于所有权的静态内存管理——每种方案都在停顿时间、内存开销、吞吐和工程复杂度之间做出了不同取舍。没有一种方案能同时赢得所有维度。Python 这次的具体教训是:为"减少停顿"设计的方案,在"对象批量创建且带有隐式循环引用"的工作负载下,会把内存压力推迟到一个更晚的时间点积累,最终表现为无法通过常规重启解决的 RSS 增长。理解了这个机制,遇到 Python 服务内存缓慢爬升时,版本号是第一个需要排查的点。