跳到主内容

连接数暴涨排查实录:从 max_connections 打满到 PgBouncer 分层治理 ​

问题背景:应用报错不是 SQL 慢,而是连接已经拿不到 ​

一次业务发布后,PostgreSQL 应用日志开始大量出现 FATAL: sorry, too many clients already,接口超时集中在获取数据库连接阶段。此时数据库 CPU 并不高,慢查询也没有明显增加,真正的瓶颈是 max_connections 被打满,新会话无法建立。

很多团队第一反应是直接调大 max_connections,但 PostgreSQL 的连接是进程模型,每个连接都会占用内存,并在高并发时放大上下文切换、锁竞争和缓存失效压力。盲目把连接数从 200 调到 1000,往往只是把故障从“连不上”变成“整体变慢”。

正确思路是先回答三个问题:谁占用了连接、连接处于什么状态、连接数为什么没有被复用。然后再通过应用连接池与 PgBouncer 的分层配置,把用户侧高并发收敛为数据库侧少量稳定连接。

先确认容量:max_connections、superuser_reserved_connections 与实际使用率 ​

以下 SQL 可以直接在 PostgreSQL 中执行,用于查看当前连接上限、超级用户保留连接和普通业务可用连接数。

sql
-- 查看连接上限与保留连接
SELECT name, setting, unit
FROM pg_settings
WHERE name IN ('max_connections', 'superuser_reserved_connections');

-- 查看当前总连接数与剩余容量
SELECT
  count(*) AS current_connections,
  current_setting('max_connections')::int AS max_connections,
  current_setting('superuser_reserved_connections')::int AS reserved_connections,
  current_setting('max_connections')::int
    - current_setting('superuser_reserved_connections')::int AS app_limit,
  current_setting('max_connections')::int - count(*) AS remain_total
FROM pg_stat_activity;

需要注意,superuser_reserved_connections 是给超级用户排障预留的。普通业务用户实际可用上限通常是 max_connections - superuser_reserved_connections。如果业务连接数长期贴近这个值,即使数据库还能被管理员登录,也说明容量治理已经迫在眉睫。

用 pg_stat_activity 定位连接来源与状态 ​

不要只看总连接数,必须按数据库、用户、客户端地址和状态拆分。下面的 SQL 可以快速判断连接来自哪个服务,以及它们是在执行 SQL、等待事务,还是长期空闲。

sql
-- 按数据库、用户、客户端和状态统计连接
SELECT
  datname,
  usename,
  client_addr,
  state,
  count(*) AS conn_count
FROM pg_stat_activity
WHERE pid <> pg_backend_pid()
GROUP BY datname, usename, client_addr, state
ORDER BY conn_count DESC;

-- 查看长事务和长时间空闲连接
SELECT
  pid,
  usename,
  application_name,
  client_addr,
  state,
  now() - state_change AS state_duration,
  now() - xact_start AS xact_duration,
  wait_event_type,
  wait_event,
  left(query, 120) AS query_sample
FROM pg_stat_activity
WHERE pid <> pg_backend_pid()
  AND (
    xact_start < now() - interval '5 minutes'
    OR state_change < now() - interval '10 minutes'
  )
ORDER BY state_change ASC;

排查时重点关注三类现象。

第一,某个 client_addr 连接数异常高,通常表示某个应用实例连接池配置过大,或者发布后旧实例没有正确退出。第二,大量 idle in transaction 连接,说明应用开启事务后长时间没有提交或回滚,常见原因是事务中调用外部接口、消息消费失败后未释放连接。第三,idle 连接很多但业务 QPS 不高,说明连接池最小空闲连接设置过大,或者每个 Pod 都维护了独立连接池。

方案对比:只调 max_connections,还是引入连接池 ​

常见方案有四种:直接提高数据库连接上限、优化应用连接池、引入 PgBouncer、应用池与 PgBouncer 分层组合。

应用实例 A本地连接池应用实例 B本地连接池PgBouncertransaction 模式PostgreSQL少量真实连接

直接调大 max_connections 成本最低,但只能短期止血。数据库需要为每个连接预留进程级资源,连接越多,突发流量下的调度和锁竞争越明显。

只使用应用连接池适合实例数量少、部署稳定的单体服务。例如 HikariCP 的 maximumPoolSize 不宜按 CPU 核数机械翻倍,很多 OLTP 场景下,一个 4 核应用实例配置 5 到 15 个连接已经足够。

PgBouncer 位于应用和 PostgreSQL 之间,支持 session、transaction、statement 三种模式。session 模式兼容性最好,但连接在客户端会话断开前不会归还,复用能力有限。transaction 模式在事务结束后即可复用后端连接,是最常用的 Web API 模式。statement 模式会在每条 SQL 后归还连接,事务不可用,适用范围很窄。

更稳妥的方案是分层治理:应用侧保留小连接池,用于限制单实例并发、减少短连接创建成本;PgBouncer 负责把大量应用实例的连接收敛到少量数据库连接;PostgreSQL 的 max_connections 只作为最终保护阈值。

PgBouncer 关键配置:从端口、模式到连接上下限 ​

下面是一份偏实战的 PgBouncer 配置示例,使用 transaction 模式承接普通业务。

