SQLx 源码深度解析

第13章 Pool 外部 API:acquire / try_acquire / timeout

作者 杨艺韬 · 10,301 字 · 发布于 · 更新于

Pool<DB> 只是 Arc<PoolInner<DB>> 的包装,clone() 因此近乎零开销。真正的难点在 acquire():超时或取消时,若它已经拿到了连接,就直接 drop 掉,而不是归还池子。

本章要点

  • Pool<DB> 结构极简——只是 Arc<PoolInner<DB>> 的包装(sqlx-core/src/pool/mod.rs:260)。Clone 是零开销的 Arc 引用计数——这让 pool.clone() 传进 tokio::spawn 或 Axum State 几乎没代价。
  • 四个构造函数(§13.3):connect(url) / connect_with(options) / connect_lazy(url) / connect_lazy_with(options)——eager(立即建 max(1, min_connections) 条连接)vs lazy(不预先建连)× URL vs Options 两维组合。
  • 核心方法 acquire()(sqlx-core/src/pool/mod.rs:357-360)—— 返回 impl Future<Output = Result<PoolConnection<DB>>>。受 acquire_timeout 限制(默认 30s)。取消或超时会 drop 获得的连接——而不是归还池子(避免"测试连接是否安全归还"的复杂性)。
  • try_acquire() 非阻塞 —— 有空闲连接立即返回,否则 None(它从不新建连接)。
  • begin() / begin_with() 捷径——acquire + Connection::begin 的封装。返回 Transaction<'static, DB>,注意 'static——意味着事务可以跨 await 自由传递。
  • close() / close_event()——优雅关闭。close_event() 返回一个 CloseEvent,可以用 do_until 在池关闭时取消长查询。
  • PoolOptions 13 个配置(§13.10)—— max_connections 默认 10(生产要改)、min_connections 默认 0、acquire_timeout 30s、idle_timeout 10 分钟、max_lifetime 30 分钟、test_before_acquire 默认 true、fair 默认 true(setter 是隐藏的 __fair)、以及 3 个回调(after_connect / before_acquire / after_release)+ 3 个日志级别控制。
  • set_connect_options 热更新—— 改 DATABASE_URL 密码旋转、数据库迁移时无缝切换;已建立的连接保留、新建的按新 options。

13.1 问题引入:Pool 必须做的三件事

第 12 章讲清楚了 Connection 是什么。但业务代码里几乎不会直接用 PgConnection::connect("...")——原因有三:

  • 建立连接太贵——每次都要走一遍 TCP、可选 TLS、认证的多轮往返(第 12 章 §12.15),每个请求都握手一次会把 handler 延迟拉到不可接受。
  • 连接数有限——Postgres 默认 max_connections = 100——上百个 worker 线程独立握手必然撞上限。
  • 并发协调难——Rust 的 &mut Connection 独占性让"多个 task 共享同一连接"实现起来笨拙。

解决方案:连接池。Pool 做三件事:

  1. 复用连接——一条连接打开后多次请求共用,摊销握手成本。
  2. 限制并发——设置 max_connections,超出时让新请求排队而不是撞上限。
  3. 自动回收——空闲太久的连接主动关闭、被用太久的连接也定期淘汰。

sqlx 的 Pool 是这类工具的规范实现。本章和下一章(第 14 章 Pool 内部)把它从外到内拆开——本章专注用户怎么用(外部 API),第 14 章讲内部怎么做(idle_conns / AsyncSemaphore / 驱逐策略)。

13.2 Pool<DB>:一个 Arc 包裹

sqlx-core/src/pool/mod.rs:260:

pub struct Pool<DB: Database>(pub(crate) Arc<PoolInner<DB>>);

只有一个字段——Arc<PoolInner<DB>>。PoolInner 持有所有内部状态(空闲队列、信号量、配置、后台任务 handle 等),但对外只暴露一个指针大小的 Pool——极其轻量。

这条设计带来几个直接收益:

1. Clone 零开销:pool.clone() 只是 Arc 的 refcount 原子加一——不拷贝任何内部数据。这让你自由传 pool:

// Axum State
let state = AppState { pool: pool.clone() };
// tokio spawn
let pool2 = pool.clone();
tokio::spawn(async move { pool2.acquire().await... });

两次 clone 都是纳秒级操作——所有任务共享同一个 PoolInner。

2. &pool 做 Executor:第 4 章 §4.4.1 讲过 impl Executor for &Pool——不需要 &mut。这条 API 形态就是 Pool 作为 Arc 包裹的直接好处——多个地方借用 &pool 合法,每个借用内部通过 Arc 访问同一 PoolInner。

3. 跨线程自然:Arc 天然 Send + Sync(只要内部 T 是 Send + Sync)——Pool 自动跨线程共享。用户不用写 Arc<Mutex<Pool>> 这种多一层包装——sqlx 已经内置了这一层。

这个"Arc 包裹"模式是 Rust 里"共享所有权的服务对象"的标准做法——tokio 的 tokio::sync::mpsc::Sender 内部也是 Arc 包裹。想让用户自由 clone + 多线程共享的资源都这么设计。

13.3 四个构造函数

sqlx-core/src/pool/mod.rs:284-332 的构造函数四件套(方法体略):

pub async fn connect(url: &str) -> Result<Self, Error> { ... }
pub async fn connect_with(options: <DB::Connection as Connection>::Options) -> Result<Self, Error> { ... }
pub fn connect_lazy(url: &str) -> Result<Self, Error> { ... }
pub fn connect_lazy_with(options: <DB::Connection as Connection>::Options) -> Self { ... }

<DB::Connection as Connection>::Options 就是各驱动的 PgConnectOptions / MySqlConnectOptions / SqliteConnectOptions。

两个维度的四种组合:

URL 字符串 预构造 Options
eager connect(url) connect_with(options)
lazy connect_lazy(url) connect_lazy_with(options)

eager vs lazy 的区别:

  • eager(connect / connect_with):立即建立 max(1, min_connections) 条连接(sqlx-core/src/pool/options.rs:537-558:先按 min_connections 补齐,再 acquire 一次并归还),整个过程受 acquire_timeout 限时——返回 Pool 前先验证 URL 可用。失败返回 Error。
  • lazy(connect_lazy / connect_lazy_with):不当场建连接——只构造 Pool 元数据、首次 acquire() 时才建。min_connections 在这里基本不会让池子预热:文档注释说会有后台任务乐观地补建(sqlx-core/src/pool/options.rs:562-563),但默认两个超时都设了值,这时维护循环只在关掉过期连接之后才调用补齐(sqlx-core/src/pool/inner.rs:544-551),只有两者都为 None 时才会 spawn 一次性的补齐任务(sqlx-core/src/pool/inner.rs:513-520)。实测 connect_lazy_with + min_connections(3) 用默认超时,500ms 后 size() 仍是 0。connect_lazy(url) 返回的 Result 只报告 URL 解析错误。

