概述
NAT 网关、防火墙与负载均衡器会静默回收空闲连接,两端都收不到 FIN,连接池却仍认为套接字可用。下一条查询写进黑洞后,TCP 重传可能要几分钟才报错。用户看到的是接口一直挂起,而简单重试还可能取到同一个失效连接。
步骤
- 在连接池配置中同时限制空闲时间、获取时间与连接总生命周期:
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 2_000,
maxLifetimeSeconds: 1_800,
});
- 开启 TCP keepalive,让操作系统在网络层发现对端消失:
keepAlive: true,
keepAliveInitialDelayMillis: 10_000,
- 取用连接后执行一次
SELECT 1探活,失败时用client.release(err)销毁连接再重新获取。注意不能无参释放,否则坏连接会回到池中继续被分发:
try { await client.query('SELECT 1'); return client; }
catch (err) { client.release(err); return pool.connect(); }
- 用
pg_terminate_backend杀掉一个空闲后端,再发起请求验证仍返回 200。
验证
pg_stat_activity 中应看到旧后端消失、新后端建立,探活失败计数增加一次。随后持续观察 waitingCount:短暂大于 0 属于正常竞争,持续大于 0 说明连接池已饱和,需要扩池或缩短事务。若每个请求都多出一次往返,应改为仅在空闲超过阈值时探活。
注意事项
- keepalive 的具体时序取决于操作系统与中间网络设备,必须按真实链路验证,不能只看本机。
- 连接可能在取用之后、提交之前断开,写操作仍需幂等与有界重试。
maxLifetimeSeconds是渐进替换,无法立刻修复已经装满坏连接的池。- 网络抖动后若连接池迅速归零,通常是没有在探活失败时销毁连接。
- 探活是最后一道防线而不是性能优化,优先让 keepalive、空闲超时与生命周期上限覆盖绝大多数场景,并为探活单独记录延迟。