编程语言细节往往隐藏着意想不到的陷阱。今天笔者与AI共同排查了一个教科书级别的生产环境问题,从原理看不复杂,它揭示了一些入门Python垃圾回收机制,但在快速迭代的代码中容易被忽略。笔者主要想展示: 在AI辅助编程定位问题方面的实验。正如我之前提到的,"笔者长期在cursor/windsurf之间徘徊",这次排查过程中,AI工具的分析能力和协作效率非常重要。
批量处理任务时偶现获取血缘数据关系失败,4月底某同学定位到图数据库连接限制错误("too many connections"),说明任务未正确释放资源。
随后其他同学修复版本1(2x.x.x):在数据库连接底层增加全局实例、复用全局实例,限制最大连接数量。
04-29上线后,数量有限的批处理任务下,未发现异常。04-30下午6点开始处理大批量任务时,执行一段时间后,开始出现数据库报错。后台反馈连接超过最大限制,于是停止服务。
奇怪的是,旧版本能处理大批量任务(20万条)且正常获取血缘数据关系。
代码架构变化导致Python垃圾回收机制行为差异:
本质问题:批量任务会导致大量外部资源(数据库连接)积累,一个版本使用了局部函数,会及时触发垃圾回收自动清理。而后续版本释放时机不可靠,取决于Python的GC活动。
解决方案1:手动触发垃圾回收——快速解决根本问题
解决方案2:基于目前的代码完善数据库连接底层兼容连接复用,即使上层未释放,也能不超过最大连接限制
●API——>服务层——>基础Agent——>上下文服务——>数据服务——>数据库服务
●批量服务——>服务层——>基础Agent——>上下文服务——>数据服务——>数据库服务
人思考1:排除API服务导致,因为生成API调用量少。
AI观察:批量创建服务,每次都会沿着调用链条创建数据库链接,确实没有主动释放链接的代码。
AI思考:全局实例 != 唯一连接,一个实例可能创建N个链接,所以这个修改并未解决问题。
人思考:纠结数据库如何从底层"兼容"控制连接复用,错误重试等没意义,因为我们最初的版本是正常的,那个时候,图数据库服务非常简单,不需要全局管理,也不需要复杂的处理。说明问题本是【某个版本上层代码或者架构变化导致】,那才是根源。
AI思考:批量创建服务,每次都会沿着调用链条创建数据库链接,确实没有主动释放链接的代码。
从代码看,没有释放连接的问题依旧存在。但为何之前正常?
人思考:如果从代码上确实存在批量创建Agent和数据库连接,那可能是释放的问题,也许旧代码可以自动释放,而新代码无法自动释放,什么情况下连接会自动释放——Python的垃圾回收?
编写几个测试脚本(3 个文件大概总1000行),完整地模拟整个流程:服务层——>基础Agent——>上下文服务——>数据服务——>数据库服务。
观察1:连接数不断增加,超过300,触发限流错误——✅正确复现生产错误。
观察2:连接数会不断被GC回收,永远不会超过1,不会累积——✅找到正确修复方案。
至此,已经确定了原因和修复方案。
继续思考:为何之前正常呢?
让AI观察更早的代码和上一个版本的服务层差异。
发现根本性问题:
更早的代码中,批量调用的时候在一个局部函数中,类似如下:
ounter(lineounter(lineounter(lineounter(lineounter(line
# 早期版本(工作正常)
def process_inference_task(...):
agent = create_agent(...) # 局部变量
result = agent.generate(...)
# 函数结束,agent立即失去引用
当函数结束时,其局部变量引用计数减少。如果函数没有return变量,这些变量的引用计数可能立即降为0,使它们成为优先回收对象。在我们的场景会自动触发数据库连接的结束。
而后续的版本架构调整,代码类似如下:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
# 新版本(25.3.4)(连接泄漏)
def _execute_agent(...):
agent = create_agent(...)
result = agent.generate(...)
return result, confidence, agent # 返回agent对象
def process_inference_task(...):
result, confidence, agent = _execute_agent(...)
# agent引用延长生命周期
创建"模拟原始版本"测试,在函数内创建和使用agent。 测试结果显示:函数局部变量模式下处理310个任务,连接数始终为0。 这解释了为什么早期版本能处理大批量任务而不触发连接限制。
Python垃圾回收系统由两个核心机制组成:
原版本垃圾回收行为:
ounter(lineounter(lineounter(lineounter(lineounter(line
def process_inference_task(...):
agent = create_agent(...) # 引用计数=1
result = agent.generate(...)
# 函数结束,agent引用计数-1变为0
# 立即被回收,数据库连接关闭
新版本垃圾回收行为:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
def _execute_agent(...):
agent = create_agent(...) # 引用计数=1
result = agent.generate(...)
return result, confidence, agent # 返回保持引用计数=1
def process_inference_task(...):
result, confidence, agent = _execute_agent(...)
# 只有process_inference_task结束时引用计数才-1
Python垃圾回收阈值通过以下参数控制:
ounter(lineounter(lineounter(line
import gc
gc.get_threshold()
(700, 10, 10) # 典型默认值
意味着:
在批处理环境中:
这完美解释了偶发性问题:在不同的服务器负载、内存压力和并发级别下,垃圾回收触发时机不同,某些情况下会在达到连接限制前回收资源,而某些情况下不会。
问题是Python垃圾回收时机与外部资源限制之间的竞争:
这是依赖垃圾回收管理外部有限资源的典型陷阱,解决方案必须采用确定性的资源释放策略,而非依赖垃圾回收的不确定行为。
⚠️ 本文包含视频内容可能无法正常播放。
原文链接:点击查看微信公众号原文