选 eager 还是 lazy?

  • 生产服务启动:eager——让启动时就验证 DB 可达。如果 DB 连不上,让服务 fail fast 而不是等第一次请求。
  • 测试:eager——让测试的 "setup pool" 阶段就能检测 DB 问题,不混到实际测试代码里。
  • 延迟连接:lazy——某些场景 Pool 构造时 DB 还没 ready(例如 DB migration 容器还在启动)、或者只在某些 feature path 下才需要 DB。
  • 健康探针解耦:lazy——如果你有独立的健康检查路径、不想让 Pool 构造和主程序启动顺序耦合。

URL vs Options:URL 简单(一个字符串);Options 精细(SSL 模式、application_name、statement_cache_capacity、Unix socket 路径等,PgConnectOptions 共 27 个 pub fn,见第 12 章 §12.9.1)。生产通常用 Options。

13.3.1 PoolOptions::new().connect(url) 才是"真正的入口"

Pool::connect(url) 的实现(sqlx-core/src/pool/mod.rs:284-288):

pub async fn connect(url: &str) -> Result<Self, Error> {
    PoolOptions::<DB>::new().connect(url).await
}

它只是 PoolOptions::new().connect(url) 的 shortcut——用默认配置(max_connections = 10 等)。如果你想改任何配置,就从 PoolOptions::new() 开始链式配置然后 .connect(url):

let pool = PgPoolOptions::new()
    .max_connections(20)
    .acquire_timeout(Duration::from_secs(5))
    .idle_timeout(Duration::from_secs(600))
    .connect(&database_url).await?;

生产代码几乎永远用这条路径——Pool::connect(url) 的默认 max_connections = 10 对任何稍大流量都不够。

13.4 acquire():借一条连接

sqlx-core/src/pool/mod.rs:357-360:

pub fn acquire(&self) -> impl Future<Output = Result<PoolConnection<DB>, Error>> + 'static {
    let shared = self.0.clone();
    async move { shared.acquire().await.map(|conn| conn.reattach()) }
}

三条关键观察:

1. 返回 impl Future + 'static——不借用 self。这让你 let fut = pool.acquire(); 然后 spawn 到别处——Future 独立生存,不 tie 到 pool 的生命周期。实现靠 shared = self.0.clone() 先 clone Arc——把一份 Arc 引用"移"进 Future。

2. PoolConnection<DB> 不是普通 Connection——它是 Pool 管理的智能指针,Deref / DerefMut 到 DB::Connection(sqlx-core/src/pool/connection.rs:52-64)。Drop 实现会归还连接给池子(第 14 章 §14.9)。拿它跑查询要写 &mut *conn:&mut PoolConnection 本身不实现 Executor(第 4 章 §4.6),直接写 &mut conn 会报 E0277。它还有几个公开方法(sqlx-core/src/pool/connection.rs:86-124):close() 关掉这条连接、让池子能补一条新的;close_on_drop() 标记 drop 时关闭而不归还;detach() 把裸连接拿走并让出名额(单池应用因此可能超过 max_connections);leak() 也拿走裸连接,但名额永久扣掉——每用一次池子容量少 1。细节见第 14 章 §14.9.1。

3. reattach() 做什么?——shared.acquire() 交回的是 Floating<DB, Live<DB>>(带着一个 DecrementSizeGuard 的"悬空"连接),reattach() 取消这个 guard、把连接装进 PoolConnection(sqlx-core/src/pool/connection.rs:255-266)——从此由 PoolConnection 的 Drop 负责归还。具体第 14 章讲。

13.4.1 acquire 的超时机制

acquire 受 acquire_timeout 约束(默认 30 秒),超时返回 Error::PoolTimedOut(sqlx-core/src/pool/inner.rs:250-293;Pool::acquire 文档注释里写的 Error::PoolClosed 与实现不符,以代码为准)。计时覆盖排队等 permit、对空闲连接的 ping 与 before_acquire、新建连接与 after_connect 全过程。

超时的两种原因:

  • Pool 已满:max_connections 条连接都被占用,排队等满了 acquire_timeout。
  • 建新连接迟迟不成功:建连本身卡住直到截止;或者 DB 拒绝连接(ConnectionRefused)、返回驱动认定的"建连期瞬时错误"(Postgres 是 53300 too_many_connections 与 57P03 cannot_connect_now,sqlx-postgres/src/error.rs:189-201),Pool 按指数退避重试直到截止。注意其他建连错误(比如认证失败)不会重试,会立即作为该错误本身返回,而不是 PoolTimedOut(sqlx-core/src/pool/inner.rs:373-384)。

生产日志里看到 PoolTimedOut 永远值得关注——意味着"负载超出 Pool 容量"或"DB 可用性异常"。

13.4.2 acquire 的取消行为

源码 sqlx-core/src/pool/mod.rs:340-345 的文档注释("Note: Cancellation/Timeout May Drop Connections")说明了这条行为:

If acquire is cancelled or times out after it acquires a connection from the idle queue or opens a new one, it will drop that connection because we don't want to assume it is safe to return to the pool, and testing it to see if it's safe to release could introduce subtle bugs if not implemented correctly.

acquire 取消时会 drop 连接、不归还——取消可能恰好发生在 ping、before_acquire 或 after_connect 的半途,连接上可能留着没读完的响应,sqlx 不去假设它还能安全归还。保守做法是丢弃、让 Pool 建新的;文档的意思是:去检测它能否安全归还,实现稍有不慎就会引入隐蔽 bug,干脆不优雅处理取消。

同一段文档还给了规避办法:如果你的场景对丢连接特别敏感(它举的例子是"池大小为 1 的内存 SQLite"),把 test_before_acquire 设成 false、并且不设 before_acquire / after_connect——这样"拿到连接"和"返回连接"之间就没有任何 .await 点,也就没有取消的窗口。

对用户的影响:

tokio::select! {
    conn = pool.acquire() => { /* 用 conn */ }
    _ = some_timeout => { /* acquire 被取消——若它已拿到连接、正卡在 ping 或回调里,这条连接被 drop */ }
}

超时分支取消 acquire 时,如果 acquire 已经拿到连接,这条连接就被丢——实际上 Pool 连接数瞬时减 1。高频取消的代码会让 Pool 持续"磨损"——不断销毁 + 重建连接。生产中避免高频 select + acquire 组合。

13.5 try_acquire:非阻塞

sqlx-core/src/pool/mod.rs:366-368:

