跳至内容
30.7 实战:前滚、回退与发布决策

30.7 实战:前滚、回退与发布决策

前六节已经把升级拆成版本决策、数据迁移、扩展、排序规则、验收和 Pigsty 平台动作。 本节再把它们收束为一条可重复的状态机:

preflight
  -> compatibility blocked
  -> compatibility repaired
  -> pg_upgrade --check
  -> pg_upgrade --copy
  -> validate
  -> rollback proof
  -> forward commit
  -> cleanup

实验会在 Pigsty 开发沙箱的 pg-meta 主机上,用 PostgreSQL 17 与 18 的二进制创建两个 Unix-socket-only 私有临时集群。它不会升级 Pigsty 管理的集群,也不会修改 inventory、 Patroni、HAProxy、PgBouncer 或真实路由。目标不是测一个漂亮的停机秒数,而是证明: 不兼容能在发布前阻断,修复顺序有证据,第一笔目标独占写之前可以回退,之后则必须先 对账。

30.7.1 注入一个扩展或排序规则兼容问题

先固定二进制与实验边界

完整边界见 lab-contract.md,主机、版本、对象与验收条件见 requirements.json,状态转移和禁止动作见 upgrade-contract.json,拓扑见 topology.mmd

runner 不联网下载,也不安装系统软件。调用者需要提前准备:

PG36_CH30_OLD_BIN    PostgreSQL 17 bin 目录
PG36_CH30_OLD_SHARE  与该 17 版二进制匹配的 share 目录
PG36_CH30_NEW_BIN    PostgreSQL 18 bin 目录

先做不连接数据库的静态合同检查:

static/labs/ch30/task.sh lint

完整实验只应在已确认的开发沙箱中运行,并使用新的私有证据目录:

export PG36_CH30_OLD_BIN=/path/to/postgresql-17/bin
export PG36_CH30_OLD_SHARE=/path/to/postgresql-17/share
export PG36_CH30_NEW_BIN=/path/to/postgresql-18/bin
export PG36_EVIDENCE_DIR="$(
  mktemp -d "${TMPDIR:-/tmp}/pg36-ch30.XXXXXX"
)"

static/labs/ch30/task.sh all

若要分步评审,可以依次执行:

static/labs/ch30/task.sh capture
static/labs/ch30/task.sh exercise
static/labs/ch30/task.sh verify
static/labs/ch30/task.sh review

capture 先读取执行主机身份、文件系统与二进制版本,校验确为相邻的 17→18 major 升级,并记录可执行文件 SHA-256。exercise 才会在随机 marker 约束的临时目录中 初始化集群;listen_addresses 为空,所有连接都走权限为 0700 的 Unix socket。 实验只有 pg36_upgrade.app.orders 中 10,000 行确定性合成数据。

正式参考 run 使用 PostgreSQL 17.10 和 18.4;minor 号只是那次证据的事实,并不是读者 环境要硬编码的常量。升级前基线为:

rows             10,000
ordered digest   7c1f9a24b7a7ac4aef59e9b482bb1374
data checksums   enabled
custom collation app.en_numeric
dependent index  app.orders_order_code_key

制造一个可控的排序规则失配

为了让阻断逻辑可重复,runner 只在一次性 fixture 中,把 app.en_numeric 对应 pg_collation 行的 collversion 精确改为 pg36-injected-stale。这是教学故障注入,绝不是生产修复方法;修改其他 catalog 行或在托管集群中照做都被合同禁止。

PostgreSQL 读取到的实际 ICU 版本为 153.121,门禁因而得到:

recorded version  pg36-injected-stale
actual version    153.121
mismatch          true
affected index    app.orders_order_code_key
release           blocked

修复必须遵守第 30.4 节建立的顺序:

REINDEX INDEX app.orders_order_code_key;
ALTER COLLATION app.en_numeric REFRESH VERSION;

先重建依赖对象,是让索引按当前排序语义重新物化;后刷新版本,只是承认依赖已经处理。 如果反过来执行,告警可能消失,但旧索引仍可能保留旧排序语义。参考 run 在重建后重新 验证 10,000 行 manifest 与查询结果,确认 mismatch 消失,才允许进入停库阶段。

pg_upgrade --check 拒绝真正不兼容的目标

源集群启用了 data checksums。runner 先故意以 --no-data-checksums 初始化一个 PG18 目标,再用计划采用的 PG18 pg_upgrade 二进制执行检查:

