Rust in Everything
基于 sqlx 0.9.0

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_connections10new_arc 用它定信号量许可数和空闲队列容量;try_increment_size 用它封顶 size获取连接
min_connections0connect_with 建池时先补足;try_min_connections 在连接被关掉后补归还与维护
acquire_timeout30 秒PoolInner::acquire 整个循环外面的 rt::timeout;connect_with 的截止时间获取连接
test_before_acquiretruecheck_idle_conn:借出空闲连接前 ping获取连接
idle_timeout10 分钟维护任务;也参与决定它的周期归还与维护
max_lifetime30 分钟归还时的 return_to_pool 和维护任务,借出时不查归还与维护
acquire_slow_threshold / acquire_slow_level / acquire_time_level2 秒 / Warn / Offacquire 成功后决定打不打日志获取连接
fairtrueAsyncSemaphore::new本页下文
parent_poolNonenew_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_connectPoolInner::connect 每建好一条新连接,包括建池和补 min_connections 时—error! 日志,close_hard,退避后重连age、idle_for 都是 0
before_acquirecheck_idle_conn,只对空闲队列里取出的连接,在 ping 之后正常 close,新建一条warn!,close_hard,新建一条age、idle_for
after_releasereturn_to_pool,在 max_lifetime 检查之后、ping 之前正常 closewarn!,close_hardage,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 一直重试到超时。

📝NOTE
没有蠢问题

问: 回调为什么要写 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);
}
❗IMPORTANT
动脑筋:整个程序一共打印几行 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 for before_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 时还回去。

💬 关联讨论

发起讨论
加载中...