推陈出新:版本升级与回滚策略
30 推陈出新:版本升级与回滚策略
第 29 章已经把逻辑复制、全量校验、切流和回退组织成迁移状态机。版本升级是在这条主线 上再增加五个同时变化的维度:
因此,升级不是“换个 RPM/DEB 再重启”,也不是 pg_upgrade 返回成功就结束。它是一项
有兼容性清单、隔离彩排、业务基线、发布门、写入分界和退出窗口的迁移项目。
学习完成标准
完成本章后,读者应能:
- 区分 minor、安全修复和 major upgrade,并从 release notes 提取行为变化;
- 在
pg_upgrade、逻辑复制和 dump/restore 之间按停机、空间、回退与重建目标选型; - 盘点扩展的包、动态库、SQL 对象、preload 和 update path;
- 发现 collation version 漂移,并坚持先重建依赖对象、再刷新版本;
- 用 catalog、checksum、
amcheck、恢复证据、查询结果和业务不变量建立升级基线; - 在隔离环境复现完整升级,并把软件准备时间与业务不可写时间分开;
- 在目标第一笔独占写入前证明回退,在其后切换为对账或前滚策略;
- 输出含停止线、观察窗口、owner 与证据包的生产升级 runbook。
一张图看懂升级决策
任一门禁失败,都回到前一个可解释状态;不能用“先上线再看看”跨过 checksum、 collation、扩展或业务结果不一致。
本章目录
30.1 先识别变化类型
30.2 三类大版本升级路径
30.3 扩展与依赖升级
30.4 locale、collation 与索引风险
30.5 升级前检查与业务验证
- 30.5.1 系统目录、无效对象与长事务
- 30.5.2
amcheck、checksum 状态与备份恢复证据 - 30.5.3 查询结果、计划、性能和业务不变量基线
- 30.5.4
amcheck与 checksum 检测对象不同
30.6 用隔离环境完成升级彩排
30.7 实战:前滚、回退与发布决策
写作与验收提示
本章提供一个真实、隔离的 PostgreSQL 17.10→18.6 参考实验。它在 Pigsty
pg-meta 主机的随机 /tmp 目录中创建两套 Unix-socket-only 临时集群,不接触
Pigsty 管理的数据目录与服务。正式证据证明:
验证器拒绝了 30 个声明反例和 20 个现场证据变异。公开摘要位于
upgrade-run.json,完整实验合同位于
lab-contract.md。
这次小型沙箱成功不预测生产停机时长,也不证明第三方扩展、驱动、备份体系或真实应用
已经兼容。production_ch30_gate 始终保持 pending;生产授权必须来自真实数据克隆、
业务彩排、备份恢复与变更审批。
参考资料
- PostgreSQL 18:升级 PostgreSQL 集群
- PostgreSQL 18:
pg_upgrade - PostgreSQL 18:逻辑复制集群升级
- PostgreSQL 18:
ALTER COLLATION - PostgreSQL 18:
pg_verifybackup - PostgreSQL 18:版本 18 发行说明
- Pigsty v4.5:PostgreSQL 大、小版本升级
- Pigsty v4.5:数据迁移
上一章:移花接木:逻辑复制、迁移与异构同步 · 返回下卷导读 · 下一章:事件分级、现场保护与应急决策——枕戈待旦 · 查看全书目录 · 查看索引中心
30.1 先识别变化类型
升级风险首先取决于“什么发生了变化”。同一个“版本升级”工单,可能只是在同一 major 里换修复版,也可能跨越 data directory、系统目录、SQL 行为和扩展 ABI。若不先分类, 团队很容易把 minor upgrade 做成一次不必要的数据迁移,或把 major upgrade 当成滚动 重启。
30.1.1 小版本、安全修复与大版本
版本号先按 PostgreSQL 规则读
从 PostgreSQL 10 开始:
9.6 及更早版本使用前两段表示 major,例如 9.5→9.6 是 major upgrade, 9.6.23→9.6.24 才是 minor upgrade。不要用通用 SemVer 的 “major.minor.patch”直觉解释 PostgreSQL。
社区的版本策略给出两个硬边界:
- 每个 major 通常支持五年,超过 EOL 不再获得正常修复;
- 同一 major 的 minor release 不改变内部存储格式,官方建议运行当前 minor;
- major 会改变不向后兼容的 data directory,需要 dump/restore、
pg_upgrade或逻辑 迁移; - 可以跨多个 major 升级,但必须阅读所有跨越版本的 release notes。
分类表:
| 类型 | 数据目录 | 典型动作 | 仍需验证 |
|---|---|---|---|
| minor 修复 | 同 major 兼容 | 换二进制并重启 | release note、扩展包、HA 逐节点顺序 |
| 紧急安全修复 | 通常仍是 minor | 缩短审批与暴露时间 | CVE 触发面、临时缓解、升级后攻击面 |
| major | 不直接兼容 | 三类迁移路径之一 | 全部数据、行为、扩展、客户端和回退合同 |
| OS/libc/ICU 更新 | PG 版本可不变 | 系统维护 + 对象评估 | collation、TLS、动态库、驱动 |
| 扩展更新 | PG 版本可不变 | 包 + ALTER EXTENSION |
update path、对象重建、不可降级 |
“安全修复”不是第四种存储格式;它是变更优先级。风险高的漏洞可能要求更快上线,但不能 免除备份、逐节点、兼容和回退检查。反过来,社区明确认为长期停留在旧 minor 往往比及时 升级风险更大。
Pigsty 中的 minor rolling 仍有顺序
同一 major 的 HA 集群通常可以:
Pigsty 提供包仓、Ansible、Patroni 与 pg 管理命令来执行这条链,但“rolling”不等于
所有节点同时更新。每一步都应确认:
并观察 replica replay、slot、客户端错误与业务 SLO。扩展 .so 必须与每个节点正在
运行的 server binary 匹配;只升级 primary 的扩展包,下一次 failover 可能把故障推给
replica。
major 不能靠“replica 先装 18、primary 仍跑 17”完成物理滚动升级。物理流复制要求同一 major;跨 major 需要逻辑复制或重建后的拓扑。
30.1.2 SQL 行为、系统目录、参数和默认值
release notes 要转成可执行差异
不要只读“新特性亮点”。major release notes 的 migration/incompatibilities 部分才是 升级清单的起点。以 17→18 为例,变化包括但不限于:
initdb默认启用 data checksums;pg_upgrade开始保留大部分优化器统计,但 extended/custom/cumulative statistics 仍不完整;- generated column 默认形态、时区缩写解析、
VACUUM/ANALYZE对继承子表的行为变化; - 旧
psql对 PostgreSQL 18 的某些 CSV\copy边界可能不兼容; - 某些系统视图列或含义发生变化;
- 默认 collation provider 相关行为会影响全文检索与
pg_trgm索引。
本章实验正是利用第一条真实边界:源 PG17 启用了 checksums,而一个用
--no-data-checksums 初始化的 PG18 目标被 pg_upgrade --check 拒绝:
这说明“新版本的默认值更安全”不能替代显式匹配。目标必须按源集群和新架构的合同 初始化。
四张差异表
升级 ADR 至少维护:
| 表 | 典型内容 | 证据 |
|---|---|---|
| removed/changed behavior | SQL、类型、权限、触发器、排序、错误码 | release notes + query corpus |
| parameter diff | 删除、重命名、默认值、单位、context | pg_settings、新旧配置渲染 |
| catalog diff | 列、view、enum/code、权限 | exporter/脚本 SQL 的兼容测试 |
| reserved words/API | parser、函数签名、客户端协议 | schema restore + driver test |
在旧环境导出当前设置时,不要只保存一个手工编辑过的 postgresql.conf:
第一张是最终有效值,第二张是配置文件解析结果。两者配合才能发现:
同样地,HBA 要用 pg_hba_file_rules 解析,不能只做文本 diff。
系统目录不是跨 major 的稳定应用 API
监控、迁移脚本和内部工具经常直接读取 pg_stat_*、pg_catalog。这些是 PostgreSQL
公开能力,但列集合和含义可以随 major 演进。避免:
应显式列名,并以 server_version_num 选择经过测试的查询:
升级前在新版本空集群上先跑完整 exporter、备份、健康检查和自动化 SQL。目标不是 “SQL 不报错”而是输出字段、单位和告警语义仍正确。
30.1.3 驱动、连接池、备份与观察组件兼容
兼容矩阵的单位是“实际组合”
不要写“JDBC 支持 PostgreSQL”。应记录:
驱动测试至少覆盖:
- 认证、TLS、channel binding、证书和密码轮换;
- 参数类型推断、binary/text 编解码、timestamp、numeric、array、JSON、large object;
- server-side prepare、statement cache、batch、COPY;
- error SQLSTATE、generated keys、cancel、timeout;
- failover、连接重建、transaction status 和 read-only 标志。
PgBouncer/连接池还要覆盖 session state。升级切换时保留的旧连接不会自动变成新版本
连接;prepared statement、临时表、SET、advisory lock 和 transaction pooling 的语义
需要按实际模式测试。发布证据必须包含“新建连接看到目标 system identifier”,而不只
看 pool 端口可达。
备份工具要同时验证生产与恢复端
逻辑迁移通常应使用目标版本的 pg_dump 读取旧 server。官方说明:新版
pg_dump 可以读取受支持的旧 server,且输出面向相同或更高 server;旧版 pg_dump
会拒绝读取更高 major,dump 输出也不保证能恢复到更低 major。
这带来几条门禁:
物理备份则与 server major、WAL、control file 和工具版本绑定。不能把 PG17 的物理备份 直接恢复成 PG18;它应先恢复为 PG17,再走获批的升级路径。Pigsty 中 pgBackRest 配置、 repository、stanza、retention 和恢复脚本都要在新拓扑重验,而不是只确认最新 backup 状态是 completed。
“监控没报警”可能是 exporter 已失明
升级后指标归零有两种解释:
因此观察组件要做三向核对:
| 组件 | 新版本检查 | 原生对照 |
|---|---|---|
| exporter | scrape success、query errors、字段数 | pg_stat_* 原始 SQL |
| Grafana/Pigsty 面板 | 时间序列连续、label 未漂移 | server identity 与真实 workload |
| log pipeline | 新错误格式、severity、采集延迟 | PostgreSQL 本地 log |
| alert rule | 正例能触发、恢复能关闭 | 人工受控 canary |
升级测试应主动产生一条查询、一个可识别错误和一小段 WAL,确认应用、日志、指标和告警 四条链都看到同一时间线。只有全部组件对新 major 给出有意义的数据,才叫“观察能力 兼容”。
返回本章目录 · 下一节:三类大版本升级路径 · 查看全书目录 · 查看索引中心
30.2 三类大版本升级路径
大版本升级没有统一最优解。路径选择本质上是在四种成本之间交换:
先写约束,再选工具;不能因为团队最熟 pg_upgrade,就把所有数据库都压进一个停机
窗口。
30.2.1 pg_upgrade 与停机窗口
它升级 cluster,不是逐条重写业务数据
pg_upgrade 用新版本创建系统目录,再迁移旧 cluster 的 catalog 与用户 relation 文件,
因此通常比逻辑 dump/restore 快得多。它要求:
- old/new server binaries 与 data/config directories 都可用;
- compile-time 与 control-data 条件兼容;
- 新 major 对应的扩展 shared libraries 已安装;
- 两个 cluster 在正式 upgrade 时都停止;
- 操作系统用户/数据库 install user、认证和 socket 可连接;
- tablespace、standby、slot、统计与生成的 rebuild scripts 都有计划。
永远运行新版本的 pg_upgrade:
--check 应在彩排和正式窗口前重复执行。若计划使用特定传输模式,检查时也要带相同
模式,才能覆盖文件系统约束。
文件传输模式决定速度和回退形态
| 模式 | 空间/速度 | old cluster 可回退边界 |
|---|---|---|
default --copy |
需要完整副本,较慢 | old relation files 独立,新端启动后仍可启动旧端 |
--copy-file-range |
可能利用高效内核复制 | 取决于实际文件系统行为,仍需实测 |
--clone |
CoW 文件系统上快且省空间 | 新旧逻辑独立,但要求文件系统支持 |
--link |
硬链接,最快且省空间 | 新端一旦启动写共享文件,旧端不再安全 |
--swap |
可能更快,直接交换目录内容 | 传输进入破坏阶段后旧端不再安全 |
不能把 --link 的分钟级结果与 --copy 的回退承诺同时写进 runbook。选择哪个模式,
就必须演练哪个模式、同一种文件系统、同样 tablespace 布局与数据规模。
本章为了验证“目标零写入时旧端可启动”,明确使用 --copy,禁止 link、clone、swap
和 --no-sync。升级完成后新 PG18 已启动并校验,随后停止新端、启动旧 PG17,
10,000 行 manifest 仍相等。这个结论只属于 copy 模式和本次 fixture。
停机窗口不只是一条命令的秒数
业务不可写窗口通常包含:
pg_upgrade 输出的 Upgrade Complete 只是中点。它会提示需运行的 rebuild/reindex
脚本;被这些脚本引用的表在完成前可能返回错误结果或性能极差。PostgreSQL 18 会迁移
大部分优化器统计,但 extended、扩展自定义和累计统计并不完整,仍建议:
彩排要分别计时每一阶段,以生产数据克隆的 P95/P99 结果安排窗口,不能拿一个 10,000
行实验的 pg_upgrade 时间乘比例。
30.2.2 逻辑复制与渐进切换
逻辑复制把新 major 建成独立 cluster:
它的优点:
- 业务不可写时间主要集中在最终冻结与切流;
- 新旧环境可同时运行,便于真实应用影子验证;
- 可以跨平台、改变物理布局、重配参数和扩展;
- 目标可逐步扩容与预热。
代价已经在第 29 章验证:
- DDL、sequence、large object 与许多对象不自动复制;
- publication/subscription、replica identity、slot WAL 必须治理;
- 目标写入会制造冲突或 silent drift;
- 全量、增量、业务不变量和切流路由都需独立证据;
- 回退在目标承接写入后需要 reverse sync 或 reconciliation。
跨 major 还要检查 row/column filter、generated columns、binary transfer 与协议选项在 两个版本交集中的语义。一般先升级 subscriber/target 能扩大兼容余量,但具体拓扑 需按官方 logical replication upgrade 章节执行。
PostgreSQL 17 起,pg_upgrade 可以在满足前提时迁移 logical slot/subscription
依赖;这不意味着任意逻辑复制拓扑能自动升级。publisher upgrade 前需要停用对应
subscription,循环、多节点和 two-phase 拓扑有非事务步骤,必须有备份和逐节点状态机。
Pigsty 推荐生产 major upgrade 优先考虑新建集群 + 逻辑迁移,因为它把应用验证和资源 准备移到切换前。这里的“推荐”仍需服从数据对象是否可逻辑复制、写入率、slot 容量、 DDL 频率和团队能否可靠完成对账。
30.2.3 dump/restore 与重建机会
dump/restore 是最“逻辑化”的升级:在新 cluster 按新版本规则重新创建对象和写入行。 它成本高,却也是一次摆脱历史物理包袱的机会:
- 重排 tablespace、partition 与对象 owner;
- 只迁移仍需保留的数据;
- 统一 encoding/locale 的新建策略;
- 重建全部 index,消除旧物理布局;
- 审阅 schema、extension、privilege 和废弃对象;
- 用 directory archive 并行 dump/restore。
常见路径:
选择性恢复不是“完整 cluster 迁移”的同义词。需要另外处理:
使用 target major 的 pg_dump,并读取 stderr 全部 warning。directory 格式是唯一支持
parallel dump 的 archive;custom/directory 都支持选择与重排 restore。并行增加
jobs + 1 个连接和源端负载,还可能因 DDL lock 排队而失败,必须在生产形态彩排。
dump/restore 的回退与逻辑迁移相似:旧端可继续保留,但目标开始承接独占写入后,改回 连接串仍会丢新事实。它不是因为“重建了一份”就天然可逆。
30.2.4 没有一种路径天然“滚动无感”
用约束选型
| 约束 | pg_upgrade |
logical replication | dump/restore |
|---|---|---|---|
| 最小写停机 | 较弱 | 强 | 较弱 |
| 额外硬件 | 可同机 | 通常需要完整新集群 | 通常需要完整新集群 |
| 升级速度 | 通常最快 | 取决于全量 + 追平 | 通常最慢 |
| 真实影子验证 | 有限 | 最强 | 恢复完成后可做 |
| 改物理布局 | 有限 | 强 | 最强 |
| DDL/sequence 编排 | 中 | 最复杂 | restore 负责大部分 |
| 旧端物理回退 | 取决于 copy/link/swap | 切流前强 | 切流前强 |
| 数据对账 | 必需 | 最重 | 必需 |
一个常见组合不是三选一,而是:
甚至同一项目会先用 dump/restore 生成测试克隆,再用 pg_upgrade 彩排正式路径。
“无感”要拆成多个 SLO
所谓无感至少包含:
逻辑切换只有几秒写冻结,不代表旧池里的长事务、prepared statement、sequence、
缓存、ETL 与外部副作用无感。minor rolling 也会发生连接断开和 failover。pg_upgrade
即使文件迁移很快,post-upgrade reindex/ANALYZE 仍可能控制上线时间。
所以 runbook 不写“无感升级”,而写:
可测量的预算才是发布决策;“滚动”“在线”“秒级”只是实现特征。
上一节:先识别变化类型 · 返回本章目录 · 下一节:扩展与依赖升级 · 查看全书目录 · 查看索引中心
30.3 扩展与依赖升级
PostgreSQL 本体升级成功,扩展仍可能让新 cluster 无法启动、schema restore 失败,或更 隐蔽地在旧类型/索引上执行新二进制代码。原因是扩展不是一个对象,而是一条跨越软件 仓库、文件系统、配置、系统目录和业务数据的依赖链。
30.3.1 二进制包、数据库对象和预加载顺序
一项扩展至少有五层
| 层 | 示例 | 升级问题 |
|---|---|---|
| OS/package | postgresql-18-postgis-*、.so |
是否有目标 major/架构/OS 包 |
| control/SQL files | .control、--1.0--1.1.sql |
default version 与 update path |
| preload/config | shared_preload_libraries、GUC |
新 server 能否启动、是否需 restart |
| database catalog | pg_extension.extversion、members |
每个 database 当前装了什么 |
| stored data/index | extension type、operator class、index | 新代码能否解释旧物理表示 |
pg_extension 只回答第四层:
还要查询目标 server 实际能提供的版本:
“源端安装了 3.4,目标仓库有 3.5”并不能证明可直接升级;中间 SQL update scripts 可能缺失,C ABI 也可能不支持目标 major。
pg_upgrade 前先装文件,不要重建 SQL 对象
官方 pg_upgrade 流程要求先在新版本安装匹配的 extension shared libraries 与支持
文件。不要在空的新 cluster 预先执行:
旧 cluster 的 extension catalog 与对象会随升级迁入,提前 CREATE 会重复。正确顺序
通常是:
pg_upgrade 会检查它能发现的 required libraries,也会为可用 extension update 生成
脚本;但官方明确指出外部模块的所有 binary compatibility 无法由它完全检查。包含自定义
background worker、WAL resource manager、shared memory 或特殊 table access method
的扩展,必须按扩展自己的 major-upgrade 文档测试。
preload 是启动依赖
盘点:
shared_preload_libraries 中任一 .so 缺失或与新 server ABI 不匹配,目标可能根本
起不来。先在隔离 cluster 加载:
再实际启动并检查 log。不要在正式窗口里通过“先把所有 preload 清空”绕过;这样启动的 server 可能缺少审计、监控、时序、列存或业务所依赖的语义。
HA 集群中每个候选 primary/replica 都必须有同一套兼容库。只在当前 primary 安装, failover 才会暴露缺包。
30.3.2 扩展升级脚本与不可降级路径
先证明存在完整 update path
目标是看到从当前 installed version 到批准 target version 的路径。然后显式指定:
不带 TO 会使用 control file 的 default version;软件仓库下一次更新 default 后,
同一 runbook 可能得到不同结果。版本和包摘要都应固定。
ALTER EXTENSION UPDATE 执行扩展提供的 SQL scripts。脚本可能:
- 修改 type/function/operator 签名;
- 重写 extension-owned table;
- 替换 operator class 或索引支持函数;
- 迁移内部元数据;
- 删除旧对象;
- 要求先后执行专用 pre/post-upgrade procedure。
即使 SQL transaction 回滚成功,已更换的 .so、preload、外部文件和其他 database
状态也不一定一起回滚。更重要的是,许多扩展根本不提供 downgrade script。
降级能力要逐版本证明
对每个扩展写矩阵:
| 项 | 必答 |
|---|---|
| package downgrade | 仓库是否保留 old major + old extension package |
| SQL downgrade path | pg_extension_update_paths 是否存在反向 path |
| on-disk format | 新版本写入后旧 binary 是否还能读 |
| index rebuild | 哪些 index 必须重建,能否 concurrent |
| logical export | extension type 如何导出为 portable form |
| fallback | 回旧 cluster、restore,还是只能前滚 |
如果没有反向 path,发布决策应写:
不要尝试把 pg_extension.extversion 手工改回旧字符串;它只改 catalog 声明,不会逆转
SQL objects、内部表或存储格式。
扩展脚本也是受信任代码
升级脚本常以 extension owner 或 superuser 权限执行,binary 还能在 server 进程内运行。
包来源、签名/摘要、供应链和发布说明应进入变更评审。对 source superuser 不可信的
cluster,pg_upgrade/restore 还可能在目标执行源端预先布置的代码;隔离环境与代码
审阅不是可选项。
30.3.3 从 ch14 ADR 获取退出与兼容信息
第 14 章要求扩展准入时就记录 extension ADR。升级时不要重新从零猜用途,而应把 ADR 转成当前 inventory:
将 ADR 与现场 catalog 对账:
还要找出 extension members 与业务依赖:
业务对象依赖 extension type/function/operator 的方向还需从 pg_depend 反查。只有列出
stored types、indexes、views、generated expressions 和 functions,才能知道升级失败
影响哪些对象。
退出策略在升级时兑现
ADR 若声明“可移除”,彩排应实际证明:
若做不到,就把 exit cost 和供应商/社区生命周期写进升级风险。一个已停止支持 PG18 的 关键扩展,可能决定整个数据库只能停留在 PG17,或必须先做一次应用层去依赖迁移。
本章正式实验只迁移内建 plpgsql,升级后才安装 amcheck 验证两个 B-tree。它故意
不声称覆盖任何第三方扩展;生产票据必须用第 14 章 ADR 和真实包矩阵补齐这块证据。
上一节:三类大版本升级路径 · 返回本章目录 · 下一节:locale、collation 与索引风险 · 查看全书目录 · 查看索引中心
30.4 locale、collation 与索引风险
collation 决定字符串怎样比较和排序。B-tree 在创建时把这种比较结果固化成页面内顺序; UNIQUE constraint 还依赖它判断两个字符串是否相等。操作系统的 libc、ICU 或 PostgreSQL builtin provider 发生变化后,heap 里的文本没有改变,旧索引却可能已经不再符合新比较 规则。
这是一个典型的“服务能启动、查询也能执行,但可能返回错结果”的升级风险。
30.4.1 libc、ICU 与排序规则版本
locale 是环境选择,collation 是数据库对象
需要分别记录:
在每个 database 中:
对数据库默认 collation,还要检查 pg_database.datcollversion 与
pg_database_collation_actual_version(oid)。一个 cluster 有多个 database,逐库检查
不能省略。
provider 特征:
| provider | 版本来源与边界 |
|---|---|
| libc | 依赖 OS C library/locale data;版本号有时只是近似代理 |
| ICU | ICU 提供版本,跨平台能力强,但升级 ICU 仍可改变规则 |
| builtin | PostgreSQL 内建规则,减少 OS 漂移,但只覆盖其支持的 locale |
| default | 继承 database 默认 provider |
版本字符串相同也不是数学证明。发行版可能 backport locale 数据而没有按预期改变外层 版本;因此还应保留一组关键字符串排序与相等性 corpus。
warning 是门禁,不是自动修复
PostgreSQL 使用 collation 时会比较 recorded 与 actual version。若不一致,会提示:
它不会自动知道所有外部语义,也不会替你安排锁和空间。OS patch、容器基础镜像、
跨发行版迁移与 pg_upgrade 都可能触发这条门禁,所以 collation 检查不应只在
PostgreSQL major upgrade 执行。
30.4.2 排序变化对唯一性和索引顺序的影响
B-tree 的正确性依赖比较函数稳定
假设旧规则认为:
新规则变成:
旧 index page 仍按第一种顺序排列。binary search 使用新 comparator 在旧顺序上导航,
可能漏行、返回错误 range,ORDER BY 也可能错误地相信 index 已有正确顺序。amcheck
文档明确把“索引 tuple 是否按 collation 逻辑顺序”列为 B-tree invariant。
UNIQUE 更危险。若旧规则认为两个值不同,新规则认为相等:
此时不能先刷新版本并继续。需要业务 owner 决定:
- 规范化并合并重复值;
- 改为 deterministic collation;
- 改唯一键设计,加入稳定业务 ID;
- 保留旧 provider/locale,推迟环境变化。
若新规则只改变顺序、不改变相等性,仍需重建所有依赖排序的结构。
受影响的不只普通文本索引
盘点:
hash index 不保存排序,但 collation-sensitive equality 和业务去重仍需按实际 operator 语义验证。不能把“只重建所有 text B-tree”当成对任意扩展 operator class 的完整规则。
query corpus 要覆盖等价类
建议保存:
语料包含 case、accent、组合字符、数字字符串、标点、emoji、各业务语言和空白。比较 旧/新结果时,先区分“批准的排序变化”与“会破坏唯一性/分页的变化”。
30.4.3 识别受影响对象并规划重建
先列依赖,再排动作
官方给出的通用依赖查询:
版本字段可能为 NULL,因此生产查询通常同时使用 IS DISTINCT FROM,再根据 provider
判断哪些 NULL 是预期。对 index 可进一步从 pg_index.indcollation 精确列出:
计划每个对象的:
顺序不能颠倒
正确顺序:
REFRESH VERSION 只把 catalog 中的 recorded version 更新为当前 provider version;
官方明确说明它不会检查对象是否已正确重建。若先 refresh,warning 消失了,旧索引
却还在,反而销毁了最显眼的故障信号。
数据库默认 collation 使用:
也同样必须在所有依赖对象重建后执行。
正式实验的反例
本章 runner 在一次性 PG17 cluster 中精确把:
门禁立即变成 blocked。runner 只允许:
完成后 recorded/actual 都为 153.121,mismatch 回到 false,且 10,000 行 logical
manifest 未改变。catalog update 是为了在可丢弃 fixture 中制造现象,生产中严禁手工
伪造 collversion;真实 mismatch 来自 provider/OS 变化。
上一节:扩展与依赖升级 · 返回本章目录 · 下一节:升级前检查与业务验证 · 查看全书目录 · 查看索引中心
30.5 升级前检查与业务验证
升级会复制或重新解释现有状态。源端已经存在的 invalid index、prepared transaction、 catalog 漂移或数据损坏,不会因为换了 major 自动痊愈;它们只会让故障因果更加难分。
升级前检查的目标不是证明数据库“完美”,而是建立一个已知、可恢复、可比较的起点。
30.5.1 系统目录、无效对象与长事务
第一组:cluster 与 database inventory
随后逐 database 采集 extension、collation、schema、owner、privilege、large object、
publication/subscription、FDW/server/user mapping、database/role settings。pg_upgrade
处理的是整个 cluster,漏掉一个平时不连接的 database,也可能在 schema dump/restore
阶段让全局升级失败。
第二组:不完整与待完成状态
invalid index 可能来自失败的 CREATE INDEX CONCURRENTLY;它是否删除、重建或保留,
要在升级前决定。NOT VALID constraint 可以是有意的渐进变更,但必须进入 inventory。
prepared transaction 是未完成的分布式业务事实,不能在停机时当普通长连接粗暴清掉。
还要检查 logical replication:
PG17+ 的某些 logical slot/subscription 状态可由 pg_upgrade 迁移,但必须满足官方升级
前提;老版本、无效 slot 和复杂拓扑不能靠默认推断。
第三组:活动与变更冻结
正式停机前应:
pg_upgrade --check 还会检查 version、control data、prepared transaction、部分不支持
类型、required libraries、logical slot/subscription 等条件。它是必要门禁,但不覆盖
应用查询与业务不变量。
一个容易遗漏的原生限制是:pg_upgrade 不支持用户列使用若干保存 OID 的 reg*
类型,如 regcollation、regconfig、regproc、regprocedure;regclass、
regrole、regtype 可升级。preflight 应按目标版本文档扫描,而不是等正式窗口报错。
30.5.2 amcheck、checksum 状态与备份恢复证据
在线先做结构检查
对已选重要 B-tree:
bt_index_check 用与实际 index scan 相同的 operator class/comparison 逻辑验证 B-tree
结构与顺序;启用 heapallindexed 还会检查 heap tuple 是否有对应 index tuple。更强的
bt_index_parent_check 检查 parent/child invariant,但锁与成本更高。
大库不能在窗口前临时对所有 index 做最重检查。按:
分层,并在生产克隆中测量耗时。失败时先转入第 35 章的数据抢救流程,不要把损坏对象 直接送进升级。
停库后再做全 cluster checksum 检查
pg_checksums 要求 server clean shutdown;check 会扫描 cluster 文件,发现至少一个
checksum failure 时返回非零。启用/禁用 checksum 更是会修改数据块或 control file,
HA 拓扑必须所有节点一致处理,不能把它顺手塞进 major upgrade。
PostgreSQL 18 initdb 默认开启 checksums,而 pg_upgrade 要求 old/new 设置匹配。
本章实验显式构造:
--check 返回 1;runner 删除这个不兼容的目标,重新以 checksums on 初始化,才允许
继续。
“有备份”必须变成一次可恢复证据
升级票据至少绑定:
pg_verifybackup 可以按 backup manifest 检查文件、摘要和所需 WAL,但官方明确说明它
不能覆盖启动 server 时的全部检查,仍需 test restore。Pigsty/pgBackRest 的
check、backup info 和 repository 健康也不能替代实际恢复。
回退到旧 cluster 依赖旧版本的可恢复性,所以升级前的 restore 应首先恢复为 old major。 升级后还要建立 new major 的新备份基线,并再次恢复;不能无限依赖切换前那份旧版本 备份。
30.5.3 查询结果、计划、性能和业务不变量基线
先比较结果,再讨论计划
一个新 major 选择了不同 plan,不一定是回归;选择了同一 plan,也不证明结果正确。 验证顺序:
- error/SQLSTATE 与行结果;
- 行数、排序、NULL、类型与精度;
- 业务不变量;
- plan shape、估算与实际行数;
- latency、吞吐、CPU、I/O、WAL 与锁。
从 pg_stat_statements、APM 和业务清单建立代表性 query corpus:
参数分布很重要。只用一个平均 customer_id 做 EXPLAIN,无法发现 skew、NULL、热点和
极端时间范围。
保存可比的证据格式
对写 SQL 应在可回滚、隔离的克隆上执行。记录:
不要逐字比较 cost;新版本 cost model、节点和统计可能合理变化。设置门禁:
本章 fixture 查询 order_code = 'order-5000' 在 PG17 和 PG18 都返回同一行,两个版本
都使用 orders_order_code_key Index Scan。实验记录完整 plan JSON,但 validator 只强制
业务结果相等和 plan 存在,不把“必须同一个节点”误写成普遍升级合同。
业务不变量是最终解释层
通用数据摘要之外,还应验证:
这些规则应在 old snapshot 和 upgraded snapshot 上运行相同版本,并保存异常主键范围。 否则应用测试只能告诉你几个请求成功,不能证明整批数据仍满足业务语义。
30.5.4 amcheck 与 checksum 检测对象不同
| 证据 | 主要检测 | 不能证明 |
|---|---|---|
| page checksum | page 从写入后是否发生可检测的物理变化 | SQL 逻辑、所有内存/写入前错误、索引业务一致性 |
pg_checksums --check |
clean-shutdown cluster 中 checksum page 扫描 | 未启用 checksum 的历史、server 可恢复、业务正确 |
amcheck B-tree |
page/link/order、可选 heap/index coverage 与 uniqueness | 所有 access method、业务行值、底层每个文件摘要 |
verify_heapam |
heap 结构异常并尽量继续报告 | 所有 index、备份可恢复 |
| backup manifest verify | 备份文件、摘要、所需 WAL 的一部分完整性 | server 启动和业务恢复 |
| test restore | 备份能在声明环境恢复并启动 | 之后每笔新写、全部应用语义 |
| logical manifest | 行级规范化结果和业务聚合 | 物理 page/index 结构 |
它们不是相互替代关系:
升级发布至少需要这四条独立证据线:
把它们压成一个“数据库健康检查已通过”复选框,会丢掉每项证据真正覆盖的边界。
上一节:locale、collation 与索引风险 · 返回本章目录 · 下一节:用隔离环境完成升级彩排 · 查看全书目录 · 查看索引中心
30.6 用隔离环境完成升级彩排
第一次在真实数据、真实扩展、真实配置和真实运维入口上组合升级,不应发生在生产窗口。 隔离彩排的价值不是练熟命令,而是发现:
30.6.1 克隆数据与版本化配置
克隆必须回答“像生产的哪一部分”
| 克隆方式 | 保真度 | 主要用途 | 风险 |
|---|---|---|---|
| schema + synthetic data | 对象高,分布低 | 快速 pg_upgrade --check、脚本开发 |
无法预测真实时长/plan |
| logical dump/restore | 逻辑对象与行高 | 新 major 兼容、清理物理布局 | 大库慢,需脱敏 |
| physical restore | page/WAL/物理布局高 | pg_upgrade 时长、完整性、恢复 |
敏感数据与存储成本 |
| storage snapshot clone | 速度快、物理高 | 重复彩排 | 一致性、tablespace/WAL 同步边界 |
| production-sized generated corpus | 分布可控 | 性能回归与极端边界 | 难覆盖真实历史异常 |
生产升级至少需要一次真实规模的物理/恢复克隆;日常 CI 可以使用 schema + deterministic fixture。无论哪种,记录:
脱敏不能破坏 join、skew、长度、locale 与唯一性分布,否则兼容测试会得到过于乐观的 结果。隔离网络、独立 secret、禁止邮件/支付/webhook/CDC 等外部副作用同样重要。
配置不是复制旧文件
Pigsty 中应把新 cluster 作为独立 inventory 对象,显式声明:
实际变量名和包 alias 以当前 Pigsty 版本为准。关键原则是:
不要把旧 postgresql.conf、postgresql.auto.conf、pg_hba.conf 原样盖到新版本上。
这样会带入已删除参数、错误 include、旧路径与旧认证边界,还会绕过 Pigsty/Patroni 的
配置 ownership。
需要版本化的输入包括:
- Pigsty inventory commit 与模板/角色版本;
- PostgreSQL/extension/OS package lock 与摘要;
- effective
pg_settings、pg_file_settings、HBA 解析; - locale/ICU/libc/architecture;
- Patroni、pgBackRest、PgBouncer、HAProxy、exporter 版本与配置;
- schema/extension ADR、query corpus 和业务 invariant 版本。
不复制生产身份
克隆环境必须重写:
否则一次彩排可能向生产 archive 写 WAL、争夺同一 DCS leader、消费真实 slot 或被应用 误连。第 19/23 章的环境 marker 与权限边界应在此复用。
30.6.2 执行升级、扩展更新和服务接入验证
彩排按正式状态机执行
建议阶段:
| 阶段 | 主要动作 | 必留证据 |
|---|---|---|
| software-ready | 仓库、binary、extensions、client tools | 版本、包摘要、所有节点矩阵 |
| clone-ready | 恢复/生成数据,隔离副作用 | source identity、manifest、restore log |
| preflight | catalog/config/collation/integrity/backup | check 报告与阻断项 |
| stop-old | drain writer,clean shutdown | 最终连接、LSN、checkpoint、时间 |
| upgrade | 指定实际模式执行 | 完整 stdout/stderr、每阶段时长 |
| post-process | 脚本、extension、reindex、ANALYZE | 对象清单、错误与耗时 |
| validate | SQL、应用、监控、备份恢复 | query corpus、invariant、SLO |
| rollback-proof | 目标零写入时启动旧端 | old identity、manifest、路由未变 |
| release | 接入服务并写 canary | target identity、pool drain、write boundary |
软件下载、包签名、镜像构建和克隆恢复不应计入业务不可写窗口;但它们必须计入项目 lead time。每个阶段同时记录 wall time、CPU、I/O、WAL、额外空间和锁。
Pigsty major upgrade 有两种平台映射
新集群 + 逻辑复制:
这通常最符合生产最小停机目标,也让旧 cluster 保持完整。
隔离 pg_upgrade / in-place 项目:
需要同时处理:
不要绕开 Patroni,在正在受管的 production data directory 上直接照抄一条裸
pg_ctl/pg_upgrade 命令。本章实验之所以能使用它们,是因为所有 data directory 都在
随机 /tmp 下,从未被 Pigsty 管理。
服务接入验证使用新连接
升级后的原生身份:
再从每种入口重复:
连接成功后运行 query corpus、写/读 canary、权限负例和 failover/failback。旧池必须 drain;否则你验证的可能仍是旧 server session。
30.6.3 比较 Pigsty 面板与原生证据的前后差异
面板比较不是截图找不同
升级会重置/改变部分累计统计,system identifier、timeline、instance label 也可能变化。 因此按信号类型比较:
| 类型 | 比较方式 |
|---|---|
| 配置/容量 | 前后绝对值与 source |
| rate/latency | 相同 workload、相同窗口的分布 |
| cumulative counter | 以启动/重置点为边界,不直接比总数 |
| catalog objects | 精确 manifest |
| plan | 结果先相等,再评估 plan/resource |
| HA/replication | 角色、timeline、lag、slot 与 failover 实验 |
Pigsty dashboard 负责关联:
每个 release gate 同时保留原生证据。例如面板显示 index latency 正常,还要保存:
以及对应 EXPLAIN、amcheck 与 query result。面板没有报警不能证明 exporter SQL 没有
因 catalog 变化而停止采集。
建立差异解释账本
每个差异必须归类:
未知差异不能因为维护窗口快结束就自动变成 expected。
本章实验的平台边界
runner 在 pg-meta 主机上读取 managed cluster 的:
并在实验前后要求完全相同。临时 PG17/18 使用私有 Unix socket,不接入 Pigsty exporter、 HAProxy 或 PgBouncer,所以它证明的是没有误伤 managed cluster,不是“Pigsty 全套 服务已经兼容升级”。
生产彩排必须另行让新 cluster 进入 Pigsty 监控与服务体系,执行上面的面板/原生双向 验证,并从新版本建立可恢复的备份基线。
上一节:升级前检查与业务验证 · 返回本章目录 · 下一节:实战:前滚、回退与发布决策 · 查看全书目录 · 查看索引中心
30.7 实战:前滚、回退与发布决策
前六节已经把升级拆成版本决策、数据迁移、扩展、排序规则、验收和 Pigsty 平台动作。 本节再把它们收束为一条可重复的状态机:
实验会在 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 不联网下载,也不安装系统软件。调用者需要提前准备:
先做不连接数据库的静态合同检查:
完整实验只应在已确认的开发沙箱中运行,并使用新的私有证据目录:
若要分步评审,可以依次执行:
capture 先读取执行主机身份、文件系统与二进制版本,校验确为相邻的 17→18 major
升级,并记录可执行文件 SHA-256。exercise 才会在随机 marker 约束的临时目录中
初始化集群;listen_addresses 为空,所有连接都走权限为 0700 的 Unix socket。
实验只有 pg36_upgrade.app.orders 中 10,000 行确定性合成数据。
正式参考 run 使用 PostgreSQL 17.10 和 18.6;minor 号只是那次证据的事实,并不是读者 环境要硬编码的常量。升级前基线为:
制造一个可控的排序规则失配
为了让阻断逻辑可重复,runner 只在一次性 fixture 中,把
app.en_numeric 对应 pg_collation 行的 collversion 精确改为
pg36-injected-stale。这是教学故障注入,绝不是生产修复方法;修改其他 catalog
行或在托管集群中照做都被合同禁止。
PostgreSQL 读取到的实际 ICU 版本为 153.121,门禁因而得到:
修复必须遵守第 30.4 节建立的顺序:
先重建依赖对象,是让索引按当前排序语义重新物化;后刷新版本,只是承认依赖已经处理。 如果反过来执行,告警可能消失,但旧索引仍可能保留旧排序语义。参考 run 在重建后重新 验证 10,000 行 manifest 与查询结果,确认 mismatch 消失,才允许进入停库阶段。
让 pg_upgrade --check 拒绝真正不兼容的目标
源集群启用了 data checksums。runner 先故意以 --no-data-checksums 初始化一个 PG18
目标,再用计划采用的 PG18 pg_upgrade 二进制执行检查:
失败目标会被精确删除。随后重新初始化 checksums 一致的目标,pg_upgrade --check
通过,正式执行:
实验固定使用默认 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 的升级前后摘要完全相等:
前后查询都使用了 index scan,但 validator 没把“执行计划文本必须相同”当成通过条件。 新版本优化器可以合法地选择不同计划;真正要验收的是结果等价、业务时延与资源预算,而 不是把旧计划冻结成正确答案。
在第一笔目标独占写之前实际回退
完成上述验证后,实验仍不向 PG18 写入。它停止新集群,重新启动旧 PG17,再用旧端读取 同一 manifest:
这不是纸面命令,也不是“旧目录还在”的推断,而是一次真实启动与查询。它成立有三个必要 前提:
- 使用 copy 模式,旧目录没有与新目录共享或交换文件;
- 新集群还没有接受任何独占写入;
- 应用、连接池和路由仍被写围栏挡在外面。
若用 link 模式,新集群一旦启动就可能修改旧集群共用的数据页,官方明确警告旧集群此后 不再安全;swap 更会交换文件,不能套用本节回退步骤。clone 是否可回退还取决于文件系统 和后续写入边界,也不能只凭模式名假设。
第一笔新写入改变了问题
回退证明完成后,runner 再次停止 PG17、启动 PG18,并提交一条明确 canary:
从这一刻开始,旧 PG17 不包含 order_id = 10001。若立即把路由切回旧集群,数据会在
用户视角消失;真实系统还可能已经发送邮件、扣款或调用外部服务。此后的“回退”不再只是
启动旧二进制,而是数据迁移和业务补偿:
因此升级 runbook 必须把两种回退写成不同状态:
| 状态 | 新版本独占写 | 可以采取的动作 |
|---|---|---|
| 验证窗口 | 0 | 停新、启旧、验证旧端后恢复旧路由 |
| 已恢复写入 | > 0 或未知 | 先写围栏与对账;默认优先修复后前滚 |
所谓“回退窗口”不是维护开始后的固定分钟数,而是第一笔无法在旧端重现的提交之前。 时间阈值仍然有用,但它不能替代数据边界。
30.7.3 输出升级 runbook、决策门和观察窗口
证据包必须能识别“看起来成功”
私有证据目录保存:
公开参考摘要在
upgrade-run.json。它保留版本、状态和验收结论,
但删除本地路径、连接信息与私有原始日志。对已完成的同一个证据包,可重复执行:
validator 不只检查成功字段。正式 run 要求:
也就是说,伪造版本关系、二进制散列、checksum 拒绝原因、collation 依赖、修复顺序、
manifest、amcheck、回退前写入数、forward canary、平台边界或清理结果,都不能继续
得到 pass。
清理不是附属动作
实验结束时逐项证明:
清理只匹配当前随机 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 与备份恢复也分别回答逻辑结构、存储页和灾难
恢复问题,不能互相替代。
观察窗口要有阈值和退出动作
切流后建议按三层观察:
具体时长必须由业务周期决定。每一项都要写基线、阈值、查询或 dashboard、owner 和超阈值 动作。例如“观察延迟”不够,至少应写成:
参考沙箱最终得到的是:
它证明升级合同可执行,却没有证明真实扩展 ABI、生产数据量、停机预算、HA 拓扑、应用
驱动、备份恢复和业务峰值。只有这些生产证据补齐并由 owner 批准,pending 才能转为
可发布。
第 31 章将把这种门禁和状态机带入更不友好的情形:系统已经发生故障时,怎样先止血、 再取证、恢复服务并留下可复盘的事件时间线。
上一节:用隔离环境完成升级彩排 · 返回本章目录 · 下一章:事件分级、现场保护与应急决策——枕戈待旦 · 查看全书目录 · 查看索引中心