应用扛 200 RPS 稳稳的,流量一翻倍就开始报 FATAL: remaining connection slots are reserved for non-replication superuser connections 或 FATAL: sorry, too many clients already。Postgres 撞到了 max_connections。新请求要么失败,要么挂着等空 slot;已有请求拿到的连接其实是死的,也会卡。
最快的修法: 在 Postgres 前面放一个连接池(PgBouncer 走 transaction 模式,或者你云厂商的托管版),让少量真实后端连接服务成千上万个应用连接;然后把每个应用实例的池子调小,让 实例数 * 池大小 控制在 max_connections 的 70% 左右以内。后面的内容帮你定位到底是哪一种情况,并防止复发。
你是哪一种情况?
跑一次下面这条,结果基本就指向原因了:
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state
ORDER BY count(*) DESC;
| 你看到的现象 | 最可能的原因 | 跳到 |
|---|---|---|
总数接近 max_connections,大部分是 idle | 应用池子太大 / 没有连接池 | Step 1、Step 2 |
idle in transaction 很多 | 漏 commit/rollback(连接泄漏) | Step 3 |
几个 active 跑了好几分钟 | 长报表查询占着 slot | Step 4 |
| 很多短命连接,源端口随机 | Serverless 没接连接池 | Step 6 |
active 里读重查询多,副本几乎闲着 | 读流量都打到主库 | Step 5 |
常见原因
按踩坑频率排序。
1. 每个应用实例自己开大池子
10 个应用实例、每个池子 20,就是 200 连接。Postgres 默认 max_connections 是 100,所有实例同时起压就溢出。
怎么判断:对比 SELECT count(*) FROM pg_stat_activity 和 SHOW max_connections。接近 max 就是要溢出了。
2. 漏提交事务导致连接泄漏
代码开了事务、抛了异常、既没 commit 也没 rollback。连接永远停在 idle in transaction,把池子占住。
怎么判断:SELECT state, count(*) FROM pg_stat_activity GROUP BY state。idle in transaction 数量高就是泄漏。
3. 长查询占着连接
报表查询跑 5 分钟,整段时间都占着一个连接。20 个这种查询就把池子塞满。
怎么判断:SELECT pid, query_start, query FROM pg_stat_activity WHERE state = 'active' ORDER BY query_start LIMIT 10。跑超过 30 秒的就是长尾查询。
4. Postgres 前面没有连接池
应用池子直连 Postgres,每个应用实例都占着真实后端连接,没有任何复用。
怎么判断:pg_stat_activity 里连接来源(client_addr)是你的应用服务器,而不是连接池主机。
5. Serverless 函数每次调用都开新连接
Vercel / Lambda 没用支持池化的驱动时,每次冷启动开一条新连接。突发流量就变成突发新连接。
怎么判断:pg_stat_activity 里很多短命连接,来自不同的随机源端口。
6. 读流量都打到主库
所有读查询都走主库,但其实可以走副本,把主库的连接腾给写。
怎么判断:pg_stat_activity 里读重查询占大头,副本利用率几乎为零。
动手前先确认
- 确认具体症状字符串:是
too many clients还是remaining connection slots reserved。 - 记下当前
max_connections和实际占用数。 - 找最近一次可能改了池子大小或查询模式的应用发布。
- 看 Postgres 本体是否健康:CPU、内存、复制延迟。
- 计划一次安全变更:连接 drain 策略 + 一个短窗口,以防需要 terminate 后端。
需要收集的信息
SHOW max_connections、SHOW shared_buffers,以及 Postgres 类型(RDS / Aurora、Cloud SQL、Supabase、自建)。SELECT state, count(*) FROM pg_stat_activity GROUP BY state。- 应用端池子配置:池大小、idle 超时、最大寿命。
- 连接池配置(如果有):PgBouncer、Supavisor、RDS Proxy。
- 过去一小时的慢查询日志。
分步修复
Step 1:池子大小合理化
要满足的约束:应用实例数 * 每实例池大小 <= 0.7 * max_connections。留余量,因为 max_connections 会给超级用户和复制保留几个 slot,操作系统维护也要占一两个。
// pg Pool 配置(node-postgres)
import { Pool } from 'pg';
const pool = new Pool({
max: 10, // 每个应用实例
idleTimeoutMillis: 30000, // 30 秒后释放 idle 客户端
connectionTimeoutMillis: 2000, // 饱和时快速失败,而不是一直挂着
});
10 个应用实例 + max_connections = 100,池子设 7 就留出了余量。多数应用需要的连接远比想象中少:每个后端能高吞吐地顺序服务很多请求,所以只在确实有争用证据时才加大,别先验性地堆大。
Step 2:Postgres 前面放连接池
自建 PgBouncer:
# pgbouncer.ini
[databases]
mydb = host=postgres-primary port=5432 dbname=app
[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = scram-sha-256
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
reserve_pool_size = 5
server_idle_timeout = 600
max_prepared_statements = 200
应用连 PgBouncer 的 6432 端口。PgBouncer 持有 25 条真实 Postgres 连接,把最多 1000 个应用连接复用到这 25 条上。
transaction 模式是正确的默认值:每个事务结束后服务端连接就还回池子,这正是复用收益的来源。除非你确实需要 session 级状态(advisory locks、LISTEN/NOTIFY、跨语句的临时表),否则别用 session 模式——它会给每个客户端钉住一条真实连接,基本放弃了复用收益。
关于认证:截至 2026 年 6 月,auth_type = scram-sha-256 是推荐设置。旧的 md5 还能用,但各 Postgres 部署里正在逐步淘汰。
托管版(不用自己跑 PgBouncer):
- Supabase(Supavisor): 连 transaction 模式连接池走
6543端口;5432是 session 模式/直连路由。自 2025 年 2 月起,6543端口只服务 transaction 模式。 - AWS RDS / Aurora: 在实例前面开 RDS Proxy。注意连接 pinning——当客户端改了代理无法安全共享的 session 状态时,它会给该客户端钉住一条后端连接,复用率就掉下来。在 PostgreSQL 上,客户端一条
SET就足以触发 pinning。盯一下代理的DatabaseConnectionsCurrentlySessionPinnedCloudWatch 指标;如果偏高,就把那些SET的值挪到代理的 initialization query(每条新后端连接只执行一次)里,或者用ALTER ROLE/ALTER DATABASE放到数据库端,然后把应用代码里那条按 session 的SET删掉。 - Cloud SQL: 用 Cloud SQL Auth Proxy 配合应用端池子,或者旁挂一个 PgBouncer sidecar。
Step 3:清掉 idle-in-transaction 连接,并阻止新的产生
-- 看一下作恶的
SELECT pid, now() - state_change AS idle_for, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND state_change < now() - interval '5 minutes'
ORDER BY state_change;
-- 干掉它们
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND state_change < now() - interval '5 minutes';
然后在服务端加个保护,让泄漏能自愈:
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
SELECT pg_reload_conf();
terminate 后端只是争取时间。要从根上修泄漏:把每个事务包进 try/finally(或用 ORM 的托管事务辅助),保证连接无论成功还是走异常路径,都一定会 commit 或 rollback。
Step 4:加超时,让任何查询都不会永远占着 slot
-- 按数据库,限制单条语句
ALTER DATABASE app SET statement_timeout = '30s';
-- 给报表角色更长的额度
ALTER ROLE reporting SET statement_timeout = '5min';
应用代码里按会话来,在已知开销大的查询前设:
await pool.query("SET statement_timeout = '10s'");
在 Postgres 17 及以上(Postgres 18 自 2025 年 9 月起就是当前稳定大版本;截至 2026 年 6 月补丁线为 18.4)还可以考虑 transaction_timeout,它是 Postgres 17 引入的。它限制整个事务的总时长,不管里面单条语句多短,从而堵上了 statement_timeout 和 idle_in_transaction_session_timeout 留下的口子(一个由许多短语句加短停顿组成的事务,否则可以无限期地开着)。一个要注意的点:如果 transaction_timeout 设得小于等于 statement_timeout 或 idle_in_transaction_session_timeout,那两者里较长的那个会被忽略——所以把 transaction_timeout 当作最外层上限,其余两个设得更紧。
ALTER DATABASE app SET transaction_timeout = '2min';
Step 5:读分流到副本
import { Pool } from 'pg';
const primary = new Pool({ host: 'postgres-primary' });
const replica = new Pool({ host: 'postgres-replica' });
function getPool(query: string) {
if (/^\s*(SELECT|EXPLAIN)\b/i.test(query) && !/FOR UPDATE/i.test(query)) {
return replica;
}
return primary;
}
路由不明确时显式打标(比如要读自己刚写入那一行的 SELECT,应该留在主库,避免副本延迟读到旧数据)。把读移到副本,主库的 slot 就能腾给写。
Step 6:Serverless 用合适的驱动
Vercel / Lambda 每次调用都可能是全新运行时,别每次都开原生 Postgres 连接:
// 基于 HTTP 的驱动——不会每次调用都建一条常驻 TCP 连接
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL);
// 或 Supabase 客户端,它走 transaction 端口连连接池
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(url, key);
如果 serverless 里非用原生驱动不可,就把它指向连接池端点(Supavisor 的 transaction 端口 6543、RDS Proxy 或 PgBouncer),不要直连数据库,并且把每个函数的池子大小设成 1。
Step 7:监控和告警
-- 连接状态快照,每 30 秒跑一次
SELECT
count(*) FILTER (WHERE state = 'active') AS active,
count(*) FILTER (WHERE state = 'idle') AS idle,
count(*) FILTER (WHERE state = 'idle in transaction') AS idle_tx,
count(*) FILTER (WHERE wait_event_type = 'Lock') AS waiting
FROM pg_stat_activity;
总连接到 max_connections 的 80% 就告警,idle in transaction 超过 5 也告警。这两个都是早期预警,能在请求开始失败之前抓住问题。
怎么确认修好了
- 跑 2 倍峰值的压力测试;
pg_stat_activity计数应该稳在max_connections的 70% 以下。 - 在连接池侧确认高复用比:很多客户端连接由少得多的服务端连接来服务(PgBouncer 在 admin 控制台
SHOW POOLS;里看cl_active对sv_active)。 - 正常流量 10 分钟后,
idle in transaction数应该接近 0。 - 副本接到了相当份额的读流量(通过
pg_stat_statements验证)。
长期预防
- 所有应用实例的池子配置标准化,把
实例数 * 池大小的推导过程写在旁边。 - 把连接池(PgBouncer、Supavisor 或 RDS Proxy)作为生产的默认配置,而不是事后补丁。
- 在数据库层面就把
idle_in_transaction_session_timeout、statement_timeout以及(Postgres 17 及以上的)transaction_timeout设好。 - 上线第一天就准备只读副本,报表和分析全走它。
- 每月复盘连接池告警,把最常触发的收紧。
容易踩的坑
- 把
max_connections提到 500 来”解决”问题。实测一条 idle 后端连接大约耗 10 MB 内存(按共享内存折算的真实开销更低,但 ~10 MB 是个稳妥的估算),跑查询或临时表的活跃连接还会涨上去,这等于把连接上限换成了内存悬崖。超过约 200 连接,基本都该上连接池而不是加大上限。 - PgBouncer 用
session模式,其实transaction就够——白白放弃了大部分复用收益。 - 在 transaction 模式下开了 prepared statements,却把
max_prepared_statements留在默认的 0,结果悄悄禁用了 prepared statement 支持,报出prepared statement "..." does not exist这类错误。把它设成大于应用用到的不同 prepared statement 数量的值。 statement_timeout设得太激进,把合法的长迁移给打断了——把长额度限定给迁移角色就好。- 杀连接但不找泄漏源——还会复发。
FAQ
每个应用实例池子该设多少? 5 到 10 起步。多数应用需要的连接比想象中少,因为每个后端能高吞吐地顺序服务请求。只在确有争用证据(请求在排队等空连接)时才加大。
PgBouncer 跟 prepared statements 能配合吗? transaction 模式下 PgBouncer 1.21 及以上可以,但前提是把 max_prepared_statements 设成非零值。它默认是 0,会禁用 transaction/statement 池化下的 prepared statement 支持。设成大于应用用到的不同 prepared statement 数量的值,命中率最好。
serverless 要不要做连接池? 要。用 serverless 友好或基于 HTTP 的驱动(Neon、Supabase 客户端,或让原生驱动指向 RDS Proxy / Supavisor),并把每个函数的池子大小设成 1。每次调用都开原生池子,正是突发流量下把数据库打爆的元凶。
statement_timeout 和 transaction_timeout 有什么区别? statement_timeout 限制单条查询;transaction_timeout(Postgres 17 引入)限制整个事务,所以由许多短语句组成的长事务也会被终止。两者会相互影响:如果 transaction_timeout 设得小于等于 statement_timeout(或 idle_in_transaction_session_timeout),那个较长的会被忽略。把 transaction_timeout 当作最外层上限,按语句和 idle 的限制设得更紧。
加大 max_connections 需要重启吗? 需要。max_connections 不能热加载,改它要重启 Postgres——这也是为什么在加大上限之前更应该先上连接池。
相关阅读
外部参考:PostgreSQL pg_stat_activity 视图文档、客户端连接默认值(statement_timeout、idle_in_transaction_session_timeout、transaction_timeout 的定义都在这里)、PgBouncer 配置参考 和 RDS Proxy pinning 指南 是上面那些字段和设置项的权威来源。