获取连接
主线位置:连接池主线的前半段。
fetch_one(&pool)里&Pool的Executor实现第一句就是pool.acquire().await?。本页从Pool::acquire走到交出一个PoolConnection:拿许可、弹空闲连接并检查、或者加计数新建连接,以及这一切外面的那个超时。连接用完之后怎么回到池里,在归还、Drop 与维护任务。
flowchart TB
start["Pool::acquire"] --> closed{池已关闭}
closed -->|是| pc[PoolClosed]
closed -->|否| permit["排队拿许可"]
permit --> pop{"队列有空闲"}
pop -->|有| chk["检查空闲连接"]
chk -->|通过| done["交出连接"]
chk -->|不通过| conn
pop -->|没有| inc{"能加计数"}
inc -->|成功| conn["connect 带退避"]
inc -->|满了| yld["yield_now 再来"]
yld --> permit
conn --> done
permit -.->|超时| pto[PoolTimedOut]
入口:整个循环包在一个超时里
Pool::acquire 克隆一份 Arc<PoolInner>,所以返回的 future 是 'static 的,可以直接交给 tokio::spawn:
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()) }
}
PoolInner::acquire 是本页的主体:
pub(super) async fn acquire(self: &Arc<Self>) -> Result<Floating<DB, Live<DB>>, Error> {
if self.is_closed() {
return Err(Error::PoolClosed);
}
let acquire_started_at = Instant::now();
let deadline = acquire_started_at + self.options.acquire_timeout;
let acquired = crate::rt::timeout(
self.options.acquire_timeout,
async {
loop {
// Handles the close-event internally
let permit = self.acquire_permit().await?;
// First attempt to pop a connection from the idle queue.
let guard = match self.pop_idle(permit) {
// Then, check that we can use it...
Ok(conn) => match check_idle_conn(conn, &self.options).await {
// All good!
Ok(live) => return Ok(live),
// if the connection isn't usable for one reason or another,
// we get the `DecrementSizeGuard` back to open a new one
Err(guard) => guard,
},
Err(permit) => if let Ok(guard) = self.try_increment_size(permit) {
// we can open a new connection
guard
} else {
// ...
tracing::debug!("woke but was unable to acquire idle connection or open new one; retrying");
// ...
crate::rt::yield_now().await;
continue;
}
};
// Attempt to connect...
return self.connect(deadline, guard).await;
}
}
)
.await
.map_err(|_| Error::PoolTimedOut)??;
// ... 慢获取的日志,见下文
Ok(acquired)
}
返回值的类型说明了这一页的几个角色:
- 许可(
AsyncSemaphoreReleaser):信号量里的一个名额,总数是max_connections。拿到许可才有资格碰空闲队列或者开新连接。 DecrementSizeGuard:许可「转正」之后的样子,代表池里的一条连接(已有的或将要建的)。它被 drop 时把size减一并归还许可。Floating<DB, C>:一条连接C加上它的 guard,是连接在「池」和「用户」之间转手时的形态。acquire交出Floating<DB, Live<DB>>,Pool::acquire用reattach把它变成PoolConnection:调guard.cancel(),许可和计数从此由PoolConnection隐式持有,直到它被 drop。
rt::timeout 在 tokio 下就是 tokio::time::timeout(实现见 tokio 卷 Sleep 的一生的 timeout 一节)。超时时内层 future 被 drop:排在信号量里的等待者离开队列,正在进行的 connect 被取消,它手里的 guard 随之 drop,计数减回去。
拿许可:acquire_permit
acquire_permit 对普通的池只有一行有用的:
close_event.do_until(acquire_self).await
acquire_self 是 self.semaphore.acquire(1),close_event 是池的关闭事件。CloseEvent::do_until 同时等两件事,池先被关闭就返回 Err(Error::PoolClosed):所以 Pool::close 一调用,所有正在排队的 acquire 立刻报 PoolClosed,不用等到超时。
信号量是 AsyncSemaphore,开了 _rt-tokio 时它的字段就是一个 tokio::sync::Semaphore,acquire(1) 调 acquire_many(1)。许可的计数、等待链表、释放时按 FIFO 分给最早的等待者,都是 tokio 的实现,见 tokio 卷的 batch_semaphore。sqlx 在这上面只加了一个约定:许可数 = 「可以借出或新建」的名额。空闲队列里的连接不占许可,借出的连接和正在建的连接各占一个。
先到先得
max_connections(1) 的池,先占住唯一的连接,再起 5 个任务依次排队(每起一个等 10 毫秒,保证它们按顺序进入信号量的队列),然后放手;每个任务拿到后占 20 毫秒(src/bin/fair.rs):
let held = pool.acquire().await?;
let mut tasks = Vec::new();
for i in 0..5 {
let (pool, order, clock) = (pool.clone(), order.clone(), clock.clone());
tasks.push(tokio::spawn(async move {
let conn = pool.acquire().await.unwrap();
println!("{:>4}ms waiter {i} got it", clock.ms());
order.lock().unwrap().push(i);
tokio::time::sleep(Duration::from_millis(20)).await;
drop(conn);
}));
// let each waiter reach the semaphore before the next one starts
tokio::time::sleep(Duration::from_millis(10)).await;
}
println!("{:>4}ms try_acquire while 5 are queued: {:?}", clock.ms(), pool.try_acquire().map(|_| ()));
drop(held);
println!("{:>4}ms holder dropped", clock.ms());
// a latecomer arriving right after the release
let late = {
let (pool, clock) = (pool.clone(), clock.clone());
tokio::spawn(async move {
let _conn = pool.acquire().await.unwrap();
println!("{:>4}ms latecomer got it", clock.ms());
})
};
59ms try_acquire while 5 are queued: None
59ms holder dropped
60ms waiter 0 got it
83ms waiter 1 got it
106ms waiter 2 got it
128ms waiter 3 got it
151ms waiter 4 got it
174ms latecomer got it
order: [0, 1, 2, 3, 4]
顺序就是排队的顺序,放手之后才来的任务排在最后。每次交接比 20 毫秒多出 2 到 3 毫秒:上一个任务 drop 连接后,归还任务要先 ping 一个来回、把连接推回队列,才释放许可,下一个等待者才醒(见归还)。fair 选项在 tokio 下改变不了这一点,见 PoolOptions。
Pool::try_acquire 走 PoolInner::try_acquire:semaphore.try_acquire(1) 不排队,有人在排队时 tokio 信号量的计数是 0,所以它拿不到,上面输出的第一行就是这样。它只从空闲队列取,不新建连接,也不经过 check_idle_conn,下文的抓包里能看到它不 ping。
子池:从父池偷许可
有 parent 的子池(#[sqlx::test] 用它)自己的信号量从 0 个许可开始。acquire_permit 在子池还没满(size < max_connections)时同时等两个信号量:
future::poll_fn(|cx| {
if close_event.as_mut().poll(cx).is_ready() {
return Poll::Ready(Err(Error::PoolClosed));
}
if parent_close_event.as_mut().poll(cx).is_ready() {
// Propagate the parent's close event to the child.
self.mark_closed();
return Poll::Ready(Err(Error::PoolClosed));
}
if let Poll::Ready(permit) = acquire_self.as_mut().poll(cx) {
return Poll::Ready(Ok(permit));
}
// Don't try the parent right away.
if poll_parent {
acquire_parent.as_mut().poll(cx).map(Ok)
} else {
poll_parent = true;
cx.waker().wake_by_ref();
Poll::Pending
}
})
第一次 poll 只看自己的信号量,然后叫醒自己、返回 Pending,第二次 poll 起才去父池拿。这样子池里刚归还的许可有机会先被用上,不必每次都从父池偷。父池关闭会传染给子池。
弹出空闲连接并检查
拿到许可后先试空闲队列。pop_idle:
if let Some(idle) = self.idle_conns.pop() {
self.num_idle.fetch_sub(1, Ordering::AcqRel);
Ok(Floating::from_idle(idle, (*self).clone(), permit))
} else {
Err(permit)
}
idle_conns 是 crossbeam 的 ArrayQueue,无锁、容量固定为 max_connections,先进先出,所以最早归还的连接最先被借出。num_idle 是另外维护的原子计数,注释说 ArrayQueue::len() 要等头尾指针停止变化才能算出长度,高并发时可能很慢。from_idle 里 DecrementSizeGuard::from_permit 调 permit.disarm(),在 tokio 下就是 SemaphorePermit::forget():许可不再随 SemaphorePermit 的 drop 自动归还,改由 guard 负责。
check_idle_conn 只做两件事:
if options.test_before_acquire {
// Check that the connection is still live
if let Err(error) = conn.ping().await {
// an error here means the other end has hung up or we lost connectivity
// either way we're fine to just discard the connection
// the error itself here isn't necessarily unexpected so WARN is too strong
tracing::info!(%error, "ping on idle connection returned error");
// connection is broken so don't try to close nicely
return Err(conn.close_hard().await);
}
}
if let Some(test) = &options.before_acquire {
// ...
}
// No need to re-connect; connection is alive or we don't care
Ok(conn.into_live())
这里不查 max_lifetime。 一条超龄的连接照样可以被借出,要等它归还时或维护任务来关(归还与维护里有实验)。idle_timeout 也不在这里查。
检查不通过时,函数把这条连接关掉,返回它的 DecrementSizeGuard。acquire 拿着这个 guard 直接去 connect:size 不变(坏连接的名额让给新连接),也不再从队列里试下一条空闲连接。before_acquire 的文档说会「tries again with another idle connection」,代码不是这样。两条空闲连接、before_acquire 拒绝第一条(src/bin/reject.rs):
let pool = PgPoolOptions::new()
.before_acquire(move |_conn, _meta| {
let flag = flag.clone();
Box::pin(async move { Ok(!flag.swap(false, Ordering::SeqCst)) })
})
.connect_with(lab::opts("pool-reject"))
.await?;
let mut obs = lab::observer().await;
let (a, b) = (pool.acquire().await?, pool.acquire().await?);
drop((a, b));
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
println!("before: size={} idle={} pids={:?}", pool.size(), pool.num_idle(), lab::backends(&mut obs, "pool-reject").await);
reject_next.store(true, Ordering::SeqCst);
let mut conn = pool.acquire().await?;
let pid: i32 = sqlx::query_scalar("select pg_backend_pid()").fetch_one(&mut *conn).await?;
println!("got pid={pid}: size={} idle={} pids={:?}", pool.size(), pool.num_idle(), lab::backends(&mut obs, "pool-reject").await);
lab::backends 查的是 pg_stat_activity 里 application_name 等于这个池的名字的后端(池的连接参数用 PgConnectOptions::application_name 设了名字):
before: size=2 idle=2 pids=[499, 501]
got pid=502: size=2 idle=1 pids=[501, 502]
499 被拒绝、关掉,队列里的 501 没被碰,借出的是新建的 502。
借出时的 ping
Postgres 的 ping 只发一个 Sync,等回一个 ReadyForQuery。把池接到打印协议消息的代理上(代理代码见一次查询的全过程),借一条连接、查一次、还回去,test_before_acquire 分别为 true 和 false,中间再试一次 try_acquire(src/bin/pings.rs):
for test in [true, false] {
p.quiet.store(true, Ordering::SeqCst);
let pool = PgPoolOptions::new()
.max_connections(1)
.test_before_acquire(test)
.connect_with(lab::via_proxy("pool-pings", p.port))
.await?;
tokio::time::sleep(Duration::from_millis(100)).await;
p.quiet.store(false, Ordering::SeqCst);
println!("--- test_before_acquire({test}) ---");
let conn = pool.acquire().await?;
println!("acquired: size={} num_idle={}", pool.size(), pool.num_idle());
let mut conn = conn;
let one: i32 = sqlx::query_scalar("select 1").fetch_one(&mut *conn).await?;
println!("query -> {one}");
drop(conn);
println!("dropped: size={} num_idle={}", pool.size(), pool.num_idle());
tokio::time::sleep(Duration::from_millis(100)).await;
println!("later: size={} num_idle={}", pool.size(), pool.num_idle());
if test {
println!("--- try_acquire (test_before_acquire(true)) ---");
let conn = pool.try_acquire();
println!("try_acquire -> {:?}", conn.as_ref().map(|_| "PoolConnection"));
println!("again while held -> {:?}", pool.try_acquire().map(|_| "PoolConnection"));
drop(conn);
tokio::time::sleep(Duration::from_millis(100)).await;
}
p.quiet.store(true, Ordering::SeqCst);
pool.close().await;
}
--- test_before_acquire(true) ---
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
acquired: size=1 num_idle=0
[conn 1] C->S Parse(sqlx_s_1, "select 1"), Describe(S, sqlx_s_1), Sync
[conn 1] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
[conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
[conn 1] S->C BindComplete, DataRow(列长 4), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
query -> 1
dropped: size=1 num_idle=0
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
later: size=1 num_idle=1
--- try_acquire (test_before_acquire(true)) ---
try_acquire -> Some("PoolConnection")
again while held -> None
[conn 1] C->S Sync
[conn 1] S->C ReadyForQuery(I)
--- test_before_acquire(false) ---
acquired: size=1 num_idle=0
[conn 2] C->S Parse(sqlx_s_1, "select 1"), Describe(S, sqlx_s_1), Sync
[conn 2] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
[conn 2] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
[conn 2] S->C BindComplete, DataRow(列长 4), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
query -> 1
dropped: size=1 num_idle=0
[conn 2] C->S Sync
[conn 2] S->C ReadyForQuery(I)
later: size=1 num_idle=1
true时acquire之前有一对Sync/ReadyForQuery,false时没有。- drop 之后的那一对两种情况都有,那是归还时无条件的 ping。
dropped:那行num_idle=0、later:那行才是 1:归还是后台任务做的。 try_acquire借出时没有 ping,只有归还时那一对;连接被借走后再try_acquire得到None,它不会新建连接。
借出时的 ping 保证交出去的连接此刻还活着(服务端没有因为空闲超时、重启、网络中断把它断掉),代价是每次借出多一个来回。关掉它,坏连接要到第一条查询才暴露,那时 acquire 已经返回,不会再替你换一条。
新建连接:try_increment_size 和 DecrementSizeGuard
队列是空的,就试着把 size 加一。try_increment_size:
let result = self
.size
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |size| {
if self.is_closed() {
return None;
}
size.checked_add(1)
.filter(|size| size <= &self.options.max_connections)
});
match result {
// we successfully incremented the size
Ok(_) => Ok(DecrementSizeGuard::from_permit((*self).clone(), permit)),
// the pool is at max capacity or is closed
Err(_) => Err(permit),
}
size 先加、连接后建。加上去的那一刻起,guard 就代表这条还不存在的连接;connect 失败、超时被取消、或者任何一步出错让 guard 被 drop,DecrementSizeGuard 的 Drop 把 size 减一并释放许可:
impl<DB: Database> Drop for DecrementSizeGuard<DB> {
fn drop(&mut self) {
if !self.cancelled {
self.pool.size.fetch_sub(1, Ordering::AcqRel);
// and here we release the permit we got on construction
self.pool.semaphore.release(1);
}
}
}
计数和许可绑在同一个 RAII 对象上,所以没有哪条错误路径能让 size 和真实连接数对不上。pool.size() 包括正在建的连接,归还与维护里 connect_lazy 的实验能看到 size=1 而服务端一条连接都还没有的瞬间。
许可拿到了,却既没有空闲连接也不能新建
try_increment_size 失败时,acquire 打一条 debug 日志,yield_now 一下再回去排队。源码注释说这「can happen for a child pool that's at its connection limit, or if the pool was closed between acquire_permit() and try_increment_size()」,并说「we're likely in the current-thread runtime if it's Tokio, and so we should yield to let any spawned return_to_pool() tasks execute」。
普通的池里,许可数和 max_connections 相等,拿到许可就意味着「借出的 + 正在建的」还没到上限,队列空时 size 一定还能加,这条路径只在关闭的竞态里出现。子池不一样:它的 size 检查在等父池许可之前做,等的时候别的任务可能已经把子池填满了。父池 max_connections(2)、子池 max_connections(1),current_thread 运行时上两个任务同时向子池要连接(src/bin/current_thread.rs 的后半段):
let parent = PgPoolOptions::new()
.max_connections(2)
.connect_with(lab::opts("pool-ct"))
.await?;
let child = PgPoolOptions::new()
.max_connections(1)
.parent(parent.clone())
.connect_lazy_with(lab::opts("pool-ct"));
let (c1, c2) = (child.clone(), child.clone());
let a = tokio::spawn(async move { drop(c1.acquire().await.unwrap()); "a done" });
let b = tokio::spawn(async move { drop(c2.acquire().await.unwrap()); "b done" });
println!("{} / {}", a.await?, b.await?);
RUST_LOG=sqlx_core::pool=debug:
...
DEBUG sqlx_core::pool::inner: woke but was unable to acquire idle connection or open new one; retrying
a done / b done
...
两个任务都从父池偷到了许可;先到的那个把子池的 size 加到 1 去建连接,后到的加不上,走到这条日志,yield_now 后回到循环开头。这一次子池已满,acquire_permit 不再去父池,只等子池自己的信号量,等先到的那个归还后拿到。后到的那个许可在 try_increment_size 返回 Err(permit) 后被 drop,还给了父池。注意这条日志的 target 是模块路径 sqlx_core::pool::inner,不是慢获取日志用的 sqlx::pool::acquire:RUST_LOG=sqlx=debug 按前缀匹配能看到它,RUST_LOG=sqlx::pool=debug 看不到。
connect:重试和退避
PoolInner::connect 拿着 guard 和 acquire 算好的截止时间:
let mut backoff = Duration::from_millis(10);
let max_backoff = deadline_as_timeout(deadline)? / 5;
loop {
let timeout = deadline_as_timeout(deadline)?;
// clone the connect options arc so it can be used without holding the RwLockReadGuard
// across an async await point
let connect_options = self
.connect_options
.read()
.expect("write-lock holder panicked")
.clone();
// result here is `Result<Result<C, Error>, TimeoutError>`
// if this block does not return, sleep for the backoff timeout and try again
match crate::rt::timeout(timeout, connect_options.connect()).await {
// successfully established connection
Ok(Ok(mut raw)) => {
// ... 跑 after_connect,成功就 return Ok(Floating::new_live(raw, guard)),
// 出错就 close_hard 然后落到下面的退避
}
// an IO error while connecting is assumed to be the system starting up
Ok(Err(Error::Io(e))) if e.kind() == std::io::ErrorKind::ConnectionRefused => (),
// We got a transient database error, retry.
Ok(Err(Error::Database(error))) if error.is_transient_in_connect_phase() => (),
// Any other error while connection should immediately
// terminate and bubble the error up
Ok(Err(e)) => return Err(e),
// timed out
Err(_) => return Err(Error::PoolTimedOut),
}
// If the connection is refused, wait in exponentially
// increasing steps for the server to come up,
// capped by a factor of the remaining time until the deadline
crate::rt::sleep(backoff).await;
backoff = cmp::min(backoff * 2, max_backoff);
}
| 建连接的结果 | 处理 |
|---|---|
成功,after_connect 也成功 | 返回 |
after_connect 出错 | error! 日志,close_hard,退避后重试 |
I/O 错误且 kind() == ConnectionRefused | 退避后重试(注释:「assumed to be the system starting up」) |
数据库错误且 is_transient_in_connect_phase | 退避后重试;Postgres 认 53300(too_many_connections)和 57P03(cannot_connect_now,数据库还在启动) |
| 其他错误(认证失败、TLS、DNS、别的 I/O 错误) | 立刻返回这个错误 |
| 截止时间到了 | PoolTimedOut |
退避从 10 毫秒开始翻倍,上限是进入 connect 时剩余时间的五分之一。deadline_as_timeout 在截止时间已过时直接返回 PoolTimedOut。
用三种对端试(src/bin/connect_fail.rs):本机 1 号端口没人监听,内核直接回 RST;一个假的服务端读完 StartupMessage 就回一条 ErrorResponse,错误码分别是 57P03 和 28P01(密码错误),并记下每次连接到来的时刻。池的 acquire_timeout 是 1 秒:
async fn try_pool(name: &str, port: u16) {
let started = Instant::now();
let res = PgPoolOptions::new()
.acquire_timeout(Duration::from_secs(1))
.connect_with(opts_at(port))
.await;
println!("{name}: after {:?}: {}", started.elapsed(), res.err().map(|e| e.to_string()).unwrap_or_default());
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// port 1 on loopback: nothing listens, the kernel answers RST -> ConnectionRefused
try_pool("closed port", 1).await;
let t0 = Instant::now();
let (port, hits) = fake_server("57P03", "the database system is starting up", t0).await;
try_pool("57P03", port).await;
println!(" attempts at ms: {:?}", hits.lock().unwrap());
let t0 = Instant::now();
let (port, hits) = fake_server("28P01", "password authentication failed for user \"lab\"", t0).await;
try_pool("28P01", port).await;
println!(" attempts at ms: {:?}", hits.lock().unwrap());
Ok(())
}
closed port: after 1.005184208s: pool timed out while waiting for an open connection
...
28P01: after 425.542µs: error returned from database: password authentication failed for user "lab"
attempts at ms: [0]
57P03 那两行留给下面的动脑筋。
acquire_timeout 是 1 秒,服务端每次都回 57P03。在报 PoolTimedOut 之前,假服务端一共收到几次连接?平台:M2 Max、macOS、rustc 1.99.0、Postgres 16.14(假服务端在本机)。答案在页末。另外两行对照上面的表:关闭的端口每次都是 ConnectionRefused,一直重试到截止时间,最后报的是 PoolTimedOut;28P01 不在可重试的名单里,第一次就原样返回,前后不到半毫秒。
问: 端口根本没开,connect 却等满 1 秒才报 pool timed out while waiting for an open connection。为什么不直接告诉我「连接被拒绝」?
答: 因为 ConnectionRefused 被当成数据库还没起来(注释原话是「assumed to be the system starting up」):与其让应用在数据库启动前的那几秒里直接失败,不如在 acquire_timeout 之内一直重试。代价是重试期间的错误被丢掉了,超时后只剩 PoolTimedOut,和「池满了等不到连接」报的是同一个错误。排查时先看是不是 size() 已经到了 max_connections:没到,多半是连不上;到了,是连接被占着。认证失败这类不会自愈的错误不重试,会立刻原样报出来。
超时:PoolTimedOut
acquire_timeout 包住的是整个循环:排队等许可、ping、before_acquire、建连接和 after_connect,全部加起来不超过它。最常见的超时是池被占满:max_connections(1),自己先占住那条连接,再用 &pool 查询(src/bin/timeout.rs):
let pool = PgPoolOptions::new()
.max_connections(1)
.acquire_timeout(Duration::from_millis(1500))
.connect_with(lab::opts("pool-timeout"))
.await?;
let held = pool.acquire().await?;
println!("holding: size={} num_idle={}", pool.size(), pool.num_idle());
for _ in 0..3 {
let started = Instant::now();
let res = sqlx::query_scalar::<_, i32>("select 1").fetch_one(&pool).await;
let err = res.unwrap_err();
println!("after {:?}: {err} / {err:?}", started.elapsed());
}
drop(held);
let started = Instant::now();
let one: i32 = sqlx::query_scalar("select 1").fetch_one(&pool).await?;
println!("released, query -> {one} after {:?}", started.elapsed());
holding: size=1 num_idle=0
after 1.500789042s: pool timed out while waiting for an open connection / PoolTimedOut
after 1.501650375s: pool timed out while waiting for an open connection / PoolTimedOut
after 1.501728792s: pool timed out while waiting for an open connection / PoolTimedOut
released, query -> 1 after 3.001542ms
每次都是 1.5 秒加不到 2 毫秒,就是 tokio::time::timeout 的精度。这是「持有连接时再 acquire」的最小形式:同一个任务里先 pool.acquire() 或 pool.begin() 拿着一条,再把 &pool 当执行器用,池小于并发需要时就会卡到超时。错误本身的类型见错误。
慢获取的日志
acquire 成功后看一眼用了多久:
let acquired_after = acquire_started_at.elapsed();
let acquire_slow_level = self
.acquire_slow_level
.filter(|_| acquired_after > self.options.acquire_slow_threshold);
if let Some(level) = acquire_slow_level {
private_tracing_dynamic_event!(
target: "sqlx::pool::acquire",
level,
acquired_after_secs = acquired_after.as_secs_f64(),
slow_acquire_threshold_secs = self.options.acquire_slow_threshold.as_secs_f64(),
"acquired connection, but time to acquire exceeded slow threshold"
);
} else if let Some(level) = self.acquire_time_level {
private_tracing_dynamic_event!(
target: "sqlx::pool::acquire",
level,
acquired_after_secs = acquired_after.as_secs_f64(),
"acquired connection"
);
}
超过 acquire_slow_threshold(默认 2 秒)用 acquire_slow_level(默认 Warn),否则用 acquire_time_level(默认 Off,不打)。两个级别在 new_arc 里从 log::LevelFilter 换成 Option<tracing::Level>,Off 就是 None。超时的那次 acquire 已经在 ? 处返回了错误,不打这条日志。
把阈值调到 200 毫秒,让另一个任务占住连接 500 毫秒(src/bin/slow.rs):
let pool = PgPoolOptions::new()
.max_connections(1)
.acquire_slow_threshold(Duration::from_millis(200))
.connect_with(lab::opts("pool-slow"))
.await?;
println!("--- fast acquire ---");
drop(pool.acquire().await?);
tokio::time::sleep(Duration::from_millis(100)).await;
println!("--- someone holds the connection for 500ms ---");
let held = pool.acquire().await?;
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(500)).await;
drop(held);
});
drop(pool.acquire().await?);
RUST_LOG=sqlx=debug:
--- fast acquire ---
--- someone holds the connection for 500ms ---
WARN sqlx::pool::acquire: acquired connection, but time to acquire exceeded slow threshold acquired_after_secs=0.503702334 slow_acquire_threshold_secs=0.2
快的那次什么都不打。acquired_after_secs 里包括了排队、ping 和可能的建连接,单看它分不出是哪一段慢;配合 pool.size() 和 pool.num_idle() 看:size 等于 max_connections 且 num_idle 为 0,是连接都被借走了。
aquired_after_secs慢获取日志是 #3073(0.8.0)加的,字段名少了一个 c:0.8.6 的 inner.rs 里两处都写着 aquired_after_secs。#3486 改成 acquired_after_secs,首次发布于 0.9.0。PR 说明提到改名的顾虑:「Some users of the crate were interested in collecting metrics or building alerts based on this and related tracing fields. If we simply rename the field, this would be a breaking change.」最后还是直接改了,CHANGELOG 把它列在 0.9.0 的 Breaking 一节:「Breaking behavior change: implementations parsing tracing logs from SQLx will need to update the spelling.」按字段名做告警的,升级到 0.9 要跟着改。
动脑筋答案
connect_fail 的完整输出:
closed port: after 1.005184208s: pool timed out while waiting for an open connection
57P03: after 1.003278709s: pool timed out while waiting for an open connection
attempts at ms: [0, 14, 38, 82, 165, 329, 532, 736, 939]
28P01: after 425.542µs: error returned from database: password authentication failed for user "lab"
attempts at ms: [0]
9 次。按源码算:进入 connect 时剩余约 1 秒,max_backoff 约 200 毫秒;退避依次是 10、20、40、80、160,然后一直是 200,于是理论上的尝试时刻是 0、10、30、70、150、310、510、710、910 毫秒,下一次在 1110 毫秒,已经超过截止时间。实测每次多出几毫秒(每次建 TCP 连接、读写一条消息的时间累积起来),第 9 次在 939 毫秒。第 10 次之前的那次 sleep 被外层的 rt::timeout 打断,报 PoolTimedOut,服务端回的 cannot_connect_now 不会出现在错误里。
小结
acquire的全部工作包在一个acquire_timeout里:排队、ping、before_acquire、建连接、after_connect。超时一律是PoolTimedOut,池关闭时排队者立刻得到PoolClosed。- 许可数等于
max_connections,借出和正在建的连接各占一个,空闲连接不占。tokio 的信号量保证先到先得;try_acquire有人排队时拿不到,也不新建连接、不 ping。 - 空闲连接先进先出地弹出,借出前只做 ping(
test_before_acquire)和before_acquire,不查max_lifetime和idle_timeout;检查不过就关掉,用同一个名额新建,不试下一条空闲连接。 size先加后连,DecrementSizeGuard保证任何失败路径都把计数和许可还回去。- 连接被拒和
53300/57P03会从 10 毫秒起指数退避重试,上限是剩余时间的五分之一;其他错误立刻返回。连不上库最后看到的只是PoolTimedOut。 - 慢于
acquire_slow_threshold(2 秒)打一条WARN,target 是sqlx::pool::acquire,字段acquired_after_secs在 0.9 之前拼错了。