pub fn try_acquire(&self) -> Option<PoolConnection<DB>> {
    self.0.try_acquire().map(|conn| conn.into_live().reattach())
}

同步方法——不是 async。立即返回:

  • Some(conn):有空闲连接、直接给你。
  • None:Pool 已关闭、拿不到 permit(连接全被借出,或按文档所说还有等待者没被唤醒),或者空闲队列是空的——你自己决定怎么办(回退 / 稍后重试 / 降级)。

注意 try_acquire 从不新建连接(sqlx-core/src/pool/inner.rs:178-186 只做 pop_idle),也不做 test_before_acquire 的 ping 和 before_acquire 回调。所以 None 不等于"池满了":一个刚 connect_lazy 出来、size() == 0 的池子,try_acquire() 也是 None(SQLite 内存库实测)。

典型用途:非关键路径。比如健康检查想知道"现在有没有现成的空闲连接":

if let Some(conn) = pool.try_acquire() {
    // ... 健康检查,用完 drop
} else {
    tracing::warn!("no idle connection available");
}

或者fallback 路径——acquire 失败走别的方式(缓存、降级响应):

let conn = pool.try_acquire().ok_or(AppError::PoolFull)?;

13.6 begin / try_begin / begin_with

sqlx-core/src/pool/mod.rs:370-418 的 begin 四件套:

