跳至内容
12.3 Go 服务中的连接与事务

12.3 Go 服务中的连接与事务

driver 把 SQL 发给 PostgreSQL;它不会替你决定连接预算、deadline、事务重试和健康语义。真正的服务可靠性来自这些边界能否闭合:

request context
  bounds pool acquisition
  bounds SQL execution
  triggers cancellation

transaction wrapper
  owns begin/commit/rollback
  retries only a complete safe unit

pool configuration
  fits the global connection budget
  exposes wait and cancellation evidence

12.3.1 连接池大小、超时与上下文取消

NewWithConfig 不等于已连接

pgxpool 的构造可以在没有建立连接时返回。启动 gate 必须主动 PingAcquire

config, err := pgxpool.ParseConfig(databaseURL)
if err != nil {
    return nil, err
}

config.MaxConns = maxConns
config.MinConns = 0
config.MinIdleConns = minIdleConns
config.ConnConfig.DefaultQueryExecMode =
    pgx.QueryExecModeExec
config.ConnConfig.RuntimeParams["application_name"] =
    "pg36-ch12-api"

pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
    return nil, err
}

pingCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
if err := pool.Ping(pingCtx); err != nil {
    pool.Close()
    return nil, err
}

这只能证明 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 后还要分两层:

application pgxpool MaxConns
  = one process's concurrent client connections to PgBouncer

PgBouncer pool_size/reserve/connlimit
  = server connections PgBouncer may open to PostgreSQL

不要把“每个 pod 50 × 100 pods”直接当成 PostgreSQL 5000 条 backend,也不要因此完全忽略 app pool。前一层决定本进程排队、fd 和 PgBouncer client 压力;后一层决定数据库真实并发。两层都过大只会把拥塞从一个队列搬到另一个。

池大小不是 CPU 数量公式

需要从 workload 推导:

concurrency in DB
≈ request rate × time actually holding a DB connection

然后用负载实验观察:

  • 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 后:

database workers observed=2
pool canceled acquires=2
liveness=200
readiness=503
business request=503
release → readiness=200

它证明排队与健康语义,不测容量。

Deadline 要形成预算瀑布

不同 timeout 回答不同问题:

client deadline
  total willingness to wait

HTTP/server request deadline
  service execution budget

pool acquire deadline
  queue budget before any SQL starts

statement_timeout
  PostgreSQL statement execution budget

lock_timeout
  one lock acquisition wait budget

transaction timeout / idle-in-transaction timeout
  transaction lifecycle guard

常见设计是让数据库 timeout 早于最外层 deadline,给 rollback、错误映射和响应留出时间:

client 1500 ms
service 1200 ms
pool acquire 150 ms
statement 900 ms
lock 100 ms where appropriate
response/cleanup headroom

数字必须来自 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 离开后要验证整条链:

request context canceled
  → pgx sends cancel / stops waiting
  → PostgreSQL worker leaves active query
  → connection is reusable or discarded safely
  → transaction rolls back
  → no committed business delta

实验:

GET /debug/hold?ms=2000
client transport timeout=400 ms
active worker observed=1
active worker after cancel=0
structured log=client_cancelled / 499

499 是内部 observability 分类,不是 PostgreSQL 或 HTTP 标准必须返回的业务 contract;客户端已经断开,通常收不到该响应。

连接错误与查询错误要分开

pool.Acquire(ctx) 处失败,SQL 还没开始。本章映射:

503 pool_unavailable

取得连接后 statement_timeout

504 database_timeout
SQLSTATE=57014

这一区分能快速回答“请求慢在应用 pool,还是慢在 PostgreSQL”。若只记一个 db_error_total,诊断又退回猜测。

12.3.2 事务函数、失败重试与外部副作用

Wrapper 必须拥有完整 transaction lifecycle

本章 wrapper 每次 attempt:

