跳至内容
12.4 会话状态与连接池陷阱

12.4 会话状态与连接池陷阱

应用看到的“一个数据库连接”可能依次经过:

request
  → application pgxpool connection
  → HAProxy service
  → PgBouncer client connection
  → one of many PostgreSQL server connections

当 PgBouncer 采用 transaction pooling 时,client connection 不是 PostgreSQL session 的永久所有者。应用必须把正确性限制在 transaction 边界,不能把上一个 transaction 留下的 session state 当作下一个 transaction 的前提。

12.4.1 session、transaction 与 statement pooling

三种“归还 backend”的时刻

modeserver connection 何时归还兼容性复用率
sessionclient 断开最接近直连最低
transactiontransaction 结束app 必须 transaction-aware
statement每条 statement 后multi-statement transaction 不成立最高、限制最多

session pooling 保留一个 client 对一个 backend 的 session 关系,许多 session feature 可用,但吸收连接峰值的能力有限。

transaction pooling:

BEGIN
  all statements use one backend
COMMIT
backend returns to pool
next transaction may use another backend

这非常适合把业务状态装进显式 transaction 的服务,也意味着跨 transaction 的 session assumption 会破坏。

statement pooling 连一个多语句事务都不能正常表达,不适合作为本章业务主线。

Transaction pooling 中哪些东西会坏

PgBouncer 官方 feature map 明确指出 transaction pooling 下不能依赖:

  • 普通 SET/RESET 的跨事务效果;
  • LISTEN
  • SQL-level PREPARE/DEALLOCATE
  • WITH HOLD cursor;
  • preserve/delete rows temp tables;
  • session-level advisory locks;
  • LOAD

例:

SET app.tenant_id = 'tenant-a';
COMMIT;

BEGIN;
SELECT ...;  -- 可能已经换 backend

第二个 transaction 不应假定 app.tenant_id 仍存在。更糟的是,拿到的 backend 可能曾服务另一个 client;可靠 pooler 会 reset/track 一部分参数,但应用不能把未知 session residue 当作隔离机制。

一个 transaction 内的状态仍然有意义

transaction pooling 在 BEGINCOMMIT/ROLLBACK 期间固定 backend,所以:

BEGIN;
SET LOCAL app.tenant_id = 'tenant-a';
SELECT ...;  -- same transaction/backend
COMMIT;

语义成立。关键不是“永远不用 SET”,而是:

state lifetime <= transaction lifetime

且每个 transaction 都重新建立需要的 context。

Autocommit 是 transaction

一条没有显式 BEGIN 的 SQL 在 PostgreSQL 中也运行于一个 transaction;在 transaction pooling 中,statement 完成后 backend 就可能归还。

所以这种代码不成立:

Exec("SET LOCAL ...")   // outside explicit transaction; warning/no effect
Query("SELECT ...")     // another transaction/backend

SET LOCAL 必须与受保护查询在同一个显式 transaction object 上执行。

不要用 session advisory lock 做跨请求 ownership

第 10 章已区分 transaction/session advisory lock。transaction pooling 下:

pg_advisory_lock()
client transaction ends
backend returns, session lock may remain on backend
next client may inherit effect
original client cannot reliably unlock same backend

这既会泄漏锁,也会让 unlock 不可达。使用:

  • pg_advisory_xact_lock,生命周期绑定当前 transaction;
  • 或把 ownership 建模成持久 lease/row;
  • 或为确需 session affinity 的任务使用单独 direct/session-pooled service。

不能为一个特殊 job 把所有在线业务都切到 session pooling;服务职责可以拆分。

12.4.2 预备语句行为必须绑定 PgBouncer 与驱动版本

“Prepared statement 与 PgBouncer 不兼容”过于粗糙

需要至少区分:

SQL PREPARE name AS ...
protocol-level named prepared statement
unnamed statement/extended protocol
driver statement cache
description cache
simple protocol interpolation
PgBouncer prepared-statement tracking

PgBouncer 1.21 起可以在 transaction pooling 中跟踪 protocol-level named prepared statements,但必须:

