Skip to content

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

"A connection pool's hardest job is not giving connections— it's deciding when not to give them." —— 每个写过 pool 配置的后端工程师说过的话

本章要点

  • 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(立即建一条连接)vs lazy(首次 acquire 才建)× URL vs Options 两维组合。
  • 核心方法 acquire()mod.rs:338-344)—— 返回 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() 返回一个 Future,可以用 do_until 取消长查询。
  • PoolOptions 13 个配置(§13.9)—— max_connections 默认 10(生产要改)、min_connections 默认 0、acquire_timeout 30s、idle_timeout 10 分钟、max_lifetime 30 分钟、test_before_acquire 默认 true、fair 默认 true、以及 3 个回调(after_connect / before_acquire / after_release)+ 3 个日志级别控制。
  • set_connect_options 热更新—— 改 DATABASE_URL 密码旋转、数据库迁移时无缝切换;已建立的连接保留、新建的按新 options。

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

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

  • 建立连接太贵——每次 Postgres 握手 3-5ms(本地)/50-200ms(跨机房),每次请求都握手会把 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

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

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

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

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

rust
// 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 的构造函数四件套:

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

两个维度的四种组合:

URL 字符串预构造 Options
eagerconnect(url)connect_with(options)
lazyconnect_lazy(url)connect_lazy_with(options)

eager vs lazy 的区别:

  • eagerconnect / connect_with):立即建立一条连接——返回 Pool 前先建一次、验证 URL 可用。失败返回 Error。
  • lazyconnect_lazy / connect_lazy_with):不建连接——只构造 Pool 元数据、首次 acquire() 时才建。

选 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 等 40+ 项)。生产通常用 Options。

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

Pool::connect(url) 的实现(mod.rs:286-288):

rust
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)

rust
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():借一条连接

mod.rs:338-344

rust
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——把 Pool 的所有权""进 Future。

2. PoolConnection<DB> 不是普通 Connection——它是 Pool 管理的智能指针,Deref 到 DB::Connection。Drop 实现会归还连接给池子(§13.4.2)。

3. reattach() 做什么?——内部把 Idle<DB> 包装成 Live<DB>——连接从"空闲队列"状态转到"借出"状态。具体第 14 章讲。

13.4.1 acquire 的超时机制

acquire 受 acquire_timeout 约束(默认 30 秒)。超时返回 Error::PoolTimedOut

超时的两种原因:

  • Pool 已满:max_connections 条连接都被占用,排队等了 30 秒。
  • 建新连接失败:Pool 尝试建新连接但 DB 连不上、超时发 StartupMessage 等。

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

13.4.2 acquire 的取消行为

源码 mod.rs:317-336 有一段重要的文档注释:

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 连接、不归还——因为 sqlx 不能安全验证"取消点"连接的状态(是否有未完成的消息、是否 transaction 中间)。保守做法是丢弃、让 Pool 建新的。

对用户的影响:

rust
tokio::select! {
    conn = pool.acquire() => { /* 用 conn */ }
    _ = some_timeout => { /* acquire 被取消——那个刚借到的连接被 drop */ }
}

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

13.5 try_acquire:非阻塞

mod.rs:350-352

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

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

  • Some(conn):有空闲连接、直接给你。
  • None:没有空闲 + 已达 max_connections——你自己决定怎么办(回退 / 稍后重试 / 降级)。

典型用途:非关键路径。比如健康检查想知道 "Pool 当前有余量吗":

rust
if let Some(conn) = pool.try_acquire() {
    // ... 健康检查,用完 drop
} else {
    log::warn!("pool at max capacity");
}

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

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

13.6 begin / try_begin / begin_with

mod.rs:357-415 的 begin 四件套:

rust
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——合并成一步:

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

try_begin 是非阻塞版——Pool 满了返回 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 传

rust
let tx = pool.begin().await?;
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

mod.rs:439-461

rust
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。
  2. 所有 pending acquire() 调用立即返回 PoolClosed 错误。
  3. 所有空闲连接被 close().await 发送 Terminate 消息。
  4. 等待所有已借出的连接归还(它们归还时直接 close 而不是进 idle queue)。
  5. 返回的 Future 完成意味着所有连接干净关闭

close&self 不是 self&mut self——因为 Pool 是 Arc 包裹、可能有其他 handle 还活着。即使其他 handle 在后台持有 Pool,close() 逻辑依然生效(通过 PoolInner 的 atomic flag)。

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

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

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

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

