pgbouncer 事务池:prepared statement、临时表与会话状态

大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。

上个月给一套 PG 库加 pgbouncer。方案评审十分钟就过,配置照着博客抄了一版。上线第一天没事,第三天开始零星报错。报的是找不到 prepared statement,重试一下又好了。白天看不出规律,凌晨两点必来一波,那是批处理启动的时段。这种故障最难查。

一、先看清错误长什么样

报错原文大概是这样。ERROR: prepared statement "S_1" does not exist,后面还跟着一句 STATEMENT: EXECUTE S_1(...)。十条请求里有一条报,另外九条正常。重启应用能安静半天,重启数据库一点用没有。

这种分布本身就说明问题。不是数据错,也不是权限错。是状态在不同会话之间被换了。想清这一点,我才把方向从数据库挪到连接池。

我先按老习惯查了库侧。pg_stat_activity 里没有长事务。锁等待是零,慢日志干干净净。连接数两百多条,比预期多一截。到这一步我才反应过来,有别人在替应用执行 EXECUTE。

二、三种池模式,保留的东西不一样

pgbouncer 有三种池模式。区别只有一个,连接在什么时候还回去。session 模式下,一条客户端连接从头独占一条服务端连接。行为和直连几乎一样,代价是省不下多少连接。

transaction 模式下,事务一结束就归还。别的客户端随时可以借走。statement 更激进,一条语句跑完就还,事务边界都不保留。模式越激进,省下的连接越多,背后的假设也越强。

问题就出在"还回去"这三个字上。会话级的资源没跟着走。它留在旧连接上,被下一个借用者接管。session 里不出事,因为连接不换手。transaction 里必出事,只要应用用了会话级的东西。

三、会话状态都挂在哪

PG 的很多能力绑在会话上,不在事务上。PREPARE 准备的语句按名字存在会话里。临时表只在当前会话可见。SET search_path 改的也是会话变量。

LISTEN 注册的监听跟着会话走。pg_advisory_lock 是会话级锁,只有 pg_advisory_xact_lock 才跟事务。连游标都要分,WITH HOLD 的游标能活过提交。这几样名字不同,性质一样,都活在会话里。

这些状态在事务池下会漏。你 SET 完,事务提交,连接还回去。下一个借用者拿到的是带你设置的连接。轻的是行为怪异,重的就是越权。多租户系统里,search_path 漏一次,用户可能查到自己不该看的库。这个后果比报错严重得多。

PREPARE 漏得最直接。下一个借用者执行 EXECUTE。服务端找不到那条语句,报错就来了。这就是我那三天看到的全部现象。

四、驱动侧和池侧,两个开关

修法有两条路,选一条就行。第一条是不让驱动做服务端准备。JDBC 的 prepareThreshold 设成 0,服务端就不缓存语句。代价是每次多一次解析,好处是最省事。

第二条是让 pgbouncer 自己接管。1.21 起支持 max_prepared_statements。设成非零值后,池子代替后端做准备。客户端照常用协议级 prepared statement,性能也更好。前提是池子版本够新。

bash 复制代码
# 第一条路:JDBC 连接串,禁用服务端 prepared statement
jdbc:postgresql://pgbouncer-host:6432/orderdb?prepareThreshold=0
ini 复制代码
# 第二条路:让池子接管准备语句
[pgbouncer]
pool_mode = transaction
max_prepared_statements = 200
default_pool_size = 40
server_reset_query = DISCARD ALL

这里有个坑要提前说。server_reset_query 在事务池下默认不执行。它只在 session 模式生效。很多人以为写了 DISCARD ALL 就安全,其实一次都没跑。真要强制清,得开 server_reset_query_always。

五、后端连接数怎么算

连接池不造连接,它只是把前端连接排队。真正打到 PG 的数,等于池大小乘库数再乘用户数。再受 max_db_connections 兜底。前端上限是 max_client_conn,这两个数不是一回事。

算一笔账就清楚了。三个库,两个用户,池大小如果是 40。后端最多 240 条连接。加上管理连接,已经接近一台 8 核机器的合理上限。

我当时把池大小抄成了 500。前端一压,池子拼命建服务端连接。PG 直接报连接数超限。应用侧的报错反而更多,方向也更容易跑偏。

sql 复制代码
-- 后端到底被占了多少:按库和用户看
SELECT datname, usename, state, count(*)
FROM pg_stat_activity
GROUP BY datname, usename, state
ORDER BY count(*) DESC;

-- 有没有连接停在 idle in transaction
SELECT pid, state, now() - state_change AS idle_for
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY idle_for DESC
LIMIT 10;

