SQLx 查询超时后池一直报 PoolTimedOut,连接可能卡在关闭阶段
SQLx、rustls 与 PgBouncer 的特定取消路径可能让关闭任务一直占着池名额。正式 0.9.0 可在受控查询前标记 close_on_drop,采用有界关闭,并评估连接重建成本。
Web 请求已经超时结束,SQLx 的连接池却迟迟没有空闲名额,后续请求不断收到 PoolTimedOut。若日志先出现 error occurred while testing the connection on-release,而全新连接又能查询数据库,要检查名额是否留在后台关闭任务里。
这条故障的具体组合是 SQLx 0.8.6、rustls、PostgreSQL 17,以及采用事务池模式并启用客户端 TLS 的 PgBouncer 1.22.1 或 1.26.0。请求取消发生在语句尚未完成时,之后服务端的 statement_timeout 取消语句,连接归还时的探测收到错误,转入关闭路径。不能用它解释所有池超时。
业务 future 结束,不代表关闭任务结束
池连接被丢弃时,SQLx 会启动后台归还任务。任务先检查连接状态;探测失败后执行 close_hard(),并在关闭完成前持有池名额。
问题在于这条 TLS 路径的 flush 不只尝试写出数据,还会进入读写完成逻辑。当 PgBouncer 已发送最后的错误、随后保持安静时,关闭操作可能继续等待对端数据。业务已经退出,名额却没有归还,所以只看正在执行的业务任务,容易误判成“池明明没人用”。
正式 SQLx 0.9.0 的相关实现仍有这条调用关系,不能仅凭从 0.8.x 升到 0.9.0 就认定关闭等待已经修好。close_hard 的名称也不等于这条实现绝不会等待 TLS。
保存依赖和 TLS feature、取消时序、归还探测日志,以及池的 size 和 num_idle 趋势。size 达上限、idle 为零还不足以定性,需进一步从任务诊断确认阻塞发生在归还、TLS flush 或关闭,而非仍在运行的查询。
acquire_timeout 只能限制排队
acquire_timeout 约束的是调用方等候取得连接的时间,不会替后台归还任务设置关闭期限。调小它可以让后续请求更早返回错误,但不会把已占住的名额释放出来;不断扩大池容量则可能只是晚一点再次耗尽。
调整服务端语句期限,使查询有机会在请求被取消之前结束,可以减少这条路径的触发,但仍需为网络断开、调用方主动取消等情况保留处理。不能承诺只改两个超时值就消除了所有取消竞态。
不要通过去掉 sslmode=require 或关闭证书校验绕过 TLS。对照非 TLS 行为只能帮助定位,不能成为生产降级传输保护的理由。
0.9.0 可在查询前标记 close_on_drop
正式 SQLx 0.9.0 的 PoolConnection::close_on_drop() 提供了更小的处理入口:在取得连接后、进入可能被取消的查询之前标记它。该连接丢弃时不走常规归还探测,而是启动关闭任务;这条任务对关闭等待设有 5 秒的内部超时。
下面是已有 Tokio 运行时和 PgPool 上的只读函数,SELECT 1 仅表示受控读取。应用若在外层取消整个函数,连接会随函数退出而丢弃:
async fn run_readonly(pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
let mut conn = pool.acquire().await?;
conn.close_on_drop();
sqlx::query("SELECT 1").execute(&mut *conn).await?;
Ok(())
}
标记必须在可取消的 await 之前设置,不能等连接已经进入后台归还后再补。这个选择会在成功路径也关闭连接,增加新连接与 TLS 握手开销;只对已经确认需要该生命周期的操作采用,并核对池容量和数据库连接预算。
内部 5 秒超时限制的是等待关闭的 future,不是承诺 TCP 连接立刻消失,也不是业务请求的超时保证。运行时应继续运行以执行清理任务;验收时观察名额释放、客户端资源和服务端会话,不能只等业务函数返回。这里核对的是正式 0.9.0 的实现,旧版本需先核对是否具有同样的标记和有界关闭逻辑,不能直接照搬版本结论。
另一种隔离方式是让少量可取消任务自己持有普通短连接,但它绕过共享池的容量管理,需要独立的全局并发预算和连接建立成本评估。已有连接调用 detach() 也会释放池名额,但连接本身仍活着,池还可能创建替代连接,总数可以超过 max_connections。不要把它当成每次超时后的通用补丁;leak() 还会永久减少池容量。
这些措施都不保证任意写操作在超时前没有发生。先在只读任务上受控验证;写入、事务或提交状态不明确时,仍需按业务幂等记录核对,不能自动重放。
已经耗尽的池可按应用原有维护流程排空并替换,但在途请求应先停止接收、记录结果不确定的操作。替换进程只恢复资源,不证明根因已经消失。复查仍要覆盖同样的请求取消、服务端取消与 TLS 代理组合,确认负载结束后空闲名额恢复,并观察是否还有后台关闭任务持续滞留。


