从 #3759 的实测里掉出来的真问题 —— #3759 本身的前提("driver 不会重连")已被实验推翻并关闭,但过程中撞到了这个。
一句话说明
当数据库端点接受 TCP 但不完成握手(过载的实例、半开的防火墙、卡住的 LB、failover 中途的 VIP),每个查询挂满 30 秒才失败,报的是 Knex: Timeout acquiring a connection. The pool is probably full —— 而池并没有满。运维照这个提示会去调 pool.max,问题却在网络。
事实核实
packages/cli/src/utils/storage-driver.ts:214-219 构造 pg driver:
new SqlDriver({
client: 'pg',
connection: databaseUrl, // ← 字符串 URL
pool: { min: 0, max: 5 }, // ← 没有任何超时设置
autoMigrate: isDev ? 'safe' : undefined,
})
SqlDriver 构造函数(sql-driver.ts:568-572)原样透传给 knex,不注入任何默认超时。于是三个边界全部来自库的默认值。
用一个"接受连接后永远不说话"的监听器实测四种配置:
A. today (no bound set) 30024ms Knex: Timeout acquiring a connection. The pool is proba…
B. acquireConnectionTimeout 3s 3003ms Knex: Timeout acquiring a connection. The pool is proba…
C. pool.createTimeoutMillis 3s 3004ms Knex: Timeout acquiring a connection. The pool is proba…
D. connection.connectionTimeoutMillis 3s 3006ms timeout expired
两个结论:
- 不是无界,是 30 秒 —— tarn 的
createTimeoutMillis 默认值兜住了。(我一开始以为是无界,那是因为测量上限只有 12 秒;如实修正。)
- 只有 D 给出准确的诊断。 B/C 都在 knex 层限时,吐出的都是那句关于池子的误导性文案。D 在 node-postgres 自己那层限时,说的是
timeout expired —— 连接尝试超时,正是实情。
影响
建议处置
原则:平台在请求路径上发起的每一次网络操作,都该有一个我们自己选定的上限,而不是从库的默认值里继承一个。
storage-driver.ts 是我们自己从 URL 构造配置的地方,改用对象形式并设 connectionTimeoutMillis(建议 10s,与 driver-mongodb 既有的 connectTimeoutMS ?? 10_000 对齐)。实测该形式在 ?sslmode=require 下同样正确限时。
- 注意副作用:改成
{ connectionString, … } 后 connectionSettings 不再被 knex 预解析成 {host, port, database},serve.ts 的 describeRegisteredDriver 读 conn.host 会退化成 (unknown) —— 启动横幅要同步适配。
SqlDriver 自身补一个 dialect 无关的 pool.createTimeoutMillis 默认值作为兜底,覆盖那些自行组装 driver 的宿主(cloud/EE)。
- 顺带复核
acquireConnectionTimeout(knex 默认 60s)在请求路径上是否也偏长。
关联
从 #3759 的实测里掉出来的真问题 —— #3759 本身的前提("driver 不会重连")已被实验推翻并关闭,但过程中撞到了这个。
一句话说明
当数据库端点接受 TCP 但不完成握手(过载的实例、半开的防火墙、卡住的 LB、failover 中途的 VIP),每个查询挂满 30 秒才失败,报的是
Knex: Timeout acquiring a connection. The pool is probably full—— 而池并没有满。运维照这个提示会去调pool.max,问题却在网络。事实核实
packages/cli/src/utils/storage-driver.ts:214-219构造 pg driver:SqlDriver构造函数(sql-driver.ts:568-572)原样透传给 knex,不注入任何默认超时。于是三个边界全部来自库的默认值。用一个"接受连接后永远不说话"的监听器实测四种配置:
两个结论:
createTimeoutMillis默认值兜住了。(我一开始以为是无界,那是因为测量上限只有 12 秒;如实修正。)timeout expired—— 连接尝试超时,正是实情。影响
pool: { max: 5 }+ 每次 30 秒:6 个并发请求打到一个黑洞端点,池就被占满,后续请求开始排队,这时候"pool is probably full"才变成真的 —— 但那是继发症状,不是病因。诊断被引向错误方向。checkDriversHealth()必须自带 2 秒上限的原因。我当时把它写成"防御性设计",其实它是在绕过这个 bug —— 探针有界了,业务查询路径仍然没有。建议处置
原则:平台在请求路径上发起的每一次网络操作,都该有一个我们自己选定的上限,而不是从库的默认值里继承一个。
storage-driver.ts是我们自己从 URL 构造配置的地方,改用对象形式并设connectionTimeoutMillis(建议 10s,与 driver-mongodb 既有的connectTimeoutMS ?? 10_000对齐)。实测该形式在?sslmode=require下同样正确限时。{ connectionString, … }后connectionSettings不再被 knex 预解析成{host, port, database},serve.ts的describeRegisteredDriver读conn.host会退化成(unknown)—— 启动横幅要同步适配。SqlDriver自身补一个 dialect 无关的pool.createTimeoutMillis默认值作为兜底,覆盖那些自行组装 driver 的宿主(cloud/EE)。acquireConnectionTimeout(knex 默认 60s)在请求路径上是否也偏长。关联
/ready探针;那里的 2s 上限就是在绕开本 issue)