return code  1
reason       old cluster uses data checksums but the new one does not
decision     blocked

失败目标会被精确删除。随后重新初始化 checksums 一致的目标,pg_upgrade --check 通过,正式执行:

pg_upgrade --copy ...

实验固定使用默认 copy 模式,禁止 link、clone、copy-file-range、swap 与 --no-sync。 copy 会多占磁盘和复制时间,但旧集群文件保持独立,适合演示“新集群尚未写入前”的软件 回退。pg_upgrade 生成的 delete_old_cluster.sh 被记录但从不执行。

30.7.2 在业务写入恢复前验证回退路径

先证明升级结果,不急着开放写入

升级完成不等于可以切流。runner 启动 PG18 后,依次验证:

  • 数据库、schema、table、index 与 extension manifest;
  • 10,000 行的有序摘要和业务查询结果;
  • 两个 B-tree 索引的 amcheck
  • 升级后统计信息补采与 ANALYZE
  • 排序规则版本不再失配;
  • 临时实例仍只监听私有 Unix socket。

参考 run 的升级前后摘要完全相等:

source rows / digest  10,000 / 7c1f9a24b7a7ac4aef59e9b482bb1374
target rows / digest  10,000 / 7c1f9a24b7a7ac4aef59e9b482bb1374
extension manifest    equal
query result          equal
amcheck               passed
post-upgrade analyze  completed

前后查询都使用了 index scan,但 validator 没把“执行计划文本必须相同”当成通过条件。 新版本优化器可以合法地选择不同计划;真正要验收的是结果等价、业务时延与资源预算,而 不是把旧计划冻结成正确答案。

在第一笔目标独占写之前实际回退

完成上述验证后,实验仍不向 PG18 写入。它停止新集群,重新启动旧 PG17,再用旧端读取 同一 manifest:

new cluster stopped                 true
old cluster restarted               true
target-only writes before proof     0
old manifest equals baseline        true
rollback proven                     true

这不是纸面命令,也不是“旧目录还在”的推断,而是一次真实启动与查询。它成立有三个必要 前提:

  1. 使用 copy 模式,旧目录没有与新目录共享或交换文件;
  2. 新集群还没有接受任何独占写入;
  3. 应用、连接池和路由仍被写围栏挡在外面。

若用 link 模式,新集群一旦启动就可能修改旧集群共用的数据页,官方明确警告旧集群此后 不再安全;swap 更会交换文件,不能套用本节回退步骤。clone 是否可回退还取决于文件系统 和后续写入边界,也不能只凭模式名假设。

第一笔新写入改变了问题

回退证明完成后,runner 再次停止 PG17、启动 PG18,并提交一条明确 canary:

order_id                         10001
target-only canary rows              1
final target rows               10001
direct rollback remains safe    false

从这一刻开始,旧 PG17 不包含 order_id = 10001。若立即把路由切回旧集群,数据会在 用户视角消失;真实系统还可能已经发送邮件、扣款或调用外部服务。此后的“回退”不再只是 启动旧二进制,而是数据迁移和业务补偿:

停止或围住新写入
  -> 确定目标独占提交边界
  -> 反向同步或逐项对账
  -> 补偿外部副作用
  -> 再次校验
  -> 才能决定回旧,或继续前滚

因此升级 runbook 必须把两种回退写成不同状态:

状态新版本独占写可以采取的动作
验证窗口0停新、启旧、验证旧端后恢复旧路由
已恢复写入> 0 或未知先写围栏与对账;默认优先修复后前滚

所谓“回退窗口”不是维护开始后的固定分钟数,而是第一笔无法在旧端重现的提交之前。 时间阈值仍然有用,但它不能替代数据边界。

30.7.3 输出升级 runbook、决策门和观察窗口

证据包必须能识别“看起来成功”

私有证据目录保存:

preflight-evidence.json
remote/upgrade-evidence.json
remote/pg_upgrade-check-bad.log
remote/pg_upgrade-check-good.log
remote/pg_upgrade.log
remote/rollback-evidence.json
remote/cleanup-evidence.json
negative-report.json
validation-report.json
public-summary.json
review.txt
source file hashes

公开参考摘要在 upgrade-run.json。它保留版本、状态和验收结论, 但删除本地路径、连接信息与私有原始日志。对已完成的同一个证据包,可重复执行:

static/labs/ch30/task.sh verify
static/labs/ch30/task.sh review

