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 reviewcapture 先读取执行主机身份、文件系统与二进制版本,校验确为相邻的 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这不是纸面命令,也不是“旧目录还在”的推断,而是一次真实启动与查询。它成立有三个必要 前提:
- 使用 copy 模式,旧目录没有与新目录共享或交换文件;
- 新集群还没有接受任何独占写入;
- 应用、连接池和路由仍被写围栏挡在外面。
若用 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 reviewvalidator 不只检查成功字段。正式 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 章将把这种门禁和状态机带入更不友好的情形:系统已经发生故障时,怎样先止血、 再取证、恢复服务并留下可复盘的事件时间线。
上一节:用隔离环境完成升级彩排 · 返回本章目录 · 下一章:事件分级、现场保护与应急决策——枕戈待旦 · 查看全书目录 · 查看索引中心