PoolOptions 与默认值
主线位置:连接池主线的起点。
PgPoolOptions::new().max_connections(5).connect(url)造出一个PoolInner:信号量的许可数、空闲队列的容量、acquire循环的超时、借出和归还时的检查、后台任务的周期,全由这里的字段决定。本页把每个字段对到用它的那段代码,acquire和归还本身在后两页讲。
flowchart TB new["new 的默认值"] --> setters["链式 setter"] setters --> cw["connect_with"] setters --> lazy["connect_lazy"] cw --> arc["new_arc"] lazy --> arc arc --> sem["信号量 max 个许可"] arc --> queue["空闲队列"] arc --> task["spawn 维护任务"] cw --> first["至少连一条再返回"]
PgPoolOptions 只是 PoolOptions<Postgres> 的别名(sqlx-postgres/src/lib.rs),PgPool 是 Pool<Postgres>。池的代码不知道自己连的是哪种数据库,它只通过 Connection trait 调三个方法:ConnectOptions::connect、ping、close(以及出错时的 close_hard)。
默认值都在 new 里
PoolOptions::new 的文档说「See the source of this method for the current default values」,源码是:
pub fn new() -> Self {
Self {
// User-specifiable routines
after_connect: None,
before_acquire: None,
after_release: None,
test_before_acquire: true,
// A production application will want to set a higher limit than this.
max_connections: 10,
min_connections: 0,
// Logging all acquires is opt-in
acquire_time_level: LevelFilter::Off,
// Default to warning, because an acquire timeout will be an error
acquire_slow_level: LevelFilter::Warn,
// Fast enough to catch problems (e.g. a full pool); slow enough
// to not flag typical time to add a new connection to a pool.
acquire_slow_threshold: Duration::from_secs(2),
acquire_timeout: Duration::from_secs(30),
idle_timeout: Some(Duration::from_secs(10 * 60)),
max_lifetime: Some(Duration::from_secs(30 * 60)),
fair: true,
parent_pool: None,
}
}
Default 也是调它。用 getter 和 Debug 打出来(src/bin/defaults.rs):
let o = PgPoolOptions::new();
println!("{o:?}");
println!(
"max={} min={} acquire_timeout={:?} idle_timeout={:?} max_lifetime={:?} test_before_acquire={} slow_threshold={:?}",
o.get_max_connections(),
o.get_min_connections(),
o.get_acquire_timeout(),
o.get_idle_timeout(),
o.get_max_lifetime(),
o.get_test_before_acquire(),
o.get_acquire_slow_threshold(),
);
PoolOptions { max_connections: 10, min_connections: 0, connect_timeout: 30s, max_lifetime: Some(1800s), idle_timeout: Some(600s), test_before_acquire: true }
max=10 min=0 acquire_timeout=30s idle_timeout=Some(600s) max_lifetime=Some(1800s) test_before_acquire=true slow_threshold=2s
Debug 里的 connect_timeout 其实是 acquire_timeout 字段:Debug 实现写的是 .field("connect_timeout", &self.acquire_timeout),名字是旧的。回调、fair 和日志级别不在 Debug 输出里。
每个字段在哪里被读:
| 字段 | 默认 | 谁读它 | 讲它的页 |
|---|---|---|---|
max_connections | 10 | new_arc 用它定信号量许可数和空闲队列容量;try_increment_size 用它封顶 size | 获取连接 |
min_connections | 0 | connect_with 建池时先补足;try_min_connections 在连接被关掉后补 | 归还与维护 |
acquire_timeout | 30 秒 | PoolInner::acquire 整个循环外面的 rt::timeout;connect_with 的截止时间 | 获取连接 |
test_before_acquire | true | check_idle_conn:借出空闲连接前 ping | 获取连接 |
idle_timeout | 10 分钟 | 维护任务;也参与决定它的周期 | 归还与维护 |
max_lifetime | 30 分钟 | 归还时的 return_to_pool 和维护任务,借出时不查 | 归还与维护 |
acquire_slow_threshold / acquire_slow_level / acquire_time_level | 2 秒 / Warn / Off | acquire 成功后决定打不打日志 | 获取连接 |
fair | true | AsyncSemaphore::new | 本页下文 |
parent_pool | None | new_arc、acquire_permit | 本页下文 |
min_connections 的文档说它「is clamped internally to not exceed max_connections」,并且故意不做 min <= max 的断言。实际的「夹住」来自 try_min_connections 每补一条都要先拿到一个许可、再过 try_increment_size,拿不到就停。
max_connections 是这一个池的上限。服务端的 max_connections 是所有客户端加起来的上限,多实例部署时每个实例各有一个池,总数要自己算。
connect 与 connect_lazy
connect_with(connect(url) 先 parse 再调它):
pub async fn connect_with(
self,
options: <DB::Connection as Connection>::Options,
) -> Result<Pool<DB>, Error> {
// Don't take longer than `acquire_timeout` starting from when this is called.
let deadline = Instant::now() + self.acquire_timeout;
let inner = PoolInner::new_arc(self, options);
if inner.options.min_connections > 0 {
// If the idle reaper is spawned then this will race with the call from that task
// and may not report any connection errors.
inner.try_min_connections(deadline).await?;
}
// If `min_connections` is nonzero then we'll likely just pull a connection
// from the idle queue here, but it should at least get tested first.
let conn = inner.acquire().await?;
inner.release(conn);
Ok(Pool(inner))
}
三点:
- 建池最多花
acquire_timeout。连不上时报的是PoolTimedOut,而不是底层的连接错误,原因在获取连接的重试一节。 - 先补足
min_connections,再走一次完整的acquire(会 ping)确认连接能用,所以建好的池里至少有max(1, min_connections)条连接。 - 最后直接调
inner.release(conn)放回空闲队列,不经过PoolConnection的Drop,也就不跑after_release、不做归还时的 ping。
connect_lazy_with 只有一行 Pool(PoolInner::new_arc(self, options)):不连库、不报错,URL 写错也要到第一次 acquire 才知道。它的文档说设置了 min_connections 时「a background task will be spawned to optimistically establish that many connections」,源码里的注释是「min_connections is guaranteed by the idle reaper now」。实测在默认的 idle_timeout / max_lifetime 下这个后台任务不会补连接,原因和实验在归还、Drop 与维护任务。
PoolInner::new_arc 是两条路共同的终点:建一个容量为 max_connections 的 ArrayQueue(空闲队列),一个许可数为 max_connections 的信号量(子池是 0,见下文),size 和 num_idle 两个原子计数从 0 开始,然后立刻 spawn_maintenance_tasks。默认参数下这一步会 rt::spawn,不在 tokio 运行时里调用就 panic(this functionality requires a Tokio context),connect_lazy 也一样。
三个回调
pub fn after_connect<F>(mut self, callback: F) -> Self
where
// We're passing the `PoolConnectionMetadata` here mostly for future-proofing.
// `age` and `idle_for` are obviously not useful for fresh connections.
for<'c> F: Fn(&'c mut DB::Connection, PoolConnectionMetadata) -> BoxFuture<'c, Result<(), Error>>
+ 'static
+ Send
+ Sync,
before_acquire 和 after_release 形状一样,只是返回 Result<bool, Error>。三者都存成 Arc<dyn Fn ...>,PoolOptions 能 Clone。
| 回调 | 什么时候跑 | 返回 Ok(false) | 返回 Err | 元数据 |
|---|---|---|---|---|
after_connect | PoolInner::connect 每建好一条新连接,包括建池和补 min_connections 时 | — | error! 日志,close_hard,退避后重连 | age、idle_for 都是 0 |
before_acquire | check_idle_conn,只对空闲队列里取出的连接,在 ping 之后 | 正常 close,新建一条 | warn!,close_hard,新建一条 | age、idle_for |
after_release | return_to_pool,在 max_lifetime 检查之后、ping 之前 | 正常 close | warn!,close_hard | age,idle_for 是 0 |
before_acquire 拒绝一条连接之后,check_idle_conn 把这条连接的 DecrementSizeGuard 交回 acquire,后者拿着它直接新建连接,不再从队列里试下一条空闲连接,也不用重新排队拿许可。回调的文档说的是「tries again with another idle connection. If it runs out of idle connections, it opens a new connection instead」,和代码不一致,以代码为准,见获取连接。after_connect 出错时走的是建连接的退避循环,所以一个总是出错的 after_connect 会让 acquire 一直重试到超时。
问: 回调为什么要写 Box::pin(async move { ... }),不能直接传一个 async 闭包吗?
答: 因为回调借用了连接:它收 &'c mut PgConnection,返回的 future 也要活在同一个 'c 里。PoolOptions 的类型文档专门解释了这一点:想写成 F: for<'a> Fn(&'a mut Foo) -> Fut 时,「There's no way to tell the compiler that those two 'as should be the same lifetime」,而 impl Fn 的返回类型里又不能写 impl Trait。让闭包返回 BoxFuture<'c, _> 就把生命周期写进了返回类型。池内部本来也要把 future 装箱才能存成一个具体类型,文档的结论是这对 API 两边都是「the path of least resistance」。
用一个 max_connections(1) 的池看三个回调的顺序(src/bin/callbacks.rs)。before_acquire 在第 2 次借出时返回 false,每次借出都查一次 pg_backend_pid():
let acquires = Arc::new(AtomicU32::new(0));
let n = acquires.clone();
let pool = PgPoolOptions::new()
.max_connections(1)
.after_connect(|conn, meta| Box::pin(async move {
let pid: i32 = sqlx::query_scalar("select pg_backend_pid()").fetch_one(&mut *conn).await?;
println!(" after_connect pid={pid} {meta:?}");
Ok(())
}))
.before_acquire(move |_conn, meta| {
let n = n.clone();
Box::pin(async move {
let k = n.fetch_add(1, Ordering::SeqCst) + 1;
// reject the connection on the 2nd checkout
let keep = k != 2;
println!(" before_acquire #{k} {meta:?} -> {keep}");
Ok(keep)
})
})
.after_release(|_conn, meta| Box::pin(async move {
println!(" after_release {meta:?}");
Ok(true)
}))
.connect_with(lab::opts("pool-cb"))
.await?;
println!("built");
for round in 1..=3 {
tokio::time::sleep(Duration::from_millis(200)).await;
println!("round {round}");
let mut conn = pool.acquire().await?;
let pid: i32 = sqlx::query_scalar("select pg_backend_pid()").fetch_one(&mut *conn).await?;
println!(" using pid={pid}");
drop(conn);
}
after_connect、几行 after_release?第 3 轮用的是第几条连接?平台:M2 Max、macOS、rustc 1.99.0、Postgres 16.14。答案在页末。test_before_acquire
默认 true:每次从空闲队列取出一条连接,先 ping 一个来回再交出去。关掉它省掉借出时的一个来回,归还时那次 ping 关不掉。抓包对比和取舍在获取连接。文档给 before_acquire 举的例子正好是「关掉 test_before_acquire,只对空闲超过一分钟的连接 ping」,meta.idle_for 就是为这种写法准备的。
fair:在 tokio 下只能是 true
字段叫 fair,setter 却叫 __fair,带 #[doc(hidden)],文档写「Currently only exposed for benchmarking; fair = true seems to be the superior option in most cases」。它的值只交给 AsyncSemaphore::new:
#[cfg(feature = "_rt-tokio")]
inner: {
debug_assert!(fair, "Tokio only has fair permits");
tokio::sync::Semaphore::new(permits)
},
tokio 的信号量没有「不公平」模式,fair 只在 async-std / smol 下用的 futures_intrusive::sync::Semaphore 上有意义。在 tokio 下传 __fair(false)(src/bin/unfair.rs):
let pool = PgPoolOptions::new()
.__fair(false)
.connect_with(lab::opts("pool-unfair"))
.await?;
println!("built: size={}", pool.size());
debug 构建:
thread 'main' (3157566) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sqlx-core-0.9.0/src/pool/inner.rs:57:24:
Tokio only has fair permits
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
--release 构建(debug_assert! 被去掉):
built: size=1
panic 的位置报在 inner.rs:57(new_arc 里调 AsyncSemaphore::new 的那一行),因为 new 标了 #[track_caller]。release 下这个选项被静默忽略,池仍然是公平的。先到先得在实验里是什么样,见获取连接的公平一节。
parent:子池
parent 也是 #[doc(hidden)],文档写「This is currently an internal-only API」。它的用户是 #[sqlx::test]:Postgres 的测试支持建一个 max_connections(20) 的主池,每个测试一个子池,子池的 PoolOptions::new() 后面接 .max_connections(5)、.idle_timeout(Some(Duration::from_secs(1)))、.parent(master_pool.clone()),见 #[sqlx::test]。
new_arc 对子池做两个断言(子池的 max_connections 不超过父池、fair 相同),并把子池自己的信号量建成 0 个许可:
let semaphore_capacity = if let Some(parent) = &options.parent_pool {
assert!(options.max_connections <= parent.options().max_connections);
assert_eq!(options.fair, parent.options().fair);
// The child pool must steal permits from the parent
0
} else {
capacity
};
子池要开连接时从父池的信号量「偷」许可;连接归还时许可放进子池自己的信号量,于是子池的许可越攒越多,直到子池的 PoolInner 被 drop,Drop 才把攒下的许可还给父池。父子两个池共用父池的 max_connections 这个总上限。偷许可的细节在获取连接。
运行中改连接参数
PoolOptions 建池后就固定了(pool.options() 只读),但连接参数可以换:Pool::set_connect_options 替换 RwLock<Arc<PgConnectOptions>> 里的 Arc。PoolInner::connect 每次建连接前克隆一次这个 Arc 再释放读锁,所以新参数只影响之后新建的连接,已有的连接不动。典型用途是轮换密码或换主机。
动脑筋答案
after_connect pid=478 PoolConnectionMetadata { age: 0ns, idle_for: 0ns }
built
round 1
before_acquire #1 PoolConnectionMetadata { age: 202.602917ms, idle_for: 202.599917ms } -> true
using pid=478
after_release PoolConnectionMetadata { age: 202.970834ms, idle_for: 0ns }
round 2
before_acquire #2 PoolConnectionMetadata { age: 406.310667ms, idle_for: 203.155167ms } -> false
after_connect pid=479 PoolConnectionMetadata { age: 0ns, idle_for: 0ns }
using pid=479
after_release PoolConnectionMetadata { age: 209.166µs, idle_for: 0ns }
round 3
before_acquire #3 PoolConnectionMetadata { age: 203.243208ms, idle_for: 202.875917ms } -> true
using pid=479
after_release PoolConnectionMetadata { age: 203.720208ms, idle_for: 0ns }
两行 after_connect、三行 after_release,第 3 轮用的是第二条连接(pid 479)。
built之前只有after_connect:建池那条连接是新建的,不过before_acquire;connect_with用inner.release直接放回队列,不过after_release。- 第 1 轮
age和idle_for几乎相等:连接建好后马上进了空闲队列,idle_since在release里的into_idle记下。 - 第 2 轮:
before_acquire返回false,旧连接被正常关闭,acquire拿着同一个许可新建一条,于是紧接着一行after_connect;这条新连接不再过before_acquire。它归还时age只有 209 微秒。 - 第 3 轮:队列里只有 479,
before_acquire第 3 次返回true。after_release的idle_for总是 0,类型文档写的是「Only relevant forbefore_acquire」。
小结
- 默认值全在
PoolOptions::new:10 条、最少 0 条、借出超时 30 秒、空闲 10 分钟、寿命 30 分钟、借出前 ping、慢于 2 秒打WARN。Debug把acquire_timeout显示成connect_timeout。 connect在acquire_timeout内至少连上max(1, min_connections)条,失败报PoolTimedOut;connect_lazy什么都不连。两者都在调用时 spawn 维护任务。after_connect管新连接,before_acquire只管从空闲队列取出的连接,after_release管归还;拒绝或出错就关掉连接,借出那一侧会换一条。fair的 setter 是隐藏的__fair;tokio 的信号量总是公平的,传false在 debug 下 panic,release 下被忽略。parent是给#[sqlx::test]用的子池:子池从父池偷许可,drop 时还回去。