← 中文资源

简体中文版本

用测试捕获连接池泄漏

把并发压到连接池上限以上,断言每个取出的连接都被归还,让遗漏的 release 在 CI 失败而不是在生产挂起。

阅读英文原文

概述

连接池的槽位是固定的,pool.connect() 取出连接后必须有配对的 release()。如果提前 return 或抛错路径漏掉归还,这个槽位就会被占住直到进程结束。开发环境连接池有余量,漏一个看不出来;生产环境每次异常都累积一点,直到池被占满,所有请求阻塞在获取连接上。此时数据库本身是空闲的,故障却表现为数据库不可用。

步骤

  1. 包装 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 };
}
  1. 把 max 设为 2,用 6 个并发请求触发真实竞争,并设置较短的 connectionTimeoutMillis。
  2. 断言 outstanding() === 0 且 pool.waitingCount === 0。
  3. 保留一个故意不归还连接的临时 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 文本带进告警内容。