pub async fn begin(&self) -> Result<Transaction<'static, DB>, Error>;
pub async fn try_begin(&self) -> Result<Option<Transaction<'static, DB>>, Error>;
pub async fn begin_with(&self, statement: impl Into<Cow<'static, str>>) -> Result<Transaction<'static, DB>, Error>;
pub async fn try_begin_with(&self, statement: impl Into<Cow<'static, str>>) -> Result<Option<Transaction<'static, DB>>, Error>;

begin() 等价于 acquire + Connection::begin——合并成一步:

let mut tx = pool.begin().await?;
sqlx::query(...).execute(&mut *tx).await?;
tx.commit().await?;

try_begin 是非阻塞版——内部走 try_acquire,拿不到空闲连接就返回 Ok(None) 而不是等待。

begin_with / try_begin_with 接受自定义 BEGIN 语句(比如 BEGIN ISOLATION LEVEL SERIALIZABLE)。

关键点:Transaction<'static, DB> 的 'static 生命周期。第 12 章讲的 Connection::begin 返回 Transaction<'_, DB>——tie 到 &mut conn。Pool 的 begin 返回 'static——因为 PoolConnection 自己带 Arc refcount,不依赖任何借用。

这条 'static 让 Pool-originated transaction 能跨 spawn 传:

let mut tx = pool.begin().await?;   // 注意 mut:async move 里要 &mut *tx
tokio::spawn(async move {
    sqlx::query(...).execute(&mut *tx).await?;
    tx.commit().await?;
    Ok::<_, sqlx::Error>(())
});

这种自由度在 Connection-originated transaction 做不到。

13.7 close / close_event / is_closed

sqlx-core/src/pool/mod.rs 里的三个方法(close 在 441-443、is_closed 在 446-448、close_event 在 530-532):

pub fn close(&self) -> impl Future<Output = ()> + '_ {
    self.0.close()
}

pub fn is_closed(&self) -> bool { self.0.is_closed() }
pub fn close_event(&self) -> CloseEvent { self.0.close_event() }

close 优雅关闭 Pool:

  1. 把 pool 标记为 closed——这一步在调用 close() 的当下就同步完成,不等 .await(sqlx-core/src/pool/inner.rs:97-98)。
  2. 所有 pending acquire() 调用立即返回 PoolClosed 错误,之后也不再开新连接。
  3. 所有空闲连接被逐条 close().await 优雅关闭(Postgres 会发 Terminate 消息)。
  4. 等待所有已借出的连接归还(它们归还时直接 close 而不是进 idle queue)。
  5. 返回的 Future 完成意味着所有连接干净关闭。

close 是 &self 不是 self 或 &mut self——因为 Pool 是 Arc 包裹、可能有其他 handle 还活着。即使其他 handle 在后台持有 Pool,close() 逻辑依然生效(通过 PoolInner 的 atomic flag)。源码文档还补了两条:多个 handle 上并发调 .close().await 是安全的;但如果返回的 Future 没有跑到完成,剩下的连接会在最后一个 handle 被 drop 时才丢掉——而那可能发生在 Pool 内部 spawn 的任务里,时机不可预测。

close_event() 返回一个 CloseEvent future——resolves when pool is closed。典型用法:

let pool2 = pool.clone();
tokio::spawn(async move {
    pool2.close_event().await;
    log::info!("Pool closing, running cleanup");
    // ... 清理逻辑
});

或者在长任务里让 pool 关闭能取消任务:

use sqlx::Executor; // pool.execute(...) 需要这个 trait

let res = pool.close_event().do_until(async {
    pool.execute("SELECT pg_sleep('30 days')").await
}).await;

do_until 是 CloseEvent 的方法(sqlx-core/src/pool/mod.rs:595)——把一个 Future 和"pool 关闭"放在一起 race、pool 关闭时取消内部 Future,并返回 Err(Error::PoolClosed);pool 已经关闭时内部 Future 根本不会被执行。所以上面 res 的类型是双层的 Result<Result<_, _>, Error>。适合"长查询 + 优雅 shutdown"场景。

is_closed() 同步检查——主要给 health check 用。

13.8 size / num_idle:监控指标

sqlx-core/src/pool/mod.rs:535-542:

pub fn size(&self) -> u32 { self.0.size() }
pub fn num_idle(&self) -> usize { self.0.num_idle() }

size() 返回当前 pool 持有的连接总数(空闲 + 借出,也包括正在建立中的那几条)。 num_idle() 返回空闲连接数(不含借出)。

典型监控代码:

loop {
    tracing::info!(
        target: "pool_metrics",
        total = pool.size(),
        idle = pool.num_idle(),
        // 两次读数不是同一瞬间的快照,用 saturating_sub 防止偶发下溢 panic
        in_use = (pool.size() as usize).saturating_sub(pool.num_idle()),
    );
    tokio::time::sleep(Duration::from_secs(10)).await;
}

实际指标:

  • total:当前 pool 大小(小于 max_connections 说明还能扩)。
  • idle:空闲——剩余容量。
  • in_use = total - idle:当前借出量——业务活跃度代理。

持续监控这两个数字能发现:min_connections 维持不住(backend 重启了?网络抖动?)、饱和(idle 长期 0 + in_use = max)、空置(long idle 但 max_connections 设太大)。

13.9 set_connect_options:热更新 Options

sqlx-core/src/pool/mod.rs:555-564(配套的读侧是 connect_options(),545-551):

pub fn set_connect_options(&self, connect_options: <DB::Connection as Connection>::Options) {
    let mut guard = self
        .0
        .connect_options
        .write()
        .expect("write-lock holder panicked");
    *guard = Arc::new(connect_options);
}

内部是一把 RwLock<Arc<Options>>——新建连接时读一次、换配置时写一次。

运行时替换 Options——已建立的连接保留,新建的按新 options。典型用例:

  1. 密码旋转:每周自动刷新 DB 密码。新密码生效后,set_connect_options(new_opts)——老连接继续跑到 max_lifetime 后自然退休、新建的用新密码。
  2. 数据库 failover:主库挂了切换到从库。改 options 的 host,新连接走新主。
  3. 动态 application_name:按部署版本打 tag,每次部署启动时 set 一次。

注意 不影响已建连接 ——因为 live connection 的配置是建立时固化的。sqlx 不主动关掉老连接——让它们自然退休,避免应用级断连风暴。

13.10 PoolOptions 的 13 个配置项

sqlx-core/src/pool/options.rs:45-88 的结构定义 + new() 默认值(sqlx-core/src/pool/options.rs:143-166),所有 13 个配置项:

配置 类型 默认 作用
max_connections u32 10 硬上限——超过则 acquire 排队
min_connections u32 0 最小维持数——eager 建池时建这么多(lazy 建池不会,见 §13.3),回收时补齐
acquire_timeout Duration 30s acquire 的总超时
idle_timeout Option<Duration> Some(10 min) 空闲连接最多保留多久——超过 close
max_lifetime Option<Duration> Some(30 min) 一条连接最多活多久——超过定期退休
test_before_acquire bool true 空闲连接交出前先 ping(多一个往返但更可靠)
fair bool true 信号量是否公平(FIFO 排队)vs 抢占;setter 是 #[doc(hidden)] 的 __fair
acquire_time_level LevelFilter Off 所有 acquire 的日志级别
acquire_slow_level LevelFilter Warn 慢 acquire 的日志级别
acquire_slow_threshold Duration 2s 多久算"慢"
after_connect Option<Fn> None 新连接建立后回调(设 session 参数等)
before_acquire Option<Fn> None acquire 前的额外校验回调
after_release Option<Fn> None 连接归还后的清理回调

(13 项——parent_pool 不算公共配置,它的 setter parent() 是 #[doc(hidden)],供 #[sqlx::test] 的测试库隔离用。fair 虽列在表里,也只有隐藏的 __fair 能改,源码注释说目前只为基准测试暴露,第 14 章 §14.4.1 还会讲到:Tokio 下设成 false 在 debug 构建里直接 panic(debug_assert!(fair, "Tokio only has fair permits")),release 构建里被静默忽略。)

13.10.1 生产推荐配置

默认值(max=10)对任何稍大流量都不够。生产典型配置:

PgPoolOptions::new()
    .max_connections(50)                      // 根据业务容量 + DB max_connections 算
    .min_connections(5)                        // 预留热连接
    .acquire_timeout(Duration::from_secs(5))   // 默认 30s 太长 - 生产宁愿快 fail
    .idle_timeout(Duration::from_secs(600))    // 10 min(默认就是)
    .max_lifetime(Duration::from_secs(1800))   // 30 min(默认就是)
    .test_before_acquire(false)                // 关 ping——省一个往返——生产接受小风险
    .connect(&database_url).await?

max_connections 的选择是最重要的一条:

  • 太小:请求排队、P99 延迟飙升。
  • 太大:DB 的 max_connections 被撞上限,整个服务拒绝新连接。

一个可操作的上界是:所有副本的 max_connections 之和,要留出余量地小于数据库自己的 max_connections(还得给运维工具、监控、迁移任务留几条)。下界则由业务并发决定——长期看到 num_idle() 为 0 而 size() 顶到上限,就是该调大的信号(§13.8)。

13.11 三个回调:after_connect / before_acquire / after_release

这三个回调是 Pool 的高级扩展点(sqlx-core/src/pool/options.rs:after_connect 380、before_acquire 435、after_release 492)。三者的闭包签名统一为 Fn(&mut DB::Connection, PoolConnectionMetadata) -> BoxFuture<..>——PoolConnectionMetadata(sqlx-core/src/pool/options.rs:117-128)带两个字段:age(连接开出来多久)和 idle_for(在空闲队列里待了多久,只对 before_acquire 有意义;其余回调是 Duration::ZERO)。源码里还专门用一大段注释解释了为什么闭包必须返回 BoxFuture 而不是泛型 Future——高阶生命周期表达不出来。

13.11.1 after_connect:新连接初始化

PgPoolOptions::new()
    .after_connect(|conn, meta| Box::pin(async move {
        // 每条新连接建立后运行
        sqlx::query("SET application_name = 'my_service'")
            .execute(&mut *conn).await?;
        sqlx::query("SET statement_timeout = 5000")  // 5s
            .execute(&mut *conn).await?;
        Ok(())
    }))

典型用途:

  • 设 session 参数(application_name / statement_timeout / search_path)。
  • 注册 prepared statement(如果有"所有连接都用"的热 statement)。
  • 设 timezone。

回调的开销是 每条新连接一次——不是每次 acquire。在默认的 max_lifetime 30 分钟 / idle_timeout 10 分钟下,一条连接平均能服务很久,所以这里做几条 SET 的开销可以忽略。

13.11.2 before_acquire:acquire 时的额外校验

PgPoolOptions::new()
    .before_acquire(|conn, meta| Box::pin(async move {
        // 返回 Ok(true) 交给调用方;Ok(false) 或 Err 都会关掉这条连接、改建新连接
        if meta.idle_for > Duration::from_secs(60) {
            let result: Result<i32, _> = sqlx::query_scalar("SELECT 1")
                .fetch_one(&mut *conn).await;
            Ok(result.is_ok())
        } else {
            Ok(true)
        }
    }))

注意 Err 不会冒泡给 acquire() 的调用方:check_idle_conn(sqlx-core/src/pool/inner.rs:481-497)只记一条 warn 日志、硬关这条连接,然后 acquire 改建一条新连接(SQLite 实测:回调恒返回 Err 时 acquire() 仍然成功)。另外它只对空闲队列里取出的连接调用,新建的连接不走它。

比 test_before_acquire 更精细——你可以用 meta.age / meta.idle_for 决定 ping 不 ping。test_before_acquire 全开会让每次 acquire 都多一个往返;before_acquire 可以只让"闲置够久"的连接去 ping、刚归还的直接放行——省掉平均延迟。

13.11.3 after_release:归还后清理

PgPoolOptions::new()
    .after_release(|conn, meta| Box::pin(async move {
        // 返回 Ok(true) 继续归还(sqlx 随后还会 ping 一次再放回 idle queue);Ok(false) 或 Err 都关掉这条连接
        sqlx::query("DISCARD TEMP")  // Postgres:清临时表
            .execute(&mut *conn).await?;
        Ok(true)
    }))

典型用途:

  • 清 session 状态(Postgres 的 DISCARD TEMP / DISCARD PLANS)——避免"上个请求的 temp table 影响下个请求"。别用 DISCARD ALL:按 Postgres 文档它包含 DEALLOCATE ALL,会把 sqlx 在这条连接上缓存的预处理语句全部释放掉,而 sqlx 的语句缓存并不知情。
  • 检测连接状态可疑(比如 conn.is_in_transaction() 为 true)——返回 false 丢弃。
  • 归还前做一致性校验。

实际生产里这个回调用得少——太频繁(每次 release 都调)成本高。通常只在状态污染严重的场景(比如大量 prepared statement + schema 变更频繁)开启。

13.12 三家 DB 的类型别名

类型别名定义在各自的驱动 crate 里、再由 facade crate 按 feature 重导出:

// sqlx-postgres/src/lib.rs:67,70
pub type PgPool = crate::pool::Pool<Postgres>;
pub type PgPoolOptions = crate::pool::PoolOptions<Postgres>;

// sqlx-mysql/src/lib.rs:54,57
pub type MySqlPool = crate::pool::Pool<MySql>;
pub type MySqlPoolOptions = crate::pool::PoolOptions<MySql>;

// sqlx-sqlite/src/lib.rs:102,105
pub type SqlitePool = crate::pool::Pool<Sqlite>;
pub type SqlitePoolOptions = crate::pool::PoolOptions<Sqlite>;

生产代码里几乎只写 PgPool / MySqlPool / SqlitePool——比 Pool<Postgres> 好读。函数签名里也用别名:

async fn find_user(pool: &PgPool, id: i32) -> Result<User, Error> { ... }

如果你写跨 DB 的库代码,用 Pool<DB: Database> 的泛型参数——但业务代码用类型别名。

13.13 Pool 的生命周期可视化

Pool 在服务生命周期里的状态流转:

stateDiagram-v2
    [*] --> Uninit: PoolOptions new
    Uninit --> Eager: connect await
    Uninit --> Lazy: connect_lazy
    Eager --> Active: 预建连接成功
    Lazy --> Active: 首次 acquire 触发建连
    Active --> Active: acquire release 循环
    Active --> Active: min_connections 维护
    Active --> Active: set_connect_options 热更新
    Active --> Closing: close 调用
    Closing --> Closed: 所有连接归还完成
    Closed --> [*]: Arc 最后一个 handle drop

四个主要状态:

  • Uninit:PoolOptions 构造中,还没调 connect。
  • Lazy / Eager:两种初始化路径——Lazy 当场不建连接、Eager 返回前已有 max(1, min_connections) 条。
  • Active:正常工作状态——用户 acquire/release 循环。
  • Closing → Closed:close 触发,等连接归还+关闭。

正常生产服务绝大部分时间在 Active——其他状态只在启动/关闭时经过。理解这些转换对写"服务启动代码"和"优雅 shutdown 代码"至关重要。

13.14 acquire 的完整执行路径

把 pool.acquire().await? 的完整路径画出来:

sequenceDiagram
    participant U as 用户
    participant Pool as Pool<DB>
    participant Inner as PoolInner
    participant Sem as AsyncSemaphore
    participant Idle as ArrayQueue<Idle>
    participant Conn as DB::Connection
    U->>Pool: acquire().await
    Pool->>Inner: shared.acquire()
    Inner->>Sem: acquire_permit()
    Note over Sem: 如果已达 max_connections 阻塞<br/>(受 acquire_timeout 限制)
    Sem-->>Inner: permit
    Inner->>Idle: try pop Idle connection
    alt 有空闲
        Idle-->>Inner: Idle<DB>
        opt test_before_acquire
            Inner->>Conn: ping()
            Note over Conn: 失败则硬关这条, 改建新连接
        end
    else 无空闲
        Inner->>Conn: establish new connection
        Conn-->>Inner: DB::Connection
    end
    Inner-->>Pool: Floating<Live<DB>>
    Pool->>Pool: reattach() → PoolConnection
    Pool-->>U: Ok(PoolConnection)

四个关键步骤:

  1. 获取 Semaphore permit——保证不超过 max_connections。受 acquire_timeout 限制。
  2. 从 idle queue 弹一条(如果有)——零开销,直接复用。
  3. 如果没有空闲——建新连接。
  4. 可选 ping——test_before_acquire 开启时多一次协议探活。

这条路径的快慢相差悬殊:命中空闲队列时,是一次信号量获取 + 一次无锁队列弹出,再加上默认开启的 test_before_acquire 那一次 ping 往返(关掉它才是全程进程内操作);没命中时要走完第 12 章 §12.15 那整套建连握手。Pool 的全部价值就是让绝大多数 acquire 落在前一条路径上。

13.15 阶段小结

到这里,Pool 的外部 API 已经完整拆开:

  1. Pool struct 是 Arc 包装(§13.2)——Clone 零开销、&pool 做 Executor、跨线程自然。
  2. 四个构造函数(§13.3)—— eager/lazy × URL/Options 两维四组合。生产选 eager + Options(精细配置)。
  3. acquire 的超时和取消行为(§13.4)—— 取消会丢连接(保守但正确);超时返回 PoolTimedOut。
  4. try_acquire 非阻塞(§13.5)—— 用于健康检查 / fallback。
  5. begin / try_begin / begin_with(§13.6)—— acquire + begin 的合并捷径;返回 Transaction<'static> 支持跨 await。
  6. close / close_event(§13.7)—— 优雅关闭;close_event 支持"pool 关闭时取消长任务"模式。
  7. size / num_idle(§13.8)—— 生产监控必备。
  8. set_connect_options(§13.9)—— 运行时密码旋转 / failover。
  9. PoolOptions 13 个配置(§13.10)—— 默认值对生产基本都要改。max_connections 是最关键一项。
  10. 三个回调(§13.11)—— after_connect 初始化 session、before_acquire 精细校验、after_release 清理状态。生产最常用 after_connect。
  11. 类型别名 PgPool / MySqlPool / SqlitePool(§13.12)—— 业务代码首选。

后面几节转向生产实践:怎么定 max_connections、怎么和 PgBouncer 配合、启动与关闭代码怎么写、出了问题怎么排查。

13.16 max_connections 的具体选择指南

max_connections 可能是 Pool 最难调的一项。下表是按场景给的经验起点,不是基准测试结论,最终要以压测和监控(§13.8)为准:

场景 推荐 max_connections 理由
开发 / 测试 5 本地足够,少占 DB 资源
小流量 web(< 100 QPS) 10-20 默认够
中等 web(100-1000 QPS) 20-50 根据 CPU × 业务并发
高流量 web(> 1000 QPS) 50-100 + 多实例水平扩展
后台 worker(OLAP 查询) 5-10 长查询 + 慢 DB,连接不宜多
批量导入 ETL 2-5 单作业,不需要多
数据库代理(高并发转发) 100-500 特殊场景

核心公式:

max_connections = min(
    服务并发量 × 单请求最大连接数,
    (DB_max_connections - 保留 buffer) / 服务实例数
)

例:你的 web 服务单请求最多用 2 条连接(一条查用户、一条查订单)、并发 20、服务部署 5 个实例、DB max_connections = 200、留 20 给运维:

app 侧:20 × 2 = 40
DB 侧:(200 - 20) / 5 = 36
取小:max_connections = 36

生产里通常设 30 留点余量。

超过 DB 上限的后果:当总连接数(所有实例 × 每实例 max_connections)超过 DB 的 max_connections 时,新连接会被 DB 拒绝。Postgres 回的是 53300("sorry, too many clients already"),sqlx 把它当作建连期瞬时错误、退避重试到 acquire_timeout(§13.4.1),所以应用侧看到的是一片 PoolTimedOut;MySQL 的 Too many connections(1040)不在重试之列,会直接作为数据库错误返回。这是 sqlx Pool 无法自动避免的——需要事先算好数字。

13.17 Pool 与 PgBouncer 的对比

生产 Postgres 部署里常用 PgBouncer 作为 server-side 连接池。sqlx Pool 和 PgBouncer 可以叠加使用——app → sqlx Pool → PgBouncer → Postgres。

两者角色不同:

  • sqlx Pool:app 内、进程级、Rust Future 感知。负责"app 内的连接分配"。
  • PgBouncer:server 外、独立进程、协议级。负责"多个 app 实例共享 Postgres 连接"。

典型叠加场景:你有 50 个 Kubernetes pod、每个 pod 的 sqlx Pool max_connections = 10——总计 500 个 "逻辑连接"。Postgres max_connections 只 200——撞上限。

解法:前面放 PgBouncer(max_client_conn=500, default_pool_size=30):

  • 500 个 app 逻辑连接连到 PgBouncer(便宜、PgBouncer 不 fork 后端)。
  • PgBouncer 复用 30 条 real Postgres 连接。
  • Postgres 只看到 30 个连接——远低于 200 上限。

PgBouncer 的 transaction pool mode 适合大多数业务:每个 transaction 独占一条 real connection、事务结束立即归还。

需要注意:PgBouncer 的 transaction pool 下,同一条客户端连接的前后两个事务可能落在不同的 server 连接上,sqlx 按连接缓存的命名预处理语句(persistent = true)就可能在新的 server 连接上找不到。PgBouncer 1.21.0(2023-10-16)起支持协议级命名预处理语句(需把 max_prepared_statements 设为非零,见 PgBouncer 官方 changelog);用更早的版本,或不想依赖这个特性时,需要关掉:

sqlx::query("...").persistent(false).execute(&pool).await?;

或者用 PgBouncer 的 session pool mode(一个 app 连接 1:1 对应一条 server 连接、但少了 PgBouncer 的复用优势)。

读懂 sqlx Pool 和 PgBouncer 的角色差异后,你能设计多层连接池——这是大型 Postgres 部署的常规做法。

13.18 实战:一个典型的 Axum + sqlx 启动代码

把 Pool 的生产配置完整写一次:

use axum::{Router, routing::get, extract::State};
use sqlx::postgres::{PgPool, PgPoolOptions};
use std::time::Duration;

#[derive(Clone)]
struct AppState {
    pool: PgPool,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. 读环境变量
    let database_url = std::env::var("DATABASE_URL")?;
    let max_conn = std::env::var("POOL_MAX_CONN")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(20);

    // 2. 建 Pool(eager + 详细配置)
    let pool = PgPoolOptions::new()
        .max_connections(max_conn)
        .min_connections(max_conn / 4)          // 保留四分之一热
        .acquire_timeout(Duration::from_secs(5))
        .idle_timeout(Some(Duration::from_secs(600)))
        .max_lifetime(Some(Duration::from_secs(1800)))
        .test_before_acquire(false)              // 关 ping
        .after_connect(|conn, _| Box::pin(async move {
            // 每条新连接设 statement_timeout
            sqlx::query("SET statement_timeout = 10000")  // 10s
                .execute(&mut *conn).await?;
            Ok(())
        }))
        .connect(&database_url).await?;

    // 3. Pool 状态监控任务
    let pool2 = pool.clone();
    tokio::spawn(async move {
        let mut interval = tokio::time::interval(Duration::from_secs(10));
        loop {
            interval.tick().await;
            tracing::info!(
                pool.size = pool2.size(),
                pool.idle = pool2.num_idle() as u32,
                "pool metrics"
            );
        }
    });

    // 4. 启动 Axum;Ctrl+C 时先停 HTTP(处理完在途请求)
    let app = Router::new()
        .route("/health", get(health))
        .with_state(AppState { pool: pool.clone() });

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app)
        .with_graceful_shutdown(async {
            tokio::signal::ctrl_c().await.expect("failed to listen for Ctrl+C");
        })
        .await?;

    // 5. HTTP 停下之后再关 Pool,等所有连接归还并关闭
    tracing::info!("shutting down pool");
    pool.close().await;
    Ok(())
}

async fn health(State(s): State<AppState>) -> &'static str {
    if sqlx::query_scalar::<_, i32>("SELECT 1").fetch_one(&s.pool).await.is_ok() {
        "ok"
    } else {
        "unhealthy"
    }
}