Acquire
BEGIN with explicit options
run complete callback
COMMIT
on failure ROLLBACK with bounded cleanup context
Release
classify error
retry or return

不能只重试最后一条 SQL:

BEGIN
  read A
  compute decision
  update B → 40001
  retry update B only    ← decision used a dead snapshot

PostgreSQL 对 serialization failure 的要求是 abort 并从 transaction 起点重新执行。40P01 也使 transaction 失败;若选择重试,同样要重放完整业务单元。

Retry allowlist

本章只允许:

40001 serialization_failure
40P01 deadlock_detected

最多三次 attempt,attempt 之间有限 backoff,并受 request context 约束。以下错误不应被 wrapper 盲重试:

error原因
23505 / 23514通常是确定性业务/contract 冲突
42501权限发布缺陷
42P01 / 42703schema/version 缺陷
57014预算已耗尽或显式取消
unknown connection loss after COMMIT sentcommit outcome 可能未知

连接断开尤其危险:

client did not receive COMMIT response

不等于:

database did not commit

这就是 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 不回滚:

attempt 1:
  nextval=1
  raise 40001
  transaction rolls back

attempt 2:
  nextval=2
  continue
  order commits once

最终是 order 1200002,库存只减 1,outbox 只多 1,metric retry=1。测试的是完整 retry 关系,不是生产中用函数制造错误。

下单 transaction

逻辑顺序:

BEGIN
  optional lab fault before business writes
  claim request key or read existing response
  atomic inventory UPDATE ... RETURNING
  INSERT order
  INSERT item
  INSERT outbox order.placed
  persist idempotent response
COMMIT

任何 domain error 都 rollback,包括:

insufficient inventory
missing SKU
same key + different fingerprint

所以 failed request 不留下一个 response=NULL 的 committed ledger。

支付 transaction

BEGIN
  claim payment key or return existing response
  SELECT order FOR UPDATE
  require state=placed
  require amount=order total
  INSERT one payment
  UPDATE order to paid
  INSERT outbox payment.captured
  persist response
COMMIT

数据库还用:

UNIQUE (payment.order_id)

保护“一单一笔 captured payment”。应用的 state/amount 检查提供领域错误;unique 是竞态或其他 writer 下的最终护栏。

defer tx.Rollback() 不是完整策略

常见 Go pattern:

tx, err := pool.Begin(ctx)
if err != nil { ... }
defer tx.Rollback(ctx)

它可以作为防漏,但仍要回答:

  • 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失败动作
livenessprocess/event loop 是否活着norestart
readiness是否应接收新流量yes, boundedremove from routing
business synthetic核心功能是否成立controlledalert/release gate

把 database SELECT 1 放进 liveness,会在数据库短暂不可用或 pool saturated 时重启所有 app,制造 reconnect storm。进程本身没有坏,重启只会放大事故。

Readiness 验证依赖身份

SELECT 1 在这些错误目标也会成功:

wrong database
wrong user
read-only replica
schema migration incomplete
pool route not intended

本章 readiness 返回:

{
  "status": "ok",
  "database": "pg36_shop",
  "user": "pg36_app",
  "writable": true,
  "schema_ready": true
}

公开生产 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 证明:

can acquire
right identity
writable
schema marker

它不证明:

  • 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。

实测连接指标

第二轮全量证据结束时:

pg36_pool_acquire_total=24
pg36_pool_empty_acquire_total=2
pg36_pool_canceled_acquire_total=2
pool acquired=0
pool idle=2
pool total=2
pool max=2

acquire_total=24 会随 probe 和测试步骤改变,不是 golden。关系断言是:

two holders consume max=2
two waiting acquisitions cancel within their own budget
holders finish successfully
pool returns to acquired=0
readiness recovers

本节检查表

  • 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。

参考资料


上一节:为服务设计查询接口 · 返回本章目录 · 下一节:会话状态与连接池陷阱 · 查看全书目录 · 查看索引中心

最后更新于