12.3 Go 服务中的连接与事务
driver 把 SQL 发给 PostgreSQL;它不会替你决定连接预算、deadline、事务重试和健康语义。真正的服务可靠性来自这些边界能否闭合:
12.3.1 连接池大小、超时与上下文取消
NewWithConfig 不等于已连接
pgxpool 的构造可以在没有建立连接时返回。启动 gate 必须主动 Ping 或 Acquire:
这只能证明 startup 时取得过一条连接。持续 readiness、业务 SLI 和 pool metrics 仍然必要。
连接预算是一个全局不等式
直连时,一个保守起点:
[ \sum_i (\text{replicas}_i \times \text{MaxConns}_i)
- \text{admin}
- \text{migrations}
- \text{jobs}
- \text{monitoring} \le \text{PostgreSQL usable connections} ]
usable 不是机械等于 max_connections;要保留:
- superuser/emergency;
- Patroni/monitoring/replication;
- migration 与诊断;
- failover 后可能同时重连的 headroom;
- 连接建立的 CPU/内存成本。
有 PgBouncer 后还要分两层:
不要把“每个 pod 50 × 100 pods”直接当成 PostgreSQL 5000 条 backend,也不要因此完全忽略 app pool。前一层决定本进程排队、fd 和 PgBouncer client 压力;后一层决定数据库真实并发。两层都过大只会把拥塞从一个队列搬到另一个。
池大小不是 CPU 数量公式
需要从 workload 推导:
然后用负载实验观察:
- acquire wait 与 canceled acquire;
- database active sessions;
- CPU、IO、locks 与 cache behavior;
- p95/p99 request latency;
- throughput 是否继续增加;
- failover/reconnect storm。
如果业务一次请求 100 ms,其中 SQL 只占 5 ms,不应在远程调用期间一直占连接。先缩短 hold time,通常比扩大池更有效。
本章把 MaxConns=2 作为故障夹具,不是生产推荐值。两条连接都执行受控 pg_sleep 后:
它证明排队与健康语义,不测容量。
Deadline 要形成预算瀑布
不同 timeout 回答不同问题:
常见设计是让数据库 timeout 早于最外层 deadline,给 rollback、错误映射和响应留出时间:
数字必须来自 SLO 与实测,不可照抄。要避免:
- inner timeout 大于 outer,永远没有机会生效;
- 全局
statement_timeout误伤 migration/ETL; - request context 没传给
Acquire/Exec/Query; - timeout 后用背景 context 继续业务写;
- rollback 没有独立有限 cleanup context。
本章业务调用始终传 request.Context();只有 rollback 使用新的 1 秒 cleanup context,避免 client cancel 让 rollback 根本发不出去,也避免无界挂起。
取消不是“goroutine 返回就结束”
HTTP client 离开后要验证整条链:
实验:
499 是内部 observability 分类,不是 PostgreSQL 或 HTTP 标准必须返回的业务 contract;客户端已经断开,通常收不到该响应。
连接错误与查询错误要分开
在 pool.Acquire(ctx) 处失败,SQL 还没开始。本章映射:
取得连接后 statement_timeout:
这一区分能快速回答“请求慢在应用 pool,还是慢在 PostgreSQL”。若只记一个 db_error_total,诊断又退回猜测。
12.3.2 事务函数、失败重试与外部副作用
Wrapper 必须拥有完整 transaction lifecycle
本章 wrapper 每次 attempt:
不能只重试最后一条 SQL:
PostgreSQL 对 serialization failure 的要求是 abort 并从 transaction 起点重新执行。40P01 也使 transaction 失败;若选择重试,同样要重放完整业务单元。
Retry allowlist
本章只允许:
最多三次 attempt,attempt 之间有限 backoff,并受 request context 约束。以下错误不应被 wrapper 盲重试:
| error | 原因 |
|---|---|
23505 / 23514 |
通常是确定性业务/contract 冲突 |
42501 |
权限发布缺陷 |
42P01 / 42703 |
schema/version 缺陷 |
57014 |
预算已耗尽或显式取消 |
| unknown connection loss after COMMIT sent | commit outcome 可能未知 |
连接断开尤其危险:
不等于:
这就是 idempotency ledger 的用途。客户端用同 key 重放,数据库返回既有结果,而不是凭网络异常猜 commit outcome。
Callback 必须可重放
事务 retry callback 中不能做:
- 发送邮件/短信;
- 调支付 API;
- publish broker message;
- 写不可回滚文件;
- 增加非幂等外部计数;
- 返回 response 给 client;
- 修改无法重置的 process state。
数据库 sequence 自身也是 non-transactional:失败 attempt 可能消耗 ID。业务必须允许 identity gap,不能把连续 ID 当作无失败证据。
本章的 40001 注入器正是利用 sequence 不回滚:
最终是 order 1200002,库存只减 1,outbox 只多 1,metric retry=1。测试的是完整 retry 关系,不是生产中用函数制造错误。
下单 transaction
逻辑顺序:
任何 domain error 都 rollback,包括:
所以 failed request 不留下一个 response=NULL 的 committed ledger。
支付 transaction
数据库还用:
保护“一单一笔 captured payment”。应用的 state/amount 检查提供领域错误;unique 是竞态或其他 writer 下的最终护栏。
defer tx.Rollback() 不是完整策略
常见 Go pattern:
它可以作为防漏,但仍要回答:
- commit error 如何分类;
- canceled ctx 下 rollback 用什么 context;
- connection 是否仍可复用;
- callback error 与 rollback error 哪个保留;
- retry 前连接何时 release;
- panic 如何处理;
- max attempts 与 backoff;
- unknown commit 如何用幂等协议恢复。
一个 helper 减少 boilerplate,不会自动赋予正确业务语义。
12.3.3 健康检查不等于业务可用
三种不同问题
| Probe | 问题 | 是否访问 DB | 失败动作 |
|---|---|---|---|
| liveness | process/event loop 是否活着 | no | restart |
| readiness | 是否应接收新流量 | yes, bounded | remove from routing |
| business synthetic | 核心功能是否成立 | controlled | alert/release gate |
把 database SELECT 1 放进 liveness,会在数据库短暂不可用或 pool saturated 时重启所有 app,制造 reconnect storm。进程本身没有坏,重启只会放大事故。
Readiness 验证依赖身份
SELECT 1 在这些错误目标也会成功:
本章 readiness 返回:
公开生产 API 不一定暴露这些字段;可以只在内部 probe 网络保留或转成 metric。验证逻辑本身必须存在。
Readiness 也需要限流与预算
如果 100 pods 每秒 probe 10 次,每次新建连接,健康检查本身就会成为故障。应:
- 复用 app pool;
- 使用短 context;
- 查询轻量稳定 marker;
- 合理 probe interval 与 failure threshold;
- 不在 probe 中运行 migration;
- 区分 startup 较长初始化与 steady-state readiness;
- 观察 probe 对 pool 的贡献。
本章 readiness 的内部预算为 150 ms。池耗尽时它返回 503,不等一个 1 秒 holder 释放;liveness 同时立即 200。
Ready 不等于核心业务成功
readiness 证明:
它不证明:
- order constraints 与 grants 全部正确;
- idempotency 能处理重复;
- outbox 能提交;
- PgBouncer prepared statement 组合正确;
- failover 后 in-flight retry 安全;
- tail latency 达标。
这些由 deployment smoke、fault matrix、持续 SLI 与 synthetic transaction 覆盖。本章 run_service_lab.py 是发布 gate,不应以每秒频率运行;它会真实写入隔离 fixture。
实测连接指标
第二轮全量证据结束时:
acquire_total=24 会随 probe 和测试步骤改变,不是 golden。关系断言是:
本节检查表
- pool 构造后显式验证首次连接;
- app replica × MaxConns 纳入全局预算;
- app pool 与 PgBouncer server pool 分层计算;
- request context 传给 Acquire/Begin/Exec/Query/Commit;
- pool wait 与 SQL execution 有不同 timeout/metric;
- database timeout 早于 outer deadline并留 cleanup headroom;
- cancel 后 active worker 与 transaction 清零;
- retry 只处理列明 SQLSTATE;
- 每次 retry 从
BEGIN前开始; - callback 没有非幂等外部副作用;
- unknown commit 用 idempotency key 恢复;
- liveness 不访问 DB;
- readiness 验证 identity、writable 与 schema contract;
- synthetic business check 不被当成高频 probe。
参考资料
- pgx v5.10.0:pgxpool
- PostgreSQL 18:Transaction Isolation
- PostgreSQL 18:Error Codes
- PostgreSQL 18:Client Connection Defaults
上一节:为服务设计查询接口 · 返回本章目录 · 下一节:会话状态与连接池陷阱 · 查看全书目录 · 查看索引中心