validator 不只检查成功字段。正式 run 要求:

30 declared counterexamples rejected
20 live evidence mutants rejected
11 source files hash-bound

也就是说,伪造版本关系、二进制散列、checksum 拒绝原因、collation 依赖、修复顺序、 manifest、amcheck、回退前写入数、forward canary、平台边界或清理结果,都不能继续 得到 pass。

清理不是附属动作

实验结束时逐项证明:

temporary postmasters stopped       true
fixture run root absent             true
remote root absent                  true
unrelated processes terminated         0
system packages changed            false
Pigsty managed data touched        false
Pigsty inventory changed           false
Patroni configuration changed      false
external listener created          false

清理只匹配当前随机 marker 所有的临时路径;它不使用宽泛进程匹配,也不终止无关 postmaster。任一临时实例仍在运行、目录 marker 不符或 Pigsty 平台身份发生变化,整次 run 都失败。

把实验提升为可执行 runbook

生产升级票据至少要有以下字段:

类别必填内容
变更身份change ID、集群、数据库、版本、窗口、指挥人与每个动作 owner
软件清单server/client、OS、驱动、连接池、扩展、动态库、collation provider
数据基线system identifier、checksum、对象/行数/摘要、容量、备份与恢复演练
预检release notes、pg_upgrade --check、扩展升级路径、失效对象、长事务、slot
平台动作Pigsty 配置、服务身份、连接端点、路由、pool drain 与监控 dashboard
状态机每次停写、停库、升级、启库、切流、恢复写入的前置条件与证据
阻断阈值校验失败、延迟、错误率、锁等待、CPU/IO、业务不变量的 stop condition
回退协议第一笔目标独占写边界、旧端启动命令、反向对账和不可补偿副作用
退出条件观察窗口、交接人、旧目录保留期、何时允许执行旧集群清理

执行时不要靠“大家觉得可以了”推进,而要逐门签字:

决策门通过证据不通过动作
软件门目标包、扩展库、驱动与配置已冻结并散列不进窗口
备份门可验证备份且完成目标版本试恢复不停源库
兼容门extension/collation/DDL/catalog 清单无未决项修复或改迁移路线
检查门以正式参数执行的 pg_upgrade --check 通过保持停写,修复后重检
数据门manifest、业务不变量、sequence/identity 一致不切流
完整性门amcheck、checksum 与备份校验各自通过隔离并诊断
应用门驱动、连接池、关键读写与影子流量通过回旧或继续阻断
服务门端点、TLS、HBA、路由和连接 drain 可观测不开放流量
回退门旧端已实际启动,且目标独占写为 0不恢复写入
发布门指挥人确认全部门禁与 owner不切换状态

其中“备份可验证”不等于执行过一次 pg_verifybackup;它还要包含新环境中的恢复、启动 和业务读取。amcheck、data checksum 与备份恢复也分别回答逻辑结构、存储页和灾难 恢复问题,不能互相替代。

观察窗口要有阈值和退出动作

切流后建议按三层观察:

0–15 min   连接失败、认证、panic/crash、错误率、关键写入、锁与复制异常
15–60 min  p95/p99、CPU/IO、cache、autovacuum、长事务、队列和业务漏斗
1 个业务周期以上  batch、报表、备份、归档、故障转移与低频路径

具体时长必须由业务周期决定。每一项都要写基线、阈值、查询或 dashboard、owner 和超阈值 动作。例如“观察延迟”不够,至少应写成:

signal       checkout p99
baseline     previous seven comparable periods
threshold    > baseline × 1.5 for 5 consecutive minutes
owner        application on-call
action       keep write fence / forward fix / invoke reconciliation plan

参考沙箱最终得到的是:

isolated-pg17-to-pg18-state-machine-demonstrated
production_ch30_gate = pending

它证明升级合同可执行,却没有证明真实扩展 ABI、生产数据量、停机预算、HA 拓扑、应用 驱动、备份恢复和业务峰值。只有这些生产证据补齐并由 owner 批准,pending 才能转为 可发布。

第 31 章将把这种门禁和状态机带入更不友好的情形:系统已经发生故障时,怎样先止血、 再取证、恢复服务并留下可复盘的事件时间线。


上一节:用隔离环境完成升级彩排 · 返回本章目录 · 下一章:事件分级、现场保护与应急决策——枕戈待旦 · 查看全书目录 · 查看索引中心

最后更新于