六、观测看三个视图

pgbouncer 有一套管理库。连上去就能看状态。SHOW POOLS 最常用。看 cl_active、cl_waiting、sv_active、sv_idle 四个数就够了。

cl_waiting 不归零,说明池子小或者后端慢。sv_idle 高、cl_waiting 也高,多半某个库被单独卡住。SHOW STATS 看累计吞吐和平均耗时,适合看趋势。SHOW CLIENTS 和 SHOW SERVERS 对前后端映射。

我那次就是靠 SHOW SERVERS 找到线索。有一批连接挂在同一个库上。这才确认是连接数预算算错,不是池子不够大。

sql 复制代码
pgbouncer=# SHOW POOLS;
 database  | user  | cl_active | cl_waiting | sv_active | sv_idle | maxwait
-----------+-------+-----------+------------+-----------+---------+---------
 orderdb   | app   |        86 |          0 |        38 |       2 |       0
 reportdb  | app   |        12 |          9 |        40 |       0 |     312

这张表读法很简单。reportdb 那行就是现场。前端十二个活跃,九个在排队。后端四十个全忙,最久的等了 312 毫秒。不是池子小,是报表查询太慢。

七、什么时候别用事务池

不是所有系统都适合事务池。用临时表的报表任务不适合。依赖 LISTEN 做通知的不适合。靠会话级 advisory lock 串行化的,也不适合。用 WITH HOLD 游标分批取数的,同样不适合。

标准做法是分池。业务接口走 transaction 池。报表和批处理走 session 池。在 pgbouncer 里按库或按用户配不同的 pool_mode。这样既拿到复用收益,也不用跟会话状态较劲。

多花的那点配置时间,比上线后查三个通宵便宜。这条账我算过一次。以后新项目我都先问一句,有没有用到会话状态。有就分池,没有才统一用事务池。

八、避坑清单

事务池配 PREPARE,先确认驱动侧关掉了服务端准备。prepareThreshold=0 是最快的止血。池子版本够新就用 max_prepared_statements 接管,别两个都不动就上线。

server_reset_query 别只看写了没写,要看生效没生效。事务池下它默认不跑。会话变量会漏给下一个借用者,多租户系统里这条能出安全事故。

最后一条是我自己踩的。算后端连接数时,我直接拿前端连接数当池子上限。池子填了五百,三个库两个用户,理论上是三千条。PG 的上限当场被顶穿,报错从"找不到语句"变成了"连接被拒"。从此我先算库乘用户再乘池大小,把结果卡在核数的两到四倍以内。

写在最后

连接池省的是连接,赌的是"你的应用不需要会话状态"。这句话在 MySQL 的客户端池里成立得很轻松。到了 PG 的服务端池,就得重新数一遍。事务池不是更先进的模式,它是更强的假设。

判断要不要用事务池,只看一件事。你的代码里有没有东西活过了事务。有,就分池或者改代码。没有,就放心用。查报错之前先想清楚这件事,能省掉两个通宵。文中的耗时与连接数是示意值,用来看量级和顺序。

你们上 pgbouncer 用的是哪种池模式?有没有遇到过 SET 漏给下一个会话?这两个答案决定要不要分池。评论区说说,我帮你看看该怎么拆。

我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋

相关推荐
三小河1 小时前
SWC 凭什么比 Babel 快 ?项目优化迁移踩坑记录
前端·后端
鶴哥只手遮天1 小时前
《Redis 8.10.3 深度剖析》系列 ·第 17 章. 一个迟到十六年的类型
redis·后端·开源
知守观1 小时前
同一个团队两套分包:DDD 四层落地后,domain 层最大的类还是 5885 行
java·后端·领域驱动设计
Lucifer三思而后行2 小时前
最近折腾了一下金仓数据库的备份恢复
数据库·备份恢复
Anymous2 小时前
我手写的 Redis 锁,输给了 10 秒的 TTL
后端·架构
Rain的Java大神之路2 小时前
设计一套生产级 API 接口:从契约、安全、幂等到限流,我踩过的坑都在这了
java·后端·面试
SimonKing2 小时前
零配置也能白嫖前沿大模型!dsh-our-free-model插件真香警告
java·后端·程序员
草莓熊Lotso2 小时前
【C++ 基于微服务的即时通讯系统】文件存储子服务实战:UUID生成、文件IO封装与全接口单元测试
开发语言·网络·数据库·c++·微服务·架构·单元测试
nice_lcj5202 小时前
【从零理解 Spring AI · 15】让检索更准:先宽召回,再精排序
java·后端·rag·spring ai·ai应用开发·检索优化·重排序