ini
; pgbouncer.ini 核心配置
[databases]
appdb = host=10.0.0.10 port=5432 dbname=appdb

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt

; 普通业务优先使用事务级复用
pool_mode = transaction

; 每个 用户/数据库 组合最多保留多少个数据库连接
default_pool_size = 20

; 允许多少客户端排队等待连接
max_client_conn = 1000

; 空闲服务器连接、空闲客户端连接超时
server_idle_timeout = 600
client_idle_timeout = 300

; 管理控制台
admin_users = pgbouncer_admin

这里最容易误解的是 default_pool_size。它不是全局总连接数,而是按“数据库 + 用户 + 连接参数”组合划分的池大小。如果有多个业务用户、多个数据库,或者应用使用了不同 search_path、字符集等启动参数,实际总连接可能成倍增加。

max_client_conn 表示 PgBouncer 能接受的客户端连接数,可以大于数据库真实连接数,因为 transaction 模式下客户端只在事务执行期间占用服务器连接。但它也不应无限放大,否则请求会在 PgBouncer 层排队,应用却误以为数据库容量充足。

应用侧连接池配置:小而稳,而不是每个实例都开大池 ​

假设 PostgreSQL 规格为 8 核 32G,规划数据库真实业务连接约 60 个,PgBouncer 后面有 10 个应用实例。那么每个应用实例的平均连接预算只有 6 个左右。考虑批处理、管理任务和少量冗余,HikariCP 可以从如下配置开始压测。

properties
# 单实例连接池不要盲目开大
jdbcUrl=jdbc:postgresql://pgbouncer-host:6432/appdb
maximumPoolSize=8
minimumIdle=2
connectionTimeout=3000
idleTimeout=600000
maxLifetime=1800000

# 事务应短,禁止在事务中等待外部接口
connectionTestQuery=SELECT 1

如果存在定时任务、报表任务或消息消费者,建议使用独立连接池,并在 PgBouncer 中映射到不同数据库用户。这样可以通过 PostgreSQL 系统视图区分流量来源,也便于给关键交易保留容量。

可以用下面的 SQL 建立连接监控基线,持续观察峰值连接、长事务和等待事件。

sql
-- 每分钟采样一次,可结合定时任务落表
SELECT
  date_trunc('minute', now()) AS sample_time,
  datname,
  usename,
  state,
  count(*) AS conn_count,
  count(*) FILTER (WHERE xact_start < now() - interval '1 minute') AS long_xact_count
FROM pg_stat_activity
WHERE pid <> pg_backend_pid()
GROUP BY date_trunc('minute', now()), datname, usename, state;

避坑总结:连接池治理中最常见的 5 个问题 ​

1. 只调 max_connections,结果数据库整体延迟升高 ​

常见错误现象是应用不再报 too many clients already,但 P99 延迟明显上升,CPU 使用率和锁等待增加。原因是连接数超过 CPU 和存储能有效并行处理的范围,大量会话争抢资源。修正方法是回滚盲目扩容,以压测得到的数据库最佳并发为基准设置 PgBouncer default_pool_size,让超额请求在连接池层快速失败或短时间排队。

2. transaction 模式下使用会话级特性导致功能异常 ​

常见报错包括临时表找不到、SET search_path 不生效、准备语句报缓存错误、游标事务结束后不可用。PgBouncer transaction 模式只保证事务内绑定同一个后端连接,事务结束后连接可能被其他客户端复用。修正方法是把 search_path 放到数据库用户或连接串中,临时表控制在同一事务内,JDBC 开启 prepareThreshold=0 或使用 PgBouncer 文档推荐的 prepared statement 兼容配置。

3. 应用实例扩容后,总连接数按实例数暴涨 ​

单个应用实例只配置 10 个连接看起来不大,但扩容到 50 个 Pod 后就是 500 个连接。修正公式是:应用实例数 × 单实例最大池大小 ≤ PgBouncer 可承接的合理客户端规模,同时 PgBouncer 后端总连接要受数据库容量约束。自动扩缩容场景必须把连接预算纳入容量评估。

4. idle in transaction 长期占用连接 ​

现象是连接状态大量为 idle in transaction,PgBouncer 池被占满,新事务等待连接。根因通常是事务方法中包含 HTTP 调用、MQ 发送、复杂计算,或者异常后没有回滚。修正方法是缩短事务边界,把外部调用移到事务外,并设置合理的事务超时和连接最大生命周期。

5. 所有服务共用一个数据库用户,排障无法定界 ​

在 pg_stat_activity 中只能看到同一个用户名,无法区分订单、支付、报表等来源。修正方法是为不同服务或不同重要级别创建独立用户,在 PgBouncer 的 [databases] 或连接映射中区分池,并在应用连接串中设置明确的 application_name。

结语 ​

处理 PostgreSQL 连接数暴涨,不能把 max_connections 当作唯一旋钮。更可靠的路径是用 pg_stat_activity 找到来源和状态,用短事务消除连接泄漏,再通过“应用小连接池 + PgBouncer transaction 池 + 数据库保护上限”的分层架构,把不可控的前端并发收敛成数据库可承受的稳定并发。连接池配置的目标不是让所有请求都拿到连接,而是让数据库始终运行在可预测、可观测、可压测的健康区间。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。