跳到正文

运维

PostgreSQL 故障切换手册

PostgreSQL 故障切换期间为 go-sequent 事件库保持单写者:路由规则、六步清单、应用侧建议,以及库不做的事。

PostgreSQL 故障切换手册

目标

  • 事件库使用单写者主库,不出现静默脑裂。
  • 使用同步提交或 quorum 时,RPO 接近 0。这是运维层面的选择。
  • 只有在可以接受异步延迟的地方,读模型查询才使用从库。

各条路径应连接哪里

路径 建议
命令 + SyncInTxn(写事务内同步更新读模型)Projector 始终连主库。余额需要提交后立刻可读。
异步 Outbox 消费者(事务内记待发消息,事后轮询投递) 主库,或对非账本视图可容忍延迟的从库。
Replay 与重建 worker 主库,或具有一致性快照的专用重建角色。

使用 Patroni、Cloud SQL HA、RDS Multi-AZ 或同类方案。前面放 PgBouncer 或云上的连接池,以吸收连接风暴。

切换清单

  1. 检测主库丢失:通过 Patroni、云健康检查或心跳缺失。
  2. 提升一个同步备库,确认新主库上 pg_is_in_recovery() 返回 false。
  3. 重新指向连接池或 DSN,或更新服务发现。如果 DSN 被缓存,对应用 Pod 做滚动重启。
  4. 验证写入:执行一次 go-sequent 追加和一次 SyncInTxn Projector 更新。
  5. 验证没有双写者:旧主库不得再接受写入。使用 fencing 或 STONITH。
  6. 重建备库:从新主库重建,并观察复制延迟。

应用侧建议

  • 保持数据库事务简短。一次 CommandService.Execute 批次就是一个事务。
  • 遇到 ErrOptimisticConcurrency(OCC:用 ExpectedVersion 挡住并发写冲突)时带退避重试。切换后的追赶阶段这类错误很常见。
  • 不要跨 goroutine 共享 UnitOfWork。
  • 只有当该探针不需要 SyncInTxn 路径时,健康检查才应降级为只读。
for attempt := 0; attempt < 5; attempt++ {
    err := app.Service.Execute(ctx, cmd)
    if !errors.Is(err, eventstore.ErrOptimisticConcurrency) {
        return err
    }
    time.Sleep(backoff(attempt))
}

当每条命令都带 Idempotency 键(同一业务键重复提交只生效一次)时,重试是安全的,见 Idempotency。

go-sequent 不做的事

  • 没有多主事件库,也没有 CRDT 合并。
  • 库内没有自动 DSN 故障切换,请依赖连接池或平台。

相关

文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。