connect_timeout 不起作用:连不上库时等的是 acquire_timeout
主线位置:查询主线的起点
Database::connect,以及每条语句的acquire。ConnectOptions的每一项怎么落到 sqlx 的PoolOptions上,见 DatabaseConnection 与 ConnectOptions 的对照表和「connect_timeout就是acquire_timeout」一节;本页只讲踩上去是什么样、怎么配。
flowchart TB ct["connect_timeout(1s)"] --> f["PoolOptions.acquire_timeout"] at["acquire_timeout(5s)"] -->|后写| f f --> c1["Database::connect 最多等"] f --> c2["池满时语句最多排队"] f --> c3["新建连接最多连"]
本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行:sea-orm 2.0.4(sqlx 0.9.0)和 sea-orm 1.1.20(sqlx 0.8.6)输出逐字相同。
现象
配置里写着「连接超时 1 秒、借连接超时 5 秒」,想要的是:数据库宕机时启动 1 秒就失败,流量高峰时请求最多排 5 秒。实际上数据库宕机时启动等了 5 秒;而只写了 connect_timeout 的服务,池满时请求按 connect_timeout 失败。
最小复现
src/bin/timeouts.rs,max_connections(1)。第一段连本机没人监听的 1 号端口,第二段先用 sqlx 的 pool.acquire() 占住唯一一条连接,再执行一条语句:
for (c, a) in [(Some(1000), None), (Some(1000), Some(5000)), (None, Some(1000))] {
let t = Instant::now();
let err = Database::connect(opts(DEAD, c, a)).await.err().expect("must fail");
println!("{} 用时 {:.2}s {err}", label(c, a), t.elapsed().as_secs_f64());
}
--- 1. 连不上的库:Database::connect 等多久 ---
connect_timeout=1000ms acquire_timeout=- 用时 1.01s Connection Error: pool timed out while waiting for an open connection
connect_timeout=1000ms acquire_timeout=5000ms 用时 5.00s Connection Error: pool timed out while waiting for an open connection
connect_timeout=- acquire_timeout=1000ms 用时 1.00s Connection Error: pool timed out while waiting for an open connection
--- 2. 池满:一条语句排队等多久 ---
connect_timeout=1000ms acquire_timeout=- 用时 1.00s Failed to acquire connection from pool: Connection pool timed out
connect_timeout=1000ms acquire_timeout=5000ms 用时 5.00s Failed to acquire connection from pool: Connection pool timed out
- 两个都设时,连不上库等的是
acquire_timeout的 5 秒,connect_timeout的 1 秒完全没起作用。 - 只设
connect_timeout时,它就是排队超时:池满的那条语句 1 秒就失败了。 - 两个阶段报的错误不一样:建池时是
DbErr::Conn,显示 sqlx 原文;执行语句时是DbErr::ConnectionAcquire(错误)。真正的原因(连接被拒)两句里都没有。
出处
ConnectOptions::sqlx_pool_options 里,两个选项写的是 sqlx 的同一个 setter,acquire_timeout 写在后面:
if let Some(connect_timeout) = self.connect_timeout {
opt = opt.acquire_timeout(connect_timeout);
}
if let Some(idle_timeout) = self.idle_timeout {
opt = opt.idle_timeout(idle_timeout);
}
if let Some(acquire_timeout) = self.acquire_timeout {
opt = opt.acquire_timeout(acquire_timeout);
}
sqlx 的 Postgres 驱动没有单独的 TCP 连接超时,建连接的时间也由池的这个截止时间管(sqlx 卷获取连接)。文档注释也说不清它到底是哪一个:字段 connect_timeout 上写的是「The connection timeout for a packet connection」,setter ConnectOptions::connect_timeout 上写的却是「Set the timeout duration when acquiring a connection」。1.1.20 的 sqlx_pool_options 是同样三段、同样顺序。这个名字怎么来的(sqlx 0.6 把 connect_timeout 改名 acquire_timeout,sea-orm 只改了调用)见 database-connection 那一页的冷知识。
搜过 SeaQL/sea-orm 的 issue(connect_timeout、acquire timeout):#896 是当年促成加 acquire_timeout 的池耗尽报告,#1674(「ConnectOptions parameter setting has no effect」)说的是 MySQL 上的请求延迟;没有找到把「两个 setter 写同一个字段」当问题提出来的 issue。
修法
只设 acquire_timeout,把它理解成「任何时候等一条连接最多等多久」:启动建池、池满时排队、池补新连接,都是这一个值。启动阶段想更快失败,就在 Database::connect 外面另套一层 tokio::time::timeout(第三段,acquire_timeout(5s),外层 1 秒):
let r = tokio::time::timeout(Duration::from_secs(1), Database::connect(opts(DEAD, None, Some(5000)))).await;
--- 3. 只设 acquire_timeout(5s),启动时另套 1s 的 tokio timeout ---
timeout(1s, Database::connect) = Err(Elapsed(())),用时 1.00s
外层 1 秒到点就返回;连上之后,请求阶段的排队上限仍是 5 秒。
本站
本站 build_connect_options 两个都设,注释写的是「默认建连超时:5s」和「默认 acquire 超时:5s」:
.connect_timeout(Duration::from_secs(read_env_secs(
"DB_CONNECT_TIMEOUT_SECS",
DEFAULT_CONNECT_TIMEOUT_SECS,
)))
.acquire_timeout(Duration::from_secs(read_env_secs(
"DB_ACQUIRE_TIMEOUT_SECS",
DEFAULT_ACQUIRE_TIMEOUT_SECS,
)))
所以环境变量 DB_CONNECT_TIMEOUT_SECS 不管设成多少都没有效果,第一段的第二行就是把它设成 1、DB_ACQUIRE_TIMEOUT_SECS 保持 5 的情形。两个默认值都是 5,目前看不出差别;哪天有人只改了 DB_CONNECT_TIMEOUT_SECS,就会发现它不生效。
怎么确认没中招
- 搜
connect_timeout(:和acquire_timeout(同时出现的,前者是死代码;单独出现的,它是排队超时。 - 打印
db.get_postgres_connection_pool().options():Debug里那个connect_timeout是 sqlx 的旧名字,显示的是最终生效的acquire_timeout(database-connection 页第一段实验)。 - 两个都不设时是 sqlx 的默认 30 秒:数据库宕机时启动卡 30 秒才报错。
小结
connect_timeout和acquire_timeout都写 sqlx 的PoolOptions::acquire_timeout,后者后写、总是赢;2.0.4 和 1.1.20 相同。- 这一个值同时管启动建池、池满排队、新建连接。只设一个时两者等价。
- 只设
acquire_timeout;启动要更快失败,在Database::connect外面套tokio::time::timeout。 - 本站的
DB_CONNECT_TIMEOUT_SECS没有效果。