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我登录数据库主机执行SELECTcount(*)FROMpg_stat_activity;返回 200。再执行SELECTstate,count(*)FROMpg_stat_activityGROUPBYstate;结果让我愣了一下statecountactive23idle177177 个连接是 idle 状态活跃连接只有 23 个但连接池已经满了。这说明问题不是慢查询拖垮的而是连接没有被及时释放。根因连接池配置 应用层没关连接我拉了一个 idle 连接的明细SELECTpid,usename,application_name,client_addr,state,state_changeFROMpg_stat_activityWHEREstateidleORDERBYstate_change;发现大量连接的application_name是java-order-servicestate_change都是几分钟前的。明显是 Java 应用拿了连接没归还。去查代码发现问题很蠢但很常见// 有问题的代码简化版publicvoidcreateOrder(Orderorder){ConnectionconndataSource.getConnection();// ... 执行业务逻辑if(order.getAmount()1000){// 这里抛了个异常连接没关闭thrownewIllegalArgumentException(金额超限);}conn.close();}异常路径没走conn.close()连接就泄漏了。类似的代码还有好几处。但光是修代码不够。我问自己就算代码完美了200 个连接能撑多大并发算了一笔账每个请求平均持有连接 50ms200 连接理论上 QPS 上限 200 / 0.05 4000但实际有网络延迟、业务逻辑、事务等待有效 QPS 大概 800-1000我们高峰期并发能到 1500200 连接根本不够。代码泄漏只是加速器容量不足才是根本问题。方案pgBouncer 三层防护我决定上三层防护而不是只修代码。第一层引入 pgBouncer 做连接池化pgBouncer 是 PostgreSQL 的连接池中间件作用很简单应用连 pgBouncer不是直接连 PostgreSQLpgBouncer 维护一小撮真实连接复用给大量客户端支持事务级池化transaction pooling一个连接可以给多个请求复用安装很简单Ubuntu/Debiansudoapt-getinstallpgbouncer配置文件/etc/pgbouncer/pgbouncer.ini[databases] order_db host10.0.1.10 port5432 dbnameorder_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 50pgBouncer 只向 PostgreSQL 开 50 个真实连接这意味着应用并发 2000数据库只承受 50 连接。userlist.txt格式app_user md5xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx密码用 PostgreSQL 的 MD5 格式可以用这个 SQL 生成SELECTrolname,rolpasswordFROMpg_authidWHERErolnameapp_user;启动 pgBouncersudosystemctlenablepgbouncersudosystemctl start pgbouncer第二层应用层连接配置兜底就算有了 pgBouncer应用层的连接池配置也要对齐。以 HikariCP 为例# 改之前直接连 PostgreSQL # spring.datasource.urljdbc:postgresql://10.0.1.10:5432/order_db # 改之后连 pgBouncer spring.datasource.urljdbc:postgresql://10.0.1.10:6432/order_db # 应用连接池不用开太大pgBouncer 已经做了池化 spring.datasource.hikari.maximum-pool-size20 spring.datasource.hikari.minimum-idle5 # 关键连接超时和空闲回收 spring.datasource.hikari.connection-timeout5000 spring.datasource.hikari.idle-timeout300000 spring.datasource.hikari.max-lifetime600000 # 泄漏检测 spring.datasource.hikari.leak-detection-threshold60000leak-detection-threshold是神器如果连接被占用超过 60 秒HikariCP 会打日志告诉你哪个线程泄漏了。第三层连接数监控 告警没有监控等于盲人摸象。我加了三层监控1. pgBouncer 连接数监控pgBouncer 自带 stats 接口可以用SHOW POOLS-- 连到 pgbouncer 的管理端口psql-p6432pgbouncer-cSHOW 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 真实连接数监控SELECTcount(*)astotal_conn,count(*)FILTER(WHEREstateactive)asactive_conn,count(*)FILTER(WHEREstateidle)asidle_connFROMpg_stat_activityWHEREbackend_typeclient backend;3. Prometheus Grafana 告警我把 pgBouncer 的指标用 pgbouncer_exporter 接到 Prometheus# alert rulesgroups:-name:postgresql-alertsrules:-alert:PgBouncerClientsWaitingexpr:pgbouncer_pools_client_waiting0for:1mlabels:severity:warningannotations:summary:pgBouncer 有客户端在等待连接-alert:PostgreSQLConnectionsHighexpr:|( pg_stat_activity_count{stateactive} / pg_settings_max_connections ) 0.7for:2mlabels:severity:critical效果并发从 200 稳定到 2000改完之后我做了压测对比指标改之前改之后应用并发连接200硬上限2000PostgreSQL 真实连接20050pgBouncer 池化P99 响应时间超时/挂掉45ms连接泄漏导致故障每月 1-2 次0最直观的感受高峰期 Grafana 的cl_waiting一直是 0连接池终于不再是瓶颈。踩坑记录这次踩了几个坑记录一下。坑 1pgBouncer 的 transaction 模式不支持 PREPARE我们有几条用PREPAREEXECUTE的查询切到 pgBouncer 后直接报错ERROR: prepared statement S_1 does not exist原因是 transaction 模式下每个事务可能用不同的后端连接PREPARE的状态没法跨连接保持。解决要么改代码不用PREPARE要么把这条业务切到session模式复用率低点但兼容性好。坑 2HikariCP 的 max-lifetime 要小于 PostgreSQL 的 idle_timeout刚开始配置没对齐HikariCP 的连接 lifetime 是 30 分钟PostgreSQL 的idle_in_transaction_session_timeout是 15 分钟。结果 HikariCP 以为连接还活其实数据库端已经断了报一堆connection reset。解决统一时间线——HikariCPmax-lifetime 10 分钟PostgreSQLidle_in_transaction_session_timeout 5 分钟pgBouncerserver_lifetime 1 小时坑 3pgBouncer 的 auth_file 密码格式不对第一次配的时候直接把明文密码写进userlist.txt启动报错。后来才搞清楚要 MD5 格式或者配auth_type scram-sha-256对齐 PostgreSQL 14 的默认认证。坑 4只改连接池没修代码泄漏一开始想偷懒觉得上了 pgBouncer 就高枕无忧了。结果压测跑了 2 小时pgBouncer 的sv_active一直飙满应用层的连接还是占着不放。解决必须修代码用 try-with-resourcespublicvoidcreateOrder(Orderorder){try(ConnectionconndataSource.getConnection()){// ... 执行业务逻辑if(order.getAmount()1000){thrownewIllegalArgumentException(金额超限);}// 自动关闭不管有没有异常}}写在最后这次事故给我的教训是连接池不是无限资源200 个连接在微服务架构下很容易打满。pgBouncer 不是银弹但它确实把「应用并发」和「数据库连接」解耦了。以前 200 并发就炸现在 2000 并发数据库还是 50 连接从容很多。如果你也在用 PostgreSQL建议检查一下你的应用连接池有没有泄漏你的 max_connections 真的够吗你有没有监控pg_stat_activity的 idle 连接比例有问题欢迎评论区交流。