连接数爆炸的典型症状与误判
最直接的报错是 `FATAL: sorry, too many clients already`,出现在应用日志里。但这只是一半——另一半是**连接泄漏**:应用侧不报错,数据库侧连接数却单调上升,直到撞墙。典型现场是发布后 10 分钟一切正常,30 分钟后开始大面积超时,重启服务后立刻恢复,然后又慢慢涨回去。
第二类症状是**资源被连接本身吃掉**。PostgreSQL 每个连接都是一个独立操作系统进程,拥有独立的内存空间(work_mem 的分配是按操作而非按连接,但会话状态、锁数组、查询上下文都按连接计)。当连接数达到几千时,即使没有任何查询在跑,进程调度和内存管理开销也会让整台机器变慢。
第三类症状是**锁与快照膨胀**。大量空闲连接中如果残留了未提交的事务,会持有锁并阻止 vacuum 回收,导致表膨胀。这也是「加了连接池反而更慢」的常见原因:池子本身没问题,问题在于池化模式选错,导致空闲连接停留在事务中间状态。
排查第一步永远是先看事实:`SHOW max_connections;` 看上限,`SELECT count(*) FROM pg_stat_activity;` 看当前值,再用 `pg_stat_activity` 按 `state` 和 `application_name` 分组,找出连接到底被谁占着。没有这组数据,任何「优化」都是猜。
SHOW max_connections;
SHOW superuser_reserved_connections;
SELECT application_name, state, count(*)
FROM pg_stat_activity
GROUP BY 1, 2 ORDER BY 3 DESC;
SELECT now() - xact_start AS xact_age, pid, state, left(query, 80)
FROM pg_stat_activity
WHERE xact_start IS NOT NULL AND now() - xact_start > interval '5 min'
ORDER BY xact_age DESC;三种池化模式对照:session / transaction / statement
**Session 模式**:客户端连接建立后,一直独占一个后端 PostgreSQL 连接,直到客户端断开才归还。这是「伪池化」,只解决了 TCP 握手开销,不解决连接数问题。10 个 pgbouncer 实例各 100 个客户端,仍然是 1000 个后端连接。
**Transaction 模式**(最常用):只在事务期间占用后端连接,`BEGIN` 时从池里取一个,`COMMIT`/`ROLLBACK` 后立刻归还。1000 个客户端配 50 个后端连接就足够了。这是绝大多数 Web 应用的正确选择,但它要求应用**不在事务外持有会话状态**。
**Statement 模式**:每条语句执行后立刻归还连接,是最激进的模式。它禁用了多语句事务、`LISTEN/NOTIFY`、`WITH HOLD` 游标、session 级 `SET` 以及 advisory lock,因此只适合纯粹的 OLTP 批处理或连接迁移场景。
**功能限制清单**(transaction 模式下需特别注意):transaction 模式下不能用 session 级 `SET`、不能用 `LISTEN`/`NOTIFY`、不能用带 `WITH HOLD` 的游标、`PREPARE`/`DEALLOCATE` 的生命周期不能跨越事务边界、advisory lock 会泄漏到随机客户端。若你的应用依赖这些特性,可以启用 `max_prepared_statements` 或改用 session 模式。
[databases]
* = host=127.0.0.1 port=5432
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
pool_mode = transaction # session | transaction | statement
max_client_conn = 1000 # 客户端上限
default_pool_size = 50 # 每库/每用户的真实后端连接数
min_pool_size = 5
reserve_pool_size = 10 # 突发时的应急余量
reserve_pool_timeout = 3
max_db_connections = 200 # 所有数据库合计的后端上限
auth_type = md5
admin_users = pgbouncer_admin连接风暴排查:定位泄漏源
连接风暴最常见的根因不是数据库,而是**应用侧没有池化**。典型的泄漏模式有三种:一是每次请求 `new` 一个连接却忘了关闭;二是连接池的 `max_size` 设得远大于数据库能承受的量(如 20 个服务实例各配 100,数据库只有 `max_connections=200`);三是 ORM 的 session 生命周期与请求边界不一致。
第二种根因是**连接风暴本身触发的重试风暴**。当连接开始被拒绝,应用往往配置了「连接失败就重试」,于是每个请求在 30ms、60ms、120ms 后各重试一次——本该被拒绝的 1000 个请求变成 3000 次连接尝试,让恢复变得更慢。这类问题必须同时设置连接超时与重试上限。
定位的具体手法:先用 `pg_stat_activity` 的分组查询找出 `application_name` 或 `client_addr` 维度上谁持有最多连接;再用 `pg_stat_database` 的 `numbackends` 看是哪个数据库在膨胀;最后用 `SELECT * FROM pg_stat_activity WHERE backend_type = 'client backend'` 配合 `query_start` 观察是否有大量空闲但未释放的连接。
同时也要确认 pgbouncer 自身的状态。`SHOW POOLS` 能看到每个池的 `cl_active`(活跃客户端)、`sv_active`(活跃后端)、`maxwait`(等待时间)与 `maxwait_us`。如果 `maxwait` 持续大于零,说明池子偏小或查询太慢;如果 `sv_active` 长期打满 `default_pool_size`,说明该扩容后端而不是扩容客户端。
SHOW POOLS;
SHOW DATABASES;
SHOW STATS;
SHOW CLIENTS;
SELECT datname, numbackends FROM pg_stat_database ORDER BY numbackends DESC;
SELECT client_addr, state, count(*) FROM pg_stat_activity GROUP BY 1,2 ORDER BY 3 DESC;max_connections 该设多大与配置模板
`max_connections` 不是越大越好,也不是越小越好。它的合理上限由**内存**决定:每个连接(后端进程)需要 `work_mem` 加上若干固定开销,通常按每连接 2–10 MB 估算。所以判断公式是:`max_connections × 每连接开销 < 物理内存的 60%`,剩下的留给 shared_buffers、OS 与文件缓存。
`max_connections` 之上还有 `superuser_reserved_connections`(默认 3),这是留给超级用户应急连接的配额。当普通连接被占满时,你仍然可以用超级用户连进来执行 `pg_terminate_backend()`——这条通道是「连接满了还想救火」的唯一入口,所以务必保留。
推荐的架构是:**应用侧小池 + pgbouncer + 较小 max_connections**。例如 10 个应用实例,每个 pgbouncer `default_pool_size=40`,PostgreSQL `max_connections=200`,这样无论客户端并发多少,数据库端的连接数都是可控常数。这个拓扑还有一个副作用是数据库侧的内存占用变得可预测。
最后一条容易被忽略:连接池只会复用连接,不会让慢查询变快。如果 `maxwait` 很高而 `sv_active` 并未打满,往往是某个查询慢导致连接被长时间占用。这时该做的是加索引或优化 SQL,而不是继续加大池子。
# postgresql.conf
max_connections = 200
superuser_reserved_connections = 3
shared_buffers = 8GB
work_mem = 8MB
# 应用连接串统一走 pgbouncer
# postgresql://user:pass@pgbouncer-host:6432/appdb