max_prepared_statements > 0

它会在 client/server name 之间重写,并确保目标 backend 已准备该 query。SQL-level PREPARE/DEALLOCATE 仍不受 transaction pooling 支持。

因此不能从“PgBouncer 版本够新”直接推导“所有 driver 默认都安全”。要验证:

PgBouncer exact version
max_prepared_statements actual value
pool_mode at database/user level
driver exact version
driver query mode
query parameter/result types
DDL/cache invalidation behavior
reconnect procedure

pgx 的默认行为

pgx v5.10.0 默认 QueryExecModeCacheStatement

extended protocol
automatically prepare and cache statements
single round trip after cached

如果 schema 或 search_path 在缓存后变化,第一次重新执行可能失败,例如 SELECT * 列数变化或 result type 变化。pgx 文档也提示默认 prepared statements 可能与 proxy/PgBouncer 不兼容,建议按环境选择 QueryExecModeExec 或在必要时 simple protocol。

本章保守固定:

config.ConnConfig.DefaultQueryExecMode =
    pgx.QueryExecModeExec

该模式:

  • 仍使用 extended protocol;
  • 不使用 named prepared statement cache;
  • 根据 Go argument type 推断 PostgreSQL parameter type;
  • 使用 text-formatted parameters/results;
  • 单 round trip;
  • 比 simple protocol 更优先。

这减少了本章未验证 PgBouncer config 下的一个变量,不代表 statement cache 永远不该用。生产若确认 PgBouncer tracking 与 workload 收益,应单独 A/B 并保存版本/config/DDL 恢复证据。

不要误用 SimpleProtocol

simple protocol 不是“更安全的参数化”。pgx 会在 client 端对参数插值并转义,它适合某些不支持 extended protocol 的 proxy。对标准 PostgreSQL/PgBouncer,优先尝试 QueryExecModeExec

simple/exec mode 还要求你认真处理 type mapping,尤其 []byte、JSON、用户自定义类型。不要为躲开 prepared statement 问题,悄悄改变参数编码语义而不运行 contract suite。

DDL 后的 cached plan

PgBouncer prepared-statement tracking 提升复用,但若相同 query 的 parameter/result types 在 DDL 后改变,PostgreSQL 可能报:

cached plan must not change result type

PgBouncer 文档建议这类 migration 后通过 admin console RECONNECT 让 server connections 重建计划。发布设计应回答:

  • DDL 是否改变返回列数/type;
  • old/new app 是否使用相同 query text 却期待不同 shape;
  • 是否使用 SELECT *
  • app pool 是否也有 cache;
  • PgBouncer reconnect 如何执行、影响多少连接;
  • reconnect storm 与 rollback;
  • failure metric 与 smoke query。

不能把 RECONNECT 当作每次 DDL 的盲目万能命令;先证明目标、作用域和版本。

建议的组合矩阵

pgx modePgBouncer transaction pool需要验证
cache_statementtracking onexact versions/config, DDL invalidation
cache_statementtracking off/unknown不应默认放行
cache_describeno named planresult/arg type drift
describe_exectwo round tripspooler round-trip backend affinity
execconservative mainlinetype mapping, performance
simple_protocolfallback onlyclient interpolation/type semantics

“能跑一条 SELECT 1”不能覆盖这个矩阵。本章 promotion blocker 要求用完整下单/支付/DDL smoke 通过实际 primary service。

12.4.3 SET LOCAL、事务边界与 RLS 上下文

SETSET LOCAL

PostgreSQL:

SET / SET SESSION
  current session
  if transaction commits, value persists after transaction

SET LOCAL
  only current transaction
  COMMIT or ROLLBACK ends it
  outside transaction block warns and has no effect

transaction pooling 的主线应是:

tx, err := conn.BeginTx(ctx, options)
...
_, err = tx.Exec(
    ctx,
    `SELECT set_config('app.tenant_id', $1, true)`,
    tenantID,
)
...
rows, err := tx.Query(ctx, tenantScopedSQL, ...)