这段代码包含 Pool 的所有实用技巧:

  1. 配置从 env 读 —— max_connections 可以按环境调。
  2. 明确配置每个关键项 —— 不依赖默认。
  3. 后台指标任务 —— 每 10s 输出 size/idle,生产监控。
  4. 优雅 shutdown —— Ctrl+C 先让 axum 停止接收新请求、处理完在途请求,serve 返回后再 close() Pool 等连接归还。顺序不能反:tokio::signal::ctrl_c() 一旦开始监听,进程就不再按默认方式被 Ctrl+C 终止(tokio 文档原话:default behavior "will NOT be reset",tokio/src/signal/ctrl_c.rs:22-31)。如果只在一个旁路任务里 close() Pool,服务器会继续运行,健康检查一律返回 unhealthy(SQLite 等价版实测)。
  5. 健康检查 endpoint —— 简单的 SELECT 1 验证 Pool 可用。

生产部署把这套代码微调一下(换成你的日志 / metrics stack),就是一份可用的后端启动模板。

13.19 Pool 层面的常见故障与排查

生产 Pool 常见问题及排查路径:

症状 1:大量 PoolTimedOut 错误

原因:并发超过 max_connections + acquire_timeout 排队不及时。排查:

  1. 看 pool.size() / num_idle()——如果 size = max_connections && idle = 0 持续几秒以上——Pool 饱和。
  2. 查 DB 的慢查询日志——有没有长查询吃着连接不放?
  3. 上游请求流量有没有突增?

