sorry, too many clients already
主线位置:连接池主线里
acquire没有空闲连接、去connect新建的那一步。connect的重试和退避见获取连接;本页讲服务端连接数用满时你实际看到什么,以及池的上限怎么定。
flowchart TB
acq["acquire 要新建"] --> conn["connect"]
conn --> srv{"服务端还有槽位"}
srv -->|有| ok["拿到连接"]
srv -->|"没有 53300"| retry["退避后重试"]
retry --> dl{"到 acquire_timeout"}
dl -->|没到| conn
dl -->|到了| pto["PoolTimedOut"]
本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,依赖 sqlx = "=0.9.0"。服务端是一个单独的实验容器,启动参数 max_connections=8,只给这一页用。
现象
Postgres 的连接数到了 max_connections,新连接在启动阶段被拒,错误码 53300(too_many_connections):
sorry, too many clients already
直接 PgConnection::connect 会立刻拿到这个错误。经连接池时却看不到它:acquire 卡满 acquire_timeout,然后报 pool timed out while waiting for an open connection,看起来像是池太小或者连接泄漏了。
最小复现
src/bin/too_many.rs:两个池 A、B,各 max_connections(5)、acquire_timeout(2s),加起来 10,超过服务端的 8。A 先并发借满 5 条并一直拿着,B 再并发借 5 条;然后直连一条;最后 A 关掉一条,B 再借:
let a = PgPoolOptions::new().max_connections(5).acquire_timeout(Duration::from_secs(2)).connect_lazy_with(opts("pit3-a")?);
let b = PgPoolOptions::new().max_connections(5).acquire_timeout(Duration::from_secs(2)).connect_lazy_with(b_opts);
let held_a = grab("A", &a, 5).await;
let held_b = grab("B", &b, 5).await;
match PgConnection::connect_with(&opts("pit3-direct")?).await {
// 打印 Database(code, message) 或其他错误
}
grab 用 join_all 同时发出 n 个 acquire,统计成功和失败;client backend 是 pg_stat_activity 里客户端后端的个数,含一条观察用的连接:
max_connections=8 superuser_reserved_connections=3 reserved_connections=0 lab 是超级用户: true
开始时 client backend = 1(含观察连接)
--- 两个池,max_connections 5 + 5 ---
A: 借 5 条,拿到 5,用时 0.1s
client backend = 6
B: 借 5 条,拿到 2,用时 2.0s
B: 3 条失败: pool timed out while waiting for an open connection
client backend = 8,B.size() = 2
--- 这时直接连一条 ---
直连: Database(Some("53300"), sorry, too many clients already)
--- A 还回一条之后,B 再借 ---
B 拿到一条,用时 0.03s
B 抢到了最后两个槽位,另外三个 acquire 各等满 2 秒,报 PoolTimedOut,没有一处提到 53300。同一时刻直连,错误原文立刻就有。A 关掉一条(close_on_drop)之后,B 下一次 acquire 几十毫秒就拿到了。
B 的连接经过一个打印协议消息的代理(TRACE=1)。代理记下 B 在这 2 秒里一共发起了 32 次连接,每次都发了 StartupMessage,其中 30 次收到的是 ErrorResponse:
[conn 1] C->S StartupMessage(v3.0)
[conn 4] C->S StartupMessage(v3.0)
[conn 2] C->S StartupMessage(v3.0)
[conn 3] C->S StartupMessage(v3.0)
[conn 5] C->S StartupMessage(v3.0)
[conn 2] S->C ErrorResponse
[conn 4] S->C Authentication
[conn 1] S->C Authentication
[conn 5] S->C ErrorResponse
...
原因
PoolInner::connect 在截止时间之内循环建连接,遇到 is_transient_in_connect_phase 为真的数据库错误就退避重试,Postgres 认的是 53300 和 57P03。退避从 10 毫秒翻倍,上限是剩余时间的五分之一,所以 2 秒里每个等待者大约试 10 次,三个等待者合计 30 次,正好对上代理的计数。截止时间一到,循环返回的是 Error::PoolTimedOut,最后一次的 53300 被丢掉。PoolTimedOut 是没有字段的变体,带不出原因。
PoolOptions::connect_with 建池时也走 acquire,所以服务端已满时建池同样是等满 acquire_timeout 后报 PoolTimedOut(这一条是读源码得出的,本页没有单独跑)。
问: 服务端都明确说满了,为什么还要重试,而不是把 53300 直接交给我?
答: 源码注释给的理由是:This may be returned if we just un-gracefully closed a connection, give the database a chance to notice it and clean it up. 池刚刚 close_hard 掉一条连接时,服务端可能还没注意到,槽位还占着,稍等一下就会空出来。对这种短暂的满,重试是对的;对「几个池的上限加起来本来就超了」这种长期的满,重试只是把错误换成了超时。
修法:按服务端上限倒推池的上限
能给应用用的连接数是:
可用 = max_connections - superuser_reserved_connections - reserved_connections
后两项是给超级用户和 pg_use_reserved_connections 角色留的,普通角色用不到;本页实验的 lab 恰好是超级用户,所以 8 条都用上了(1 + 5 + 2)。生产里应用一般不是超级用户,Postgres 16 默认 max_connections=100、superuser_reserved_connections=3,可用是 97。
然后把所有会连这个库的东西加起来,保证不超过「可用」:
Σ(实例数 × 该实例上所有池的 max_connections) + 迁移、定时任务、psql、监控等 ≤ 可用
- 「实例数」按扩容的上限算,滚动发布时新旧实例会同时在线,也要算进去。
- 一个进程里有几个池(读写分开、
PgListener自己的池、测试里每个用例一个池)就加几份。 PoolConnection::detach出来的连接不再占池的计数,池会再开一条补上,见归还、Drop 与维护任务。
把实验里两个池都改成 3,加上观察连接一共 7 条,不超过 8:
--- 按上限算好:3 + 3 + 观察连接 1 ---
A: 借 3 条,拿到 3,用时 0.1s
B: 借 3 条,拿到 3,用时 0.1s
client backend = 7
实例数多到这样算下来每个池只剩一两条时,该在前面加连接池代理(pgbouncer 一类),而不是调大服务端的 max_connections:每条后端连接是一个进程,调大有内存代价。
怎么确认自己没中招
- 看到
PoolTimedOut,先在同一台机器上psql或直连一次:报sorry, too many clients already就是服务端满了,不是池的问题。 - 在库里看是谁占着连接。给每个池设
PgConnectOptions::application_name,就能按池分开数:
select application_name, usename, count(*)
from pg_stat_activity
where backend_type = 'client backend'
group by 1, 2
order by 3 desc;
- 用上面的式子算一遍最坏情况,留出余量。
小结
- 服务端连接数满了,直连报
53300 sorry, too many clients already;经 sqlx 连接池只报PoolTimedOut,因为connect把53300当成暂时错误,退避重试到acquire_timeout,最后的原因被丢掉。 - 两个
max_connections(5)的池连一个上限 8 的服务端,后来的池只拿到剩下的 2 条,其余acquire各等满超时;代理看到 2 秒内 32 次连接尝试。 - 池的上限按「实例数 × 每实例各池上限 + 其他客户端 ≤ max_connections − 保留连接」倒推;不够分时加连接池代理。