set_config(..., true) 的第三个参数表示 transaction-local,便于参数化 value。不要拼:

SET LOCAL app.tenant_id = '<user input>';

RLS context 要 fail closed

若 ch23 使用:

current_setting('app.tenant_id', true)

policy 要明确 missing context 是:

zero rows / reject

而不是 fallback 到“全部租户”。还要验证:

  • runtime role 不具 BYPASSRLS
  • object owner 是否绕过 RLS;
  • FORCE ROW LEVEL SECURITY 是否需要;
  • SECURITY DEFINER 是否重新建立 context;
  • connection reset 后不存在可继承 tenant;
  • transaction retry 每次重新 SET LOCAL
  • background jobs 使用什么身份。

把 tenant 放到 application_name 不安全也会产生高基数;它是 observability label,不是授权 context。

同一 transaction 才能相信 context

错误:

pool.Exec(ctx, "SELECT set_config(..., true)")
pool.Query(ctx, tenantSQL)

两个 pool method 可能 acquire 不同连接,也一定是不同 autocommit transaction。正确:

tx, _ := pool.Begin(ctx)
tx.Exec(ctx, setLocalSQL, tenant)
tx.Query(ctx, tenantSQL)
tx.Commit(ctx)

若 query 是单条并且能把 tenant 作为普通 $1 predicate,就优先显式参数;RLS context 用于数据库必须统一执行的访问策略,不是减少一个参数的技巧。

search_path 也不要依赖 session residue

本章所有对象 schema-qualified:

shop_ch12.sales_order

SECURITY DEFINER function 固定:

SET search_path = pg_catalog

然后引用 qualified object。这样:

  • transaction pooling 不依赖前一 transaction 的 path;
  • 恶意同名 object 更难劫持;
  • query contract 明确;
  • migration 与 app 看同一对象。

若通过 role/database startup parameter 固定 search_path,仍要把它作为 connection contract 验证。

12.4.4 在 ch22、ch23 分别深化池化与权限

本节只建立应用必须知道的最小边界,不在这里展开两个独立大主题。

第 22 章将深入连接治理:

max_connections budget
PgBouncer topology and auth
pool_size/reserve/connlimit
queueing and admission control
pause/resume/reconnect
failover and connection storm
per-user/per-database pools
SHOW POOLS/STATS evidence

第 23 章将深入安全与访问:

roles and ownership
default privileges
RLS and FORCE RLS
tenant/session context
SECURITY DEFINER hardening
credential rotation
TLS/HBA
audit and break-glass

本章保留的 cross-chapter contract:

  1. 在线服务默认可以在 transaction pooling 下正确运行;
  2. 正确性不依赖跨 transaction session state;
  3. runtime role 最小权限、不是 object owner、没有 BYPASSRLS;
  4. connection/query mode 与 pooler 版本配置绑定;
  5. 特殊 session workload 使用独立 service,不污染在线主线;
  6. 权限或 pool 配置变化后重跑同一业务/failure matrix。

本节检查表

  • 知道实际 pool_mode,不从端口名猜;
  • transaction pooling 下没有跨事务 SET 前提;
  • LISTEN、temp table、cursor、advisory lock 的 lifetime 已评审;
  • transaction-local state 与业务 SQL 在同一 tx object;
  • pgx 与 PgBouncer exact versions 已记录;
  • max_prepared_statements 实际值已记录;
  • 区分 protocol prepared、SQL PREPARE 与 driver cache;
  • query mode 改变后重新验证 type mapping;
  • DDL result-shape change 有 cache/reconnect 计划;
  • SET LOCALset_config(..., true) fail closed;
  • tenant context 不是 authorization 的唯一应用侧证据;
  • SQL schema-qualified,SECURITY DEFINER path 固定;
  • 特殊 session workload 使用独立连接路径。

参考资料


上一节:Go 服务中的连接与事务 · 返回本章目录 · 下一节:服务级可观测性 · 查看全书目录 · 查看索引中心

最后更新于