解决:增大 max_connections(前提 DB 允许)、优化慢查询、加队列上限降级。

症状 2:DB 侧报连接数超限(Postgres 日志里是 "sorry, too many clients already",MySQL 是 Too many connections)

原因:所有服务实例加起来超过 DB 的 max_connections。Postgres 下这个错误会被 sqlx 退避重试吞掉,应用侧表现为症状 1 的 PoolTimedOut(§13.16)。排查:

  1. 在 Postgres 跑 SELECT count(*) FROM pg_stat_activity——实际连接数。
  2. 算你的 app 应该有多少:instance_count × pool_max_connections = ?
  3. 如果 1 > 2——其他服务/工具也在连(监控 / 管理工具);如果 1 ≈ 2——你的 pool 总量就是过大。

解决:加 PgBouncer(§13.17)、减小 pool_max_connections、减少实例数。

症状 3:连接数缓慢增长

原因:应用泄漏了 PoolConnection(没让它 drop)。排查:

  1. 看 pool.size() 是否随时间缓慢升高?
  2. 业务代码里有没有把 PoolConnection 存进 Vec 或 long-lived struct?
  3. 有没有 spawn 了 task 持有 conn 但 task 没结束?

解决:修复泄漏。PoolConnection 应该短期持有、离开作用域就 drop。

