PostgreSQL连接池打满导致服务雪崩:我用pgBouncer+连接数监控把并发从200稳定到2000
PostgreSQL连接池打满导致服务雪崩:我用pgBouncer+连接数监控把并发从200稳定到2000
上周三下午,我正在喝茶,监控群突然炸了。
“订单服务全部超时!” “支付接口 502!” “数据库 CPU 只有 30%,但所有请求都在排队!”
我打开 Grafana 一看,PgBouncer 的连接数飙到 200(我们的 max_connections 设的 200),但 CPU 和内存都很闲。说白了,连接池满了,新请求进不来,服务一层层拖垮。
这篇文章记录我从发现到解决的完整过程,包括几个踩坑点。如果你也在用 PostgreSQL,建议看完。
现象:CPU 很闲,但服务全挂了
先说说当时的现场。
14:23,订单服务开始出现大量 connection timeout。
14:25,支付、库存、用户服务相继告警——它们共用同一个 PostgreSQL 集群。
14:28,我登录数据库主机,执行:
SELECT count(*) FROM pg_stat_activity;
返回 200。再执行:
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
结果让我愣了一下:
| state | count |
|---|---|
| active | 23 |
| idle | 177 |
177 个连接是 idle 状态! 活跃连接只有 23 个,但连接池已经满了。
这说明问题不是慢查询拖垮的,而是连接没有被及时释放。
根因:连接池配置 + 应用层没关连接
我拉了一个 idle 连接的明细:
SELECT pid, usename, application_name, client_addr, state, state_change
FROM pg_stat_activity
WHERE state = 'idle'
ORDER BY state_change;
发现大量连接的 application_name 是 java-order-service,state_change 都是几分钟前的。明显是 Java 应用拿了连接没归还。
去查代码,发现问题很蠢但很常见:
// 有问题的代码(简化版)
public void createOrder(Order order) {
Connection conn = dataSource.getConnection();
// ... 执行业务逻辑
if (order.getAmount() > 1000) {
// 这里抛了个异常,连接没关闭!
throw new IllegalArgumentException("金额超限");
}
conn.close();
}
异常路径没走 conn.close(),连接就泄漏了。类似的代码还有好几处。
但光是修代码不够。我问自己:就算代码完美了,200 个连接能撑多大并发?
算了一笔账:
- 每个请求平均持有连接 50ms
- 200 连接理论上 QPS 上限 = 200 / 0.05 = 4000
- 但实际有网络延迟、业务逻辑、事务等待,有效 QPS 大概 800-1000
我们高峰期并发能到 1500,200 连接根本不够。代码泄漏只是加速器,容量不足才是根本问题。
方案:pgBouncer + 三层防护
我决定上三层防护,而不是只修代码。
第一层:引入 pgBouncer 做连接池化
pgBouncer 是 PostgreSQL 的连接池中间件,作用很简单:
- 应用连 pgBouncer,不是直接连 PostgreSQL
- pgBouncer 维护一小撮真实连接,复用给大量客户端
- 支持事务级池化(transaction pooling),一个连接可以给多个请求复用
安装很简单,Ubuntu/Debian:
sudo apt-get install pgbouncer
配置文件 /etc/pgbouncer/pgbouncer.ini:
[databases]
order_db = host=10.0.1.10 port=5432 dbname=order_db
[pgbouncer]
listen_port = 6432
listen_addr = 0.0.0.0
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
# 池化模式:transaction 模式最高效
pool_mode = transaction
# 关键参数
max_client_conn = 2000
default_pool_size = 50
reserve_pool_size = 10
reserve_pool_timeout = 3
# 连接超时释放
server_idle_timeout = 60
server_lifetime = 3600
划重点:
pool_mode = transaction:事务结束立即归还连接,复用率最高max_client_conn = 2000:应用端可以发 2000 个并发请求过来default_pool_size = 50:pgBouncer 只向 PostgreSQL 开 50 个真实连接
这意味着:应用并发 2000,数据库只承受 50 连接。
userlist.txt 格式:
"app_user" "md5xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
密码用 PostgreSQL 的 MD5 格式,可以用这个 SQL 生成:
SELECT rolname, rolpassword FROM pg_authid WHERE rolname = 'app_user';
启动 pgBouncer:
sudo systemctl enable pgbouncer
sudo systemctl start pgbouncer
第二层:应用层连接配置兜底
就算有了 pgBouncer,应用层的连接池配置也要对齐。以 HikariCP 为例:
# 改之前:直接连 PostgreSQL
# spring.datasource.url=jdbc:postgresql://10.0.1.10:5432/order_db
# 改之后:连 pgBouncer
spring.datasource.url=jdbc:postgresql://10.0.1.10:6432/order_db
# 应用连接池不用开太大,pgBouncer 已经做了池化
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
# 关键:连接超时和空闲回收
spring.datasource.hikari.connection-timeout=5000
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.max-lifetime=600000
# 泄漏检测
spring.datasource.hikari.leak-detection-threshold=60000
leak-detection-threshold 是神器:如果连接被占用超过 60 秒,HikariCP 会打日志告诉你哪个线程泄漏了。
第三层:连接数监控 + 告警
没有监控等于盲人摸象。我加了三层监控:
1. pgBouncer 连接数监控
pgBouncer 自带 stats 接口,可以用 SHOW POOLS:
-- 连到 pgbouncer 的管理端口
psql -p 6432 pgbouncer -c "SHOW POOLS"
输出:
database | user | cl_active | cl_waiting | sv_active | sv_idle | sv_used | max_wait
-----------+-------+-----------+------------+-----------+---------+---------+----------
order_db | app | 45 | 2 | 10 | 15 | 25 | 0.2
关键字段:
cl_waiting:客户端等待连接的请求数,> 0 说明池子不够用了sv_active:正在执行的真实连接数max_wait:最长等待时间,秒级
2. PostgreSQL 真实连接数监控
SELECT count(*) as total_conn,
count(*) FILTER (WHERE state = 'active') as active_conn,
count(*) FILTER (WHERE state = 'idle') as idle_conn
FROM pg_stat_activity
WHERE backend_type = 'client backend';
3. Prometheus + Grafana 告警
我把 pgBouncer 的指标用 pgbouncer_exporter 接到 Prometheus:
# alert rules
groups:
- name: postgresql-alerts
rules:
- alert: PgBouncerClientsWaiting
expr: pgbouncer_pools_client_waiting > 0
for: 1m
labels:
severity: warning
annotations:
summary: "pgBouncer 有客户端在等待连接"
- alert: PostgreSQLConnectionsHigh
expr: |
(
pg_stat_activity_count{state="active"}
/ pg_settings_max_connections
) > 0.7
for: 2m
labels:
severity: critical
效果:并发从 200 稳定到 2000
改完之后,我做了压测对比:
| 指标 | 改之前 | 改之后 |
|---|---|---|
| 应用并发连接 | 200(硬上限) | 2000 |
| PostgreSQL 真实连接 | 200 | 50(pgBouncer 池化) |
| P99 响应时间 | 超时/挂掉 | 45ms |
| 连接泄漏导致故障 | 每月 1-2 次 | 0 |
最直观的感受:高峰期 Grafana 的 cl_waiting 一直是 0,连接池终于不再是瓶颈。
踩坑记录
这次踩了几个坑,记录一下。
坑 1:pgBouncer 的 transaction 模式不支持 PREPARE
我们有几条用 PREPARE + EXECUTE 的查询,切到 pgBouncer 后直接报错:
ERROR: prepared statement "S_1" does not exist
原因是 transaction 模式下,每个事务可能用不同的后端连接,PREPARE 的状态没法跨连接保持。
解决:要么改代码不用 PREPARE,要么把这条业务切到 session 模式(复用率低点,但兼容性好)。
坑 2:HikariCP 的 max-lifetime 要小于 PostgreSQL 的 idle_timeout
刚开始配置没对齐,HikariCP 的连接 lifetime 是 30 分钟,PostgreSQL 的 idle_in_transaction_session_timeout 是 15 分钟。结果 HikariCP 以为连接还活,其实数据库端已经断了,报一堆 connection reset。
解决:统一时间线——
- HikariCP
max-lifetime= 10 分钟 - PostgreSQL
idle_in_transaction_session_timeout= 5 分钟 - pgBouncer
server_lifetime= 1 小时
坑 3:pgBouncer 的 auth_file 密码格式不对
第一次配的时候,直接把明文密码写进 userlist.txt,启动报错。后来才搞清楚要 MD5 格式,或者配 auth_type = scram-sha-256 对齐 PostgreSQL 14+ 的默认认证。
坑 4:只改连接池,没修代码泄漏
一开始想偷懒,觉得上了 pgBouncer 就高枕无忧了。结果压测跑了 2 小时,pgBouncer 的 sv_active 一直飙满,应用层的连接还是占着不放。
解决:必须修代码,用 try-with-resources:
public void createOrder(Order order) {
try (Connection conn = dataSource.getConnection()) {
// ... 执行业务逻辑
if (order.getAmount() > 1000) {
throw new IllegalArgumentException("金额超限");
}
// 自动关闭,不管有没有异常
}
}
写在最后
这次事故给我的教训是:连接池不是无限资源,200 个连接在微服务架构下很容易打满。
pgBouncer 不是银弹,但它确实把「应用并发」和「数据库连接」解耦了。以前 200 并发就炸,现在 2000 并发数据库还是 50 连接,从容很多。
如果你也在用 PostgreSQL,建议检查一下:
- 你的应用连接池有没有泄漏?
- 你的 max_connections 真的够吗?
- 你有没有监控
pg_stat_activity的 idle 连接比例?
有问题欢迎评论区交流。
更多推荐



所有评论(0)