do_until 是 CloseEvent 的扩展方法——把一个 Future 包在 pool 关闭前的 "race" 里、pool 关闭时取消内部 Future。适合"长查询 + 优雅 shutdown"场景。

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

13.8 size / num_idle:监控指标

mod.rs:533-540

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

size() 返回当前 pool 持有的连接总数(空闲 + 借出)。 num_idle() 返回空闲连接数(不含借出)。

典型监控代码:

rust
loop {
    tracing::info!(
        target: "pool_metrics",
        total = pool.size(),
        idle = pool.num_idle(),
        in_use = pool.size() as usize - 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

mod.rs:552-566

rust
pub fn set_connect_options(&self, connect_options: <DB::Connection as Connection>::Options) {
    let mut guard = self.0.connect_options.write().expect("...");
    *guard = Arc::new(connect_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 个配置项

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

配置类型默认作用
max_connectionsu3210硬上限——超过则 acquire 排队
min_connectionsu320最小维持数——Pool 启动时建这么多,回收时补齐
acquire_timeoutDuration30sacquire 的总超时
idle_timeoutOption<Duration>Some(10 min)空闲连接最多保留多久——超过 close
max_lifetimeOption<Duration>Some(30 min)一条连接最多活多久——超过定期退休
test_before_acquirebooltrueacquire 前 ping 连接(多 1-5ms 但更可靠)
fairbooltrue信号量是否公平(FIFO 排队)vs 抢占
acquire_time_levelLevelFilterOff所有 acquire 的日志级别
acquire_slow_levelLevelFilterWarn慢 acquire 的日志级别
acquire_slow_thresholdDuration2s多久算"慢"
after_connectOption<Fn>None新连接建立后回调(设 session 参数等)
before_acquireOption<Fn>Noneacquire 前的额外校验回调
after_releaseOption<Fn>None连接归还后的清理回调

(13 项——parent_pool 不算公共配置,用于测试隔离)

13.10.1 生产推荐配置

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

rust
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(默认就是)—— Postgres 建议 <1h
    .test_before_acquire(false)                // 关 ping——省 1-5ms——生产接受小风险
    .connect(&database_url).await?

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

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

计算方法:max_connections = min(N * business_concurrency, DB_max_connections / N_instances)——N_instances 是你服务的副本数。典型 web 服务一个副本 20-50 合理;micro service 一个副本 5-10 够。

13.11 三个回调:after_connect / before_acquire / after_release

这三个回调是 Pool 的高级扩展点(pool/options.rs:380-496):

13.11.1 after_connect:新连接初始化

rust
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 min + idle_timeout 10 min = 大概每小时建几条),开销几乎可忽略。

13.11.2 before_acquire:acquire 时的额外校验

rust
PgPoolOptions::new()
    .before_acquire(|conn, meta| Box::pin(async move {
        // 返回 Ok(true) 保留连接、Ok(false) 丢弃重建、Err 抛错
        if meta.age > Duration::from_secs(100) {
            let result: Result<i32, _> = sqlx::query_scalar("SELECT 1")
                .fetch_one(&mut *conn).await;
            Ok(result.is_ok())
        } else {
            Ok(true)
        }
    }))

比 test_before_acquire 更精细——你可以按年龄条件决定 ping 不 ping。test_before_acquire 全开会让所有 acquire 多 1-5ms;before_acquire 可以只让老连接 ping、新连接直接放行——省平均延迟。

13.11.3 after_release:归还后清理

rust
PgPoolOptions::new()
    .after_release(|conn, meta| Box::pin(async move {
        // 返回 Ok(true) 归还到 idle queue、Ok(false) 丢弃
        sqlx::query("DISCARD TEMP")  // Postgres:清临时表
            .execute(&mut *conn).await?;
        Ok(true)
    }))

典型用途

  • 清 session 状态(Postgres 的 DISCARD TEMP / DISCARD PLANS / DISCARD ALL)——避免"上个请求的 temp table 影响下个请求"。
  • 检测连接状态 fishy(比如 transaction depth != 0)——返回 false 丢弃。
  • 归还前做一致性校验。

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

13.12 三家 DB 的类型别名

sqlx/src/lib.rs 按 feature 导出三个类型别名:

rust
pub type PgPool = Pool<Postgres>;
pub type MySqlPool = Pool<MySql>;
pub type SqlitePool = Pool<Sqlite>;

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

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

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

13.12.1 Pool 的生命周期可视化

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

四个主要状态

  • Uninit:PoolOptions 构造中,还没调 connect。
  • Lazy / Eager:两种初始化路径——Lazy 没连接、Eager 有一条。
  • Active:正常工作状态——用户 acquire/release 循环。
  • Closing → Closed:close 触发,等连接归还+关闭。

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

13.12.2 acquire 的完整执行路径

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

四个关键步骤:

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

这条路径的性能下限是步骤 1(信号量获取 + 队列弹出)——大约 1-2μs。性能上限是步骤 3(建新连接)——几毫秒到几百毫秒。稳态下(空闲够用)每次 acquire 落在 1-10μs 级——远快于第 12 章讨论的 Connection 直接 connect 的几毫秒。

13.13 本章小结

本章把 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)—— 业务代码首选。

下一章我们打开 PoolInner 的黑盒——看 Pool 的 idle_conns、AsyncSemaphore、max_lifetime 检查、min_connections 维护的内部机制怎么实现。

13.14 max_connections 的具体选择指南

max_connections 可能是 Pool 最难调的一项。给一份具体场景的建议数值:

场景推荐 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,连接不宜多
批量导入 ETL2-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 拒绝(too many connections)——所有实例都陷入 "acquire 超时" 循环。这是 sqlx Pool 无法自动避免的——需要事先算好数字

13.15 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 不支持 prepared statement 跨连接复用——sqlx 的 persistent = true 会导致 prepared statement 失效。需要关掉

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

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

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

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

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

rust
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. 优雅 shutdown
    let pool3 = pool.clone();
    tokio::spawn(async move {
        tokio::signal::ctrl_c().await.unwrap();
        tracing::info!("shutting down pool");
        pool3.close().await;
    });

    // 5. 启动 Axum
    let app = Router::new()
        .route("/health", get(health))
        .with_state(AppState { pool });

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).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 时 close 并等连接归还。
  5. 健康检查 endpoint —— 简单的 SELECT 1 验证 Pool 可用。

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

13.17 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:too many connections 错误(DB 侧)

原因:所有服务实例加起来超过 DB 的 max_connections。排查:

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

解决:加 PgBouncer(§13.15)、减小 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——启动时预热。

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

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

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

解决:开 test_before_acquire 或设更短的 idle_timeout(比 DB/防火墙的超时短)。

这五个症状覆盖 Pool 相关的 80% 生产问题。遇到新问题先排查这五类——根因通常在其中之一。

13.18 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.19 Pool 作为 sqlx 的生产接口

第四部分"连接与事务"到这里讲了 Connection(第 12 章)和 Pool(本章)——两个最贴近生产的抽象

大多数 sqlx 用户 99% 的代码里接触的都是 &pool——不是直接 Connection、也不是直接 Transaction。Pool 就是 sqlx 的生产接口 ——理解它的行为、配置、监控方式,就等于理解了 sqlx 在生产环境里的"人格"。

下一章进入 Pool 的内部实现——idle_conns 的 ArrayQueue、AsyncSemaphore 的公平性、spawn_maintenance_tasks 的后台维护任务——把黑盒打开给你看。读完第 14 章,你对 Pool 的每一个性能 quirk 都能定位到具体的源码位置。

13.20 Pool 相关的配置全景表

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

连接数量

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

超时

  • acquire_timeout = 30s——acquire 总时限。推荐 5s(短快 fail 优于慢排队)。
  • idle_timeout = 10min——空闲连接保留时间。
  • max_lifetime = 30min——连接总寿命。Postgres 建议 < 1 小时。

健康检查

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

公平性

  • fair = true——信号量公平排队。改成 false 一般没收益(第 14 章讨论)。

日志

  • 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 个需要改。上面加粗的是必改;其他按业务需求可选。

13.21 三家 DB 的 Pool 行为差异

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

差异PgPoolMySqlPoolSqlitePool
连接成本3-5ms(本地)3-5ms(本地)几十 μs
推荐 min_connectionsmax_connections / 4(预热)同 Postgres可以 0(建立便宜)
idle_timeout 建议10-30 min10-30 min短些(1-5 min)
max_lifetime 建议30 min-1h30 min-1h不重要(无累积状态)
test_before_acquire建议关(ping 多 1-5ms)同 Postgres开开关关影响小(ping 极快)
并发限制场景DB max_connections(典型 100-500)同 Postgresfile locking 而非 connection 数

SQLite 特殊——它的"连接"是本地文件 handle,不走网络、没有 server-side 连接数限制。Pool 对 SQLite 的主要价值是并发写保护——SQLite 一个数据库文件同一时间只能有一个写者(除非 WAL 模式下多读一写)。Pool 的 max_connections 变相限制了并发写。

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

13.22 本章的重要理解

读完本章,你应该能独立回答下面这些问题:

  1. Pool 的 Clone 为什么零开销?——Arc<PoolInner>,clone 只是 refcount++。
  2. connect vs connect_lazy 区别?——前者立即建一条连接、后者延迟到首次 acquire。
  3. acquire 超时的两种原因?——Pool 已满排队 / 建新连接失败。
  4. set_connect_options 什么时候有用?——密码旋转、failover、动态 application_name。
  5. max_connections 如何算?——min(并发 × 每请求连接数, DB上限/实例数)
  6. after_connect 典型用途?——设 session 参数(statement_timeout / application_name)。
  7. begin vs acquire 的选择?——begin 是 acquire + Connection::begin 的捷径,事务场景首选。
  8. PoolConnection 泄漏怎么发现?——监控 pool.size() 随时间增长 / 稳定态 idle 总是 0。

能答出这八题说明你对 Pool 的外部使用掌握到位了——剩下的内部机制(idle queue、semaphore)第 14 章详讲。

13.23 Pool 与 tokio task 的关系

Pool 内部会 tokio::spawn 几个后台维护任务

  • Min connections 补齐——定期检查当前连接数是否 < min_connections、补齐。
  • Idle timeout 驱逐——扫描 idle queue 里每条连接的空闲时长、超过 idle_timeout 的 close。
  • Max lifetime 驱逐——连接建立时间超过 max_lifetime 的强制退休。

这些后台任务在 PoolOptions::connect 内部 spawn——用户看不到但一直在跑。用户代码只感受到"Pool 自己会维护连接"。

任务的生命周期

  • Pool 启动时 spawn。
  • Pool close 时任务收到 close signal、清理完退出。
  • Pool 自己 drop(最后一个 Arc handle 释放)时任务也退出。

这条隐式依赖意味着 Pool 需要一个活着的 Tokio runtime——如果 Pool 在 #[tokio::main] 外构造(或 runtime 已经 shutdown),后台任务无法正常运行。生产代码里 Pool 几乎总是在 runtime 内构造——这条约束不显眼但真实存在。

第 14 章会详细讲这些维护任务的具体实现——spawn_maintenance_tasks 函数如何组织它们、哪些用 tokio::time::interval、哪些用 event_listener 等事件驱动。

13.24 Pool 和 transaction 的组合

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

rust
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 本章核心要义

再精炼成三句话:

  1. Pool 的外部 API 表面简单——几个方法(connect / acquire / begin / close)、一堆配置。但默认配置不能直接生产——至少改 max_connections。
  2. Pool 是 sqlx 在生产里的门面——业务代码几乎只接触 &pool。生产事故的 90% 和 Pool 配置或监控有关。
  3. Pool 通过 Arc 包裹让共享变便宜——这是 sqlx 整个**"handler 持有 pool + 多线程并发"** 模式的物理基础。

下一章我们撕开 Pool 的内部——看 idle queue / Semaphore / maintenance task 这些把"一个 handle、内部多状态"实现起来的机制。

13.26 本章讨论过的实战案例回顾

为了方便查询,把本章讨论的实战案例列一下:

  • §13.10.1 生产推荐配置——max_connections / acquire_timeout 等典型设置。
  • §13.11 三个回调——after_connect 设 session 参数、before_acquire 精细 ping、after_release DISCARD TEMP。
  • §13.14 max_connections 场景表——从开发到高流量 web 的具体数值。
  • §13.15 PgBouncer 叠加——多 pod 场景怎么避免 DB 连接撞上限。
  • §13.16 Axum 启动代码——完整的 Pool + metrics + shutdown 模板。
  • §13.17 五类故障症状——PoolTimedOut、too many connections、连接泄漏等排查路径。
  • §13.20 配置全景表——13 项按类别分组、标注必改/可选。
  • §13.21 三家 DB 的 Pool 行为差异——Postgres/MySQL/SQLite 的推荐值不同。
  • §13.24 Pool + Transaction 组合——transfer 示例。

每个案例都是生产里常见的场景——收藏本章作为 sqlx Pool 生产使用的 reference 页。

13.27 从 Pool 到下一站

本章只讲 Pool 的外部 API——用户能调、能配的那些。第 14 章讲内部实现——idle queue 的 ArrayQueue 选择、AsyncSemaphore 的 fair 语义、maintenance task 的调度、PoolConnection 的 Drop 实现。

这两章的关系像**"用户手册 vs 机械工程图"**——用户手册告诉你按按钮会怎样(本章)、机械图告诉你按钮怎么工作(下章)。大多数用户只需要用户手册——但能工程师读懂机械图才能修机器。第 14 章面向想深入理解 sqlx Pool 内部机制的读者——如果你的业务在 Pool 这层遇到奇怪的性能问题、或者你想给 sqlx 贡献代码,那章就是必读的。

作为本章收尾:sqlx 的 Pool 不是一个"库提供的便利"——它是 Rust async 数据库编程的基础设施。每个使用 sqlx 的生产 Rust 服务都依赖 Pool 做连接分配。理解它、配置好它、监控它——决定你的服务在数据库层的生产质量

13.28 Pool API 演进的三次关键改进

回顾 Pool 从 0.1 到 0.8 的几次关键改进:

  • 0.2:首次有 Pool(0.1 只有直接 Connection)。基本的 acquire / close / max_connections。
  • 0.4:引入 PoolOptions 取代之前的零散方法;test_before_acquire / idle_timeout / max_lifetime 加入。
  • 0.5after_connect / before_acquire / after_release 三个回调一起加入——让用户能精细定制 Pool 行为。
  • 0.6Transaction<'static> 改造——之前 begin 返回的 Transaction 借用 pool 借用很难用、0.6 改成 'static 让 spawn 友好。
  • 0.7:GAT 简化内部类型;connect_lazy 正式化。
  • 0.8(本书版本):close_event + do_until 优雅 shutdown 的原语补全;acquire_slow_level 慢 acquire 日志。

三次关键改进决定了今天 Pool 的形态:

  1. 0.4 的 PoolOptions —— 把配置集中成一个 struct、链式 API——现代 Pool 配置的基础形态。
  2. 0.5 的三回调——让 Pool 从"固定行为"变成"可扩展行为"——高级用户能完全定制。
  3. 0.6 的 'static Transaction——让 begin 的返回值真正能跨 spawn——解锁了很多并发场景。

每次改进都是实际用户遇到的痛点触发的——PoolOptions 之前用户抱怨配置零散、三回调之前用户想定制无从下手、'static 之前 spawn 里 begin 的生命周期是惊恐。这种"痛点驱动"的演进和第 9/10 章讨论的 API 演进路径如出一辙。

sqlx 作为库的一个核心品质是:它在回应真实用户需求——不是作者在象牙塔里设计完美 API、而是 N 次版本迭代里每次都响应社区反馈。这也是 sqlx 从 0.1 到 0.8 保持社区热度的原因——用户觉得它在听见我

13.29 pool 作为一个教学案例的价值

从"学习 Rust"角度看,sqlx Pool 是一个非常好的教学样本。它展示了 Rust 里"管理一组资源"应该怎么做:

  • Arc + Inner 的服务对象模式(§13.2)。
  • Eager/Lazy 构造的 API 对偶(§13.3)。
  • 阻塞 vs 非阻塞方法对(acquire / try_acquire)。
  • 优雅 shutdown 的 close_event 设计(§13.7)。
  • 配置 + 回调分离(§13.10, §13.11)。
  • 生命周期延展Transaction<'static>,§13.6)。

这些模式在 Rust 生态其他地方反复出现——HTTP client 的 connection pool、Redis 库、grpc 的 channel——掌握 sqlx Pool 的设计就等于掌握这类资源管理库的通用模式

如果你是在学 Rust 的资源管理、想看一个实现完备的 open-source 范例——sqlx 的 pool/ 模块(约 1500 行)值得完整读一遍。比读任何 "Rust 设计模式" 书都更能学到实战感。

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

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

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

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

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

这三条规则覆盖了 80% 的 sqlx Pool 生产使用场景——按它们写的代码就是"生产级的 sqlx 代码"。

这一章到这里结束。下一章第 14 章进入 Pool 的内部实现——把 Pool 的黑盒彻底拆给你看,让你对每一个性能/行为 quirk 都能定位到具体源码。

基于 VitePress 构建