症状 4:重启后连接池很慢填满

原因:min_connections 默认 0、Pool 懒建立连接。排查:

  1. 启动初期 idle = 0——每次 acquire 都建新连接、慢。

解决:min_connections = max_connections / 4,并用 eager 的 connect / connect_with 建池——启动时预热(lazy 建池不会按 min_connections 预热,见 §13.3)。

症状 5:dead connections 造成零星 query 失败

原因:DB server 重启 / 中间防火墙断开了空闲连接。排查:

  1. 看 query 错误类型——Error::Io(connection reset / broken pipe 之类)是典型。
  2. DB 日志里有没有 connection reset 记录?

解决:保持 test_before_acquire 默认开启(别在 §13.10.1 那样关掉),或设更短的 idle_timeout(比 DB/防火墙的超时短)。

这五个症状是 Pool 相关问题里最常见的几类。遇到新问题先排查这五类——根因通常在其中之一。

13.20 Pool 设计的三条启示

读完本章,从 sqlx Pool 设计萃取三条通用启示:

1. Arc + Inner 的分层是服务对象的标准结构。Pool 对外 Arc 轻量(clone 零开销、跨线程自然)、PoolInner 内藏复杂状态——对外简洁、对内富集。任何"多个 handle 共享一份资源"的 Rust 设计都值得参考这条模式。

2. 提供 try_* 非阻塞版本。acquire 有 try_acquire、begin 有 try_begin——给用户"阻塞 vs 立刻失败"的选择。这种 API 对偶让代码在 fallback / 降级场景更清爽——用户不需要手动 select! 加超时。

3. 配置与回调的分离。PoolOptions 的 13 个配置里 3 个是回调(Fn)——数据型配置和逻辑型配置分开。这让普通用户只设几个数字、高级用户能注入复杂逻辑——"默认简单、需要时深入"的 API 阶梯。

这三条对"管理一组资源"的任何 Rust 库都适用——HTTP 客户端 pool、Redis pool、自定义资源 pool——都值得遵守这套结构。

13.21 Pool 相关的配置全景表

再把本章讨论的所有 PoolOptions 配置项放一张完整表——按作用分类:

连接数量:

  • max_connections = 10——Pool 硬上限。生产必改。
  • min_connections = 0——最小维持数。推荐 max/4。

超时:

  • acquire_timeout = 30s——acquire 总时限。推荐 5s(短快 fail 优于慢排队)。
  • idle_timeout = 10min——空闲连接保留时间。
  • max_lifetime = 30min——连接总寿命。源码注释建议别设成无限,哪怕一天退休一次,也让数据库有机会回收会话级资源。

健康检查:

  • test_before_acquire = true——acquire 前 ping。推荐生产 关掉(省延迟)。
  • 替代方案:before_acquire 回调做精细校验。

公平性:

  • fair = true——信号量公平排队。只能经隐藏的 __fair 改;Tokio 下改成 false,debug 构建直接 panic、release 构建静默忽略(第 14 章 §14.4.1)。

日志:

  • acquire_time_level = Off——所有 acquire 的日志级别。
  • acquire_slow_level = Warn——慢 acquire 日志级别。
  • acquire_slow_threshold = 2s——多久算慢。

回调(可选):

  • after_connect——新连接初始化(SET session vars)。
  • before_acquire——每次 acquire 前额外校验。
  • after_release——归还后清理(DISCARD TEMP 等)。

总计 13 个可配置项——但生产只有 5-6 个需要改。真正必改的只有 max_connections;其他按业务需求可选。

13.22 三家 DB 的 Pool 行为差异

虽然 sqlx 的 Pool 是泛型 Pool<DB>,三家 DB 下的实际行为有一些细微差异:

差异 PgPool MySqlPool SqlitePool
连接成本 TCP + 可选 TLS + 认证,多个网络往返 同 Postgres 本地 sqlite3_open_v2 + 起一个 worker 线程
推荐 min_connections max_connections / 4(预热) 同 Postgres 可以 0(建立便宜)
idle_timeout 建议 保持在中间件/防火墙的空闲超时之下 同 Postgres 意义不大
max_lifetime 建议 有意义(避免长连接累积状态) 同 Postgres 不重要(无 server 端 session 状态)
test_before_acquire 可以关(省一个网络往返) 同 Postgres ping 只是一次线程往返,开关影响小
并发限制场景 服务端 max_connections 服务端 max_connections 文件锁 / WAL,而不是连接数

SQLite 特殊——它的"连接"是本地文件 handle,不走网络、没有 server-side 连接数限制。SQLite 一个数据库文件同一时间只能有一个写者(WAL 模式下读者可以和这个写者并发,但写者仍只有一个),Pool 的 max_connections 变相限制了同时争抢写锁的连接数。

实际上生产 SQLite 用 Pool 不多——用 SqliteConnection::connect_with().await? 直接一个 mutable connection 往往够用。Pool 在 SQLite 场景的主要作用是让 sqlx 的 API 统一(Executor trait 需要 Pool 或 &mut Connection)。

13.23 Pool 与 tokio task 的关系

Pool 内部会起一个后台维护任务(sqlx-core/src/pool/inner.rs:503 的 spawn_maintenance_tasks),它在一个循环里同时干三件事:

  • Idle timeout 驱逐——空闲时长超过 idle_timeout 的连接被 close。
  • Max lifetime 驱逐——建立时间超过 max_lifetime 的连接强制退休。
  • Min connections 补齐——每回收掉一条就顺手把连接数补回 min_connections。

几条实现细节值得记住:

  • 只有一个任务,不是三个。循环周期是 min(max_lifetime, idle_timeout);两者都是 None 时根本不起循环任务——只在 min_connections > 0 时 spawn 一次性的补齐任务就返回。
  • spawn 走的是 crate::rt::spawn,不是直接 tokio::spawn——这样 async-std 运行时下同样能用。
  • 任务只持 Weak<PoolInner>(源码注释:"so they don't keep PoolInner from being dropped")——最后一个 Pool handle 掉了,upgrade() 失败,任务自己退出,不会让 Pool 因为后台任务而永远活着。
  • 整个循环包在 close_event().do_until(...) 里——close() 一调用,维护任务立刻取消。

用户看不到这些但它一直在跑。用户代码只感受到"Pool 自己会维护连接"。

任务的生命周期:

  • Pool 启动时 spawn。
  • Pool close 时,外层的 do_until 立即取消循环,任务退出。
  • Pool 自己 drop(最后一个 Arc handle 释放)时任务也退出。

这条隐式依赖意味着 Pool 需要一个活着的异步运行时(Tokio 或 async-std)。在 Tokio 下,如果在 runtime 之外构造 Pool——哪怕是同步的 connect_lazy——crate::rt::spawn 找不到 runtime,会在创建连接池那一刻 panic:"this functionality requires a Tokio context"(sqlx-core/src/rt/mod.rs:61-78、138-144;默认 idle_timeout 非空,维护任务必定要 spawn)。生产代码里 Pool 几乎总是在 runtime 内构造——这条约束不显眼但真实存在。

第 14 章会详细讲这个维护任务的具体实现——它怎么用 try_acquire + release 扫一遍空闲队列、is_beyond_idle_timeout / is_beyond_max_lifetime 怎么判定、以及 min_connections_maintenance 的补齐逻辑。

13.24 Pool 和 transaction 的组合

pool.begin().await? 的典型用法再回顾一次:

// Error 是业务自定义的错误类型:含 InsufficientFunds 变体,并实现 From<sqlx::Error>
pub async fn transfer(pool: &PgPool, from: i32, to: i32, amount: i64) -> Result<(), Error> {
    let mut tx = pool.begin().await?;

    // 扣款
    let rows = sqlx::query("UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1")
        .bind(amount).bind(from)
        .execute(&mut *tx).await?
        .rows_affected();

    if rows != 1 { return Err(Error::InsufficientFunds); }

    // 入账
    sqlx::query("UPDATE accounts SET balance = balance + $1 WHERE id = $2")
        .bind(amount).bind(to)
        .execute(&mut *tx).await?;

    tx.commit().await?;
    Ok(())
}

Pool 管连接、Transaction 管事务语义——两者职责分开。Pool 提供 begin 捷径让代码少一行(不用手动 acquire + begin),但本质上 Pool 不参与事务逻辑——事务的 commit/rollback 由 Transaction 自己管、第 15 章详细。

一个关键观察:Transaction<'static> 的 'static 来自 PoolConnection 的 Arc refcount——不是"没有任何借用",而是"借用的对象(PoolInner)通过 Arc 保持活着"。这让事务可以自由跨 await / spawn——用户无需关心生命周期。

13.25 Pool API 演进的几次关键改进

回顾 Pool 从 0.4 到 0.8 的几次关键改进,以下条目逐个对照各 tag 下的 sqlx-core/src/pool/ 和 CHANGELOG.md:

  • 0.4.0(2020-11):PoolOptions 链式配置(0.3 叫 Builder,配置名是 max_size / min_size / test_on_acquire)、connect_lazy / connect_lazy_with、Pool::begin 返回 Transaction<'static, DB> —— 今天这套外部 API 的骨架此时已经成型。
  • 0.5.12 / 0.5.13(2022-04):close_event() 进来(0.5.11 及之前都没有),优雅 shutdown 从"close 完等着"变成"可以让长任务感知关闭"。
  • 0.6.0(2022-06):Pool::connect_options() / Pool::options() 把两套配置暴露出来读(PR #1897);另有一次"Pool 的破坏性修正"(PR #1901)——PoolOptions::connect_timeout 更名为 acquire_timeout(现在这个名字更准确:它管的是整个 acquire 而不只是建连),after_connect / before_acquire / after_release 三个回调的签名也在这一版改成今天的 (&mut conn, PoolConnectionMetadata) -> BoxFuture 形态,close() 也改成调用即开始关闭、.await 只为等它关干净。
  • 0.7.0(2023-06):Pool::set_connect_options(PR #2088)和 PoolOptions 的 get_* 读取方法(PR #2113)——密码旋转、failover 这类运行时改配置的场景从这时起才有官方支持。
  • 0.8.0(2024-07):acquire_time_level / acquire_slow_level / acquire_slow_threshold 这组 acquire 日志控制项加入(0.7 及之前的 PoolOptions 里没有)。

这条演进线的重心很清楚:外部 API 早就定型,后面几个版本加的全是"运维能力"——优雅关闭、运行时改连接配置、acquire 慢了要能看见。

13.26 从用户视角的 Pool 三条 Golden Rule

把本章所有内容浓缩成三条用户视角的黄金规则:

规则 1:永远改 max_connections。默认 10 几乎不适合任何生产场景。按 §13.16 的公式算出合适值、从 env 读取——让运维能调。

规则 2:永远监控 pool.size() 和 num_idle()。这两个数字能告诉你 pool 的饱和度、连接泄漏、连接抖动——任何 Pool 相关的生产问题都可以先看这两个指标。

规则 3:handler 用 &pool、事务用 &mut *tx。不要在 handler 里手动 acquire——让 sqlx 自己管。事务用 &mut *tx 通过 DerefMut 满足 Executor bound(第 4 章 §4.6 讨论过)。

下一章进入 Pool 的内部实现——idle_conns 的 ArrayQueue、AsyncSemaphore、后台维护任务与 PoolConnection 的 Drop 归还。