PgDog v0.1.62:事务池复用连接时,SET/RESET 与 advisory lock 的会话状态怎么串味
PgDog 是 Rust 写的 PostgreSQL 连接池 / 负载均衡 / 分片代理,AGPL-3.0,默认监听 6432,配置走 pgdog.toml 与 users.toml。
- 项目地址:https://github.com/pgdogdev/pgdog
- 官网:https://pgdog.dev
- 文档:https://docs.pgdog.dev
问题:事务模式下连接是共享的
pooler_mode = "transaction" 是默认值。一个事务结束,server 连接立刻还给池子,下一个客户端接着用。凡是落在会话级的东西——SET、RESET、DISCARD ALL、advisory lock、temp 对象、prepared statement——都可能被下一个客户端继承。v0.1.62 集中修的就是这批"串味"路径。
v0.1.62 逐条行为差异
1. SET 在事务内正确覆盖 SET LOCAL
之前两者顺序语义不对,同一事务里 SET LOCAL 之后跟 SET,最终生效值可能反了。
BEGIN;
SET LOCAL statement_timeout = '1s';
SET statement_timeout = '30s';
SHOW statement_timeout; -- 期望 30s
COMMIT;
2. RESET 恢复的是启动参数
之前 RESET 回到的是代理侧默认值,现在回到该 server 连接建立时 startup packet 里的参数值。差别在 application_name、search_path 这类由连接串带进来的参数上最明显。
3. ROLLBACK 不再撤销上一个事务里的 RESET
BEGIN;
RESET statement_timeout;
ROLLBACK;
SHOW statement_timeout; -- 0.1.62 之前这里会退回 RESET 前的值
4. 非法参数设置不再把连接留在半脏状态SET statement_timeout = 'abc'; 报错后,之前连接可能已被部分改动,现在先校验再改。
5. 参数名转义修正
带引号 / 特殊字符的 GUC 名之前被错误转义。
6. SET LOCAL ... TO DEFAULT 不再被当成 SET
这是最直接的泄漏路径:本应只作用于当前事务的语句被按会话级 SET 处理,提交后仍留在连接上,下一个客户端直接读到。
SET LOCAL application_name TO DEFAULT; -- 0.1.62 之前会逃出事务
7. DISCARD ALL 返回正确的 command tag
tag 不对会打断 JDBC 驱动对 prepared statement 的处理。验证就一句话:DISCARD ALL;
8. pg_try_advisory_lock 不再乐观 pin
之前只要调用就 pin 住会话,哪怕锁没拿到;现在只有真正获取成功才 pin。触发场景是两个客户端抢同一把锁,第二个返回 false 却仍占着一条 server 连接,池子可用连接数凭空少一条。
SELECT pg_try_advisory_lock(42); -- 第二个客户端应为 false 且不占连接
9. 跨分片 DELETE 里 now() 结果一致
非确定性函数在同一语句的各分片间取同一值。
10. prepared statement 缓存的内存峰值
tokio-postgres、go/pq、Postgrex 这类会 prepare 一次性语句的驱动,突发新语句后缓存内存会顶上去;0.1.62 做了回收。相关旋钮是 prepared_statements_limit = 1000、prepared_statements_ttl = 300000(jitter 30000)。
其余条目:COPY 标识符转义、跨分片 avg() 精度、join 分片键识别、不支持的聚合返回 ERROR 而非 FATAL、FreeBSD 编译、query_parser 关闭时 ABORT/END 的回归。
v0.1.61 同一条线上的还有:DISCARD/TEMP、FORCE_RELOAD、LISTEN/NOTIFY 标识符转义、passthrough auth 坏密码阻塞连接池、Patroni 场景用 WAL timeline 判断新主、pg_advisory_unlock(NULL) 误释放全部 advisory lock、CREATE TEMP SEQUENCE 需要 pin 连接。
配置片段与默认值
pooler_mode = "transaction" # session 模式把连接锁给单客户端直到断开
default_pool_size = 10
min_pool_size = 1
checkout_timeout = 5000
rollback_timeout = 5000 # 放弃的事务自动回滚
healthcheck_interval = 30000
ban_timeout = 300000
prepared_statements = "extended" # disabled | extended | full
prepared_statements_limit = 1000
prepared_statements_ttl = 300000 # jitter 30000
query_parser = "on"
read_write_strategy = "conservative" # conservative | aggressive
load_balancing_strategy = "least_active_connections" # random | least_active_connections | round_robin
openmetrics_port = 9090
workers 建议每 CPU 2 个。
什么时候必须 session 模式或 pin
- LISTEN/NOTIFY:PgDog 自己实现了 pub/sub 队列来收发通知,所以在事务模式下不 pin 也能用。这是相对 PgBouncer 的差异点——后者通常要靠绕过连接池直达库。但走别的代理或直连时,仍需 session 模式。
- advisory lock:阻塞式
pg_advisory_lock会 pin;pg_try_advisory_lock只在真正拿到锁时 pin(0.1.62 起)。 - temp 对象:
CREATE TEMP SEQUENCE明确需要 pin,temp 表同理走 DISCARD/TEMP 这条路径。 - prepared statements:默认
extended够用;一次性 prepare 密集的负载要盯prepared_statements_limit和 TTL,否则内存会跟着语句种类数走。
升级与验证
docker run ghcr.io/pgdogdev/pgdog:latest
helm repo add pgdogdev https://helm.pgdog.dev
helm install pgdog pgdogdev/pgdog
PGPASSWORD=postgres psql -h 127.0.0.1 -p 6432 -U postgres
升级后至少跑三组验证:
-- 1. SET / SET LOCAL 覆盖顺序
BEGIN; SET LOCAL statement_timeout='1s'; SET statement_timeout='30s';
SHOW statement_timeout; COMMIT;
-- 2. RESET 是否跨事务留存
BEGIN; RESET statement_timeout; ROLLBACK; SHOW statement_timeout;
-- 3. 另一个会话不应继承上面的任何设置
SHOW statement_timeout; SHOW application_name;
再从两个客户端并发 pg_try_advisory_lock,配合 9090 端口的 OpenMetrics 看池子占用是否回到基线。被遗弃的事务会被回滚、被遗弃的查询会被 drain,应用崩溃时不会演变成连接风暴。
取舍与未验证点
事务模式省连接、吞吐好,代价是每个会话级特性都变成特例,要代理自己维护一套语义——SET/LISTEN/advisory lock 这三块 PgDog 是自己在做。对比 PgBouncer:无负载均衡(要外部 TCP 代理)、无读写分离、无 failover、无健康检查、无 mirroring、prepared statements 支持有限。
还没验证的:长时间跑突发 prepare 后缓存内存是否真的稳定回落;不同版本 JDBC 驱动对修正后 DISCARD ALL tag 的兼容性;跨分片 now() 一致性在更复杂语句形态下的覆盖范围。