泄漏的连接会把连接池榨干
连接池泄漏首先是所有权问题:业务工作已经结束,但它借走的连接仍然处于借出状态。此时数据库可能仍然正常响应其他客户端,只有这个应用拿不到连接。应把归还资源视为操作契约的一部分,覆盖异常和提前返回,而不是只在成功路径末尾顺手清理一下。 一份借用应当有明确的主人和终点 SQLAlchemy Connection 表示从 Engine 所属池中取得的一次借用。关闭这个 Connection,通常是把底层 DBAPI 连接还回池里,而不一定真正断开数据库连接。归还借用与提交事务是两个独立决定:池可以运转正常,但业务修改已经回滚;池也可能耗尽,而底层数据库连接全部仍然存活。只检查连接是否活着,无法回答谁还占着它。 资源作用域应尽量短,但不能破坏事务要求的原子性。with engine.begin() 在代码块成功时提交,失败时回滚,并释放连接;with engine.connect() 管理的是连接作用域,不承诺自动提交写入,关闭时尚未结束的事务会被回滚。两者看起来都用了 with,但持久化语义不同,不能为了“统一写法”随意互换,否则资源不再泄漏了,数据却可能没有保存。 与当前事务无关的远程等待、用户交互,不应长时间占着事务和连接。这并不意味着可以把必须一起提交的变更拆开。若连接由调用方拥有,辅助函数不应私自关闭;若辅助函数自己借用,就应负责归还,或者在接口中明确把所有权交给谁。只有打开次数的指标,却说不清每份借用由谁结束,仍然没有定位真正的生命周期边界。 坏掉的样子 漏掉一次归还,就减少一份可用容量,后来者开始排队或超时。对于配置了有限容量和获取超时的 QueuePool,新调用并非一定永远挂住,而是可能在执行 SQL 之前就收到池超时。把上限调大可以暂时推迟复发,却也增加数据库压力;缺失的归还路径依然存在,不能算修复。 不过,“池满了”不是泄漏的充分证据。真实并发、长事务、数据库锁等待或应用自身死锁,都可能占住全部借用。数据库 CPU 很低也不决定原因,因为应用可能握着连接等待别的事情。需要把借出数量、获取等待、持有年龄、事务状态和仍在进行的业务工作关联起来,判断这些连接是否还处于合理的使用范围。 一个有说服力的泄漏复现,应证明已经结束的工作仍然保留借用,并且归还这些具体借用后容量恢复。不要依赖垃圾回收及时释放;对象何时被回收不是可靠的资源归还契约。Engine.dispose() 也不能强制收回仍然借出的连接,它处理的是池内已经空闲的连接。重启进程可能暂时清空症状,但不会改变下一次请求重新走进同一遗漏分支的事实。 快速验证 在 macOS 或 Linux 的空目录运行 python3 -m venv .venv,然后执行 .venv/bin/python -m pip install SQLAlchemy==2.0.52。把下面保存为 pool_probe.py,执行 .venv/bin/python pool_probe.py。本次使用 Python 3.14.4 和 SQLite 3.53.0,数据库文件只存在于脚本自己的临时目录,结束后自动清理,不会访问生产数据库。 ...