概述
连接池的槽位是固定的,pool.connect() 取出连接后必须有配对的 release()。如果提前 return 或抛错路径漏掉归还,这个槽位就会被占住直到进程结束。开发环境连接池有余量,漏一个看不出来;生产环境每次异常都累积一点,直到池被占满,所有请求阻塞在获取连接上。此时数据库本身是空闲的,故障却表现为数据库不可用。
步骤
- 包装
connect与release,统计未归还的取出次数:
export function trackedPool(pool) {
let outstanding = 0;
const connect = pool.connect.bind(pool);
pool.connect = async (...args) => {
const client = await connect(...args);
outstanding += 1;
const release = client.release.bind(client);
client.release = (...rest) => { outstanding -= 1; return release(...rest); };
return client;
};
return { pool, outstanding: () => outstanding };
}
- 把
max设为 2,用 6 个并发请求触发真实竞争,并设置较短的connectionTimeoutMillis。 - 断言
outstanding() === 0且pool.waitingCount === 0。 - 保留一个故意不归还连接的临时 handler,确认测试会在约 750 毫秒后失败,证明守卫本身有效。
验证
正确实现下 node --test test/pool-leak.test.js 应全部通过;把某个 catch 分支里的 client.release() 注释掉后,测试应报 timeout exceeded when trying to connect 而不是一直挂起。若把连接池上限调大后测试仍然通过,说明并发没有真正超过上限,需要检查请求是否被串行化。
注意事项
- 计数器只能观察经过被包装实例取出的连接,直接新建的 Client 不在统计内。
- 该测试发现的是“忘记归还”,不是“查询过慢长时间占用连接”,后者应结合
pg_stat_activity监控。 - 建议提供
withClient(fn)辅助函数并用 lint 禁止在辅助函数之外直接调用pool.connect(),从结构上消除泄漏。 - 处于 idle in transaction 超过一分钟的会话也应告警,并把 SQL 文本带进告警内容。