跳转到主要内容

30 推陈出新:版本升级与回滚策略

第 29 章已经把逻辑复制、全量校验、切流和回退组织成迁移状态机。版本升级是在这条主线 上再增加五个同时变化的维度:

PostgreSQL server
  + data/catalog format
      + SQL behavior and defaults
          + extensions and native libraries
              + clients, pools, backups, exporters and automation

因此,升级不是“换个 RPM/DEB 再重启”,也不是 pg_upgrade 返回成功就结束。它是一项 有兼容性清单、隔离彩排、业务基线、发布门、写入分界和退出窗口的迁移项目。

学习完成标准

完成本章后,读者应能:

  1. 区分 minor、安全修复和 major upgrade,并从 release notes 提取行为变化;
  2. pg_upgrade、逻辑复制和 dump/restore 之间按停机、空间、回退与重建目标选型;
  3. 盘点扩展的包、动态库、SQL 对象、preload 和 update path;
  4. 发现 collation version 漂移,并坚持先重建依赖对象、再刷新版本;
  5. 用 catalog、checksum、amcheck、恢复证据、查询结果和业务不变量建立升级基线;
  6. 在隔离环境复现完整升级,并把软件准备时间与业务不可写时间分开;
  7. 在目标第一笔独占写入前证明回退,在其后切换为对账或前滚策略;
  8. 输出含停止线、观察窗口、owner 与证据包的生产升级 runbook。

一张图看懂升级决策

识别变化
  -> inventory 数据 / 配置 / 扩展 / 客户端 / collation
      -> 选择 pg_upgrade / logical / dump-restore
          -> 克隆与彩排
              -> compatibility gate
                  -> stop old writer
                      -> upgrade and rebuild
                          -> application-visible validation
                              -> rollback proof before target writes
                                  -> release and observe

任一门禁失败,都回到前一个可解释状态;不能用“先上线再看看”跨过 checksum、 collation、扩展或业务结果不一致。

本章目录

30.1 先识别变化类型

30.2 三类大版本升级路径

30.3 扩展与依赖升级

30.4 locale、collation 与索引风险

30.5 升级前检查与业务验证

30.6 用隔离环境完成升级彩排

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

写作与验收提示

本章提供一个真实、隔离的 PostgreSQL 17.10→18.6 参考实验。它在 Pigsty pg-meta 主机的随机 /tmp 目录中创建两套 Unix-socket-only 临时集群,不接触 Pigsty 管理的数据目录与服务。正式证据证明:

fixture rows before / after       10,000 / 10,000
ordered digest equal              true
stale ICU collation gate          blocked
REINDEX then REFRESH VERSION      passed
checksum-incompatible target      rejected
pg_upgrade method                 copy
amcheck and staged ANALYZE        passed
old PG17 restart before new write passed
forward canary                    order_id 10001
remote and fixture cleanup        verified

验证器拒绝了 30 个声明反例和 20 个现场证据变异。公开摘要位于 upgrade-run.json,完整实验合同位于 lab-contract.md

这次小型沙箱成功不预测生产停机时长,也不证明第三方扩展、驱动、备份体系或真实应用 已经兼容。production_ch30_gate 始终保持 pending;生产授权必须来自真实数据克隆、 业务彩排、备份恢复与变更审批。

参考资料


上一章:移花接木:逻辑复制、迁移与异构同步 · 返回下卷导读 · 下一章:事件分级、现场保护与应急决策——枕戈待旦 · 查看全书目录 · 查看索引中心

30.1 先识别变化类型

升级风险首先取决于“什么发生了变化”。同一个“版本升级”工单,可能只是在同一 major 里换修复版,也可能跨越 data directory、系统目录、SQL 行为和扩展 ABI。若不先分类, 团队很容易把 minor upgrade 做成一次不必要的数据迁移,或把 major upgrade 当成滚动 重启。

30.1.1 小版本、安全修复与大版本

版本号先按 PostgreSQL 规则读

从 PostgreSQL 10 开始:

17 -> major
17.10 -> major 17 的第 10 个 minor release
17 -> 18 -> major upgrade
17.9 -> 17.10 -> minor upgrade

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 集群通常可以:

准备并锁定同一套 minor/extension 包
  -> 升级 replicas
      -> 分别重启并等待重新追平
          -> switchover
              -> 升级原 primary
                  -> 重启、追平、验收

Pigsty 提供包仓、Ansible、Patroni 与 pg 管理命令来执行这条链,但“rolling”不等于 所有节点同时更新。每一步都应确认:

SELECT version(), pg_is_in_recovery();

并观察 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 拒绝:

old cluster uses data checksums but the new one does not

这说明“新版本的默认值更安全”不能替代显式匹配。目标必须按源集群和新架构的合同 初始化。

四张差异表

升级 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

SELECT name,
       setting,
       unit,
       source,
       sourcefile,
       sourceline,
       pending_restart
FROM pg_settings
ORDER BY name;

SELECT sourcefile,
       sourceline,
       seqno,
       name,
       setting,
       applied,
       error
FROM pg_file_settings
ORDER BY seqno;

第一张是最终有效值,第二张是配置文件解析结果。两者配合才能发现:

同名参数被后续 include 覆盖
新版本已不认识旧参数
配置行语法错误
值已写入但需要 restart
环境变量 / ALTER SYSTEM / 命令行改变来源

同样地,HBA 要用 pg_hba_file_rules 解析,不能只做文本 diff。

系统目录不是跨 major 的稳定应用 API

监控、迁移脚本和内部工具经常直接读取 pg_stat_*pg_catalog。这些是 PostgreSQL 公开能力,但列集合和含义可以随 major 演进。避免:

SELECT * 后按列位置解码
假设枚举状态永远只有旧值
把 OID 当跨集群稳定标识
在 SQL 中硬编码 server 版本分支却不测试

应显式列名,并以 server_version_num 选择经过测试的查询:

SELECT current_setting('server_version_num')::int;

升级前在新版本空集群上先跑完整 exporter、备份、健康检查和自动化 SQL。目标不是 “SQL 不报错”而是输出字段、单位和告警语义仍正确。

30.1.3 驱动、连接池、备份与观察组件兼容

兼容矩阵的单位是“实际组合”

不要写“JDBC 支持 PostgreSQL”。应记录:

application: checkout-v4
runtime: Java 21
driver: pgjdbc x.y.z
pool: HikariCP x.y.z
proxy: PgBouncer x.y
old_server: PostgreSQL 17
new_server: PostgreSQL 18
auth: scram-sha-256 + TLS verify-full
session_features:
  - prepared statements
  - binary transfer
  - application_name
  - statement_timeout
tested_queries: checkout-corpus-v8

驱动测试至少覆盖:

  • 认证、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。

这带来几条门禁:

new pg_dump -> old source   must pass
new pg_restore -> new target must pass
backup agent -> old during window must pass
backup agent -> new after cutover must pass
restore tooling -> isolated new cluster must pass

物理备份则与 server major、WAL、control file 和工具版本绑定。不能把 PG17 的物理备份 直接恢复成 PG18;它应先恢复为 PG17,再走获批的升级路径。Pigsty 中 pgBackRest 配置、 repository、stanza、retention 和恢复脚本都要在新拓扑重验,而不是只确认最新 backup 状态是 completed。

“监控没报警”可能是 exporter 已失明

升级后指标归零有两种解释:

系统没有负载
采集 SQL 已失败或字段含义改变

因此观察组件要做三向核对:

组件 新版本检查 原生对照
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

/usr/lib/postgresql/18/bin/pg_upgrade \
  --old-bindir /usr/lib/postgresql/17/bin \
  --new-bindir /usr/lib/postgresql/18/bin \
  --old-datadir /pg/data/17 \
  --new-datadir /pg/data/18 \
  --username postgres \
  --socketdir /secure/short/socket \
  --check

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

停机窗口不只是一条命令的秒数

业务不可写窗口通常包含:

drain writers
  -> final checkpoint / backup evidence
      -> stop old topology
          -> pg_upgrade checks and transfer
              -> rebuild scripts / extension updates / collation work
                  -> staged ANALYZE
                      -> application validation
                          -> routing and pool drain

pg_upgrade 输出的 Upgrade Complete 只是中点。它会提示需运行的 rebuild/reindex 脚本;被这些脚本引用的表在完成前可能返回错误结果或性能极差。PostgreSQL 18 会迁移 大部分优化器统计,但 extended、扩展自定义和累计统计并不完整,仍建议:

vacuumdb --all --analyze-in-stages --missing-stats-only
vacuumdb --all --analyze-only

彩排要分别计时每一阶段,以生产数据克隆的 P95/P99 结果安排窗口,不能拿一个 10,000 行实验的 pg_upgrade 时间乘比例。

30.2.2 逻辑复制与渐进切换

逻辑复制把新 major 建成独立 cluster:

old source remains writable
  -> schema prepared on new target
      -> initial copy
          -> incremental apply
              -> shadow read and reconciliation
                  -> freeze, final marker, sequence sync
                      -> cutover

它的优点:

  • 业务不可写时间主要集中在最终冻结与切流;
  • 新旧环境可同时运行,便于真实应用影子验证;
  • 可以跨平台、改变物理布局、重配参数和扩展;
  • 目标可逐步扩容与预热。

代价已经在第 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。

常见路径:

# 用目标版本工具读取旧 server
pg_dump -Fd -j 8 -d app -f app.dump
pg_dumpall --globals-only > globals.sql

# 在新 server 恢复并审阅错误
psql -X -d postgres -f globals.sql
pg_restore -j 8 -d app_new app.dump

选择性恢复不是“完整 cluster 迁移”的同义词。需要另外处理:

roles and memberships
database-level settings
tablespaces
extensions and shared libraries
large objects
publications/subscriptions
security labels
replication slots
external files and FDW credentials

使用 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 切流前强 切流前强
数据对账 必需 最重 必需

一个常见组合不是三选一,而是:

production: logical replication
  + disaster rehearsal: dump/restore
  + small internal clusters: pg_upgrade

甚至同一项目会先用 dump/restore 生成测试克隆,再用 pg_upgrade 彩排正式路径。

“无感”要拆成多个 SLO

所谓无感至少包含:

connection establishment
read availability
write availability
transaction in flight
latency and plan stability
background jobs
CDC and replica continuity
error and retry semantics
data freshness

逻辑切换只有几秒写冻结,不代表旧池里的长事务、prepared statement、sequence、 缓存、ETL 与外部副作用无感。minor rolling 也会发生连接断开和 failover。pg_upgrade 即使文件迁移很快,post-upgrade reindex/ANALYZE 仍可能控制上线时间。

所以 runbook 不写“无感升级”,而写:

read_unavailable_budget: 5s
write_unavailable_budget: 30s
inflight_policy: drain-then-retry-idempotently
replication_rpo: 0
latency_gate: p99 <= baseline * 1.20
rollback_boundary: before first target-only commit

可测量的预算才是发布决策;“滚动”“在线”“秒级”只是实现特征。


上一节:先识别变化类型 · 返回本章目录 · 下一节:扩展与依赖升级 · 查看全书目录 · 查看索引中心

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 只回答第四层:

SELECT e.extname,
       e.extversion,
       n.nspname AS schema,
       r.rolname AS owner,
       e.extrelocatable
FROM pg_extension AS e
JOIN pg_namespace AS n ON n.oid = e.extnamespace
JOIN pg_roles AS r ON r.oid = e.extowner
ORDER BY e.extname;

还要查询目标 server 实际能提供的版本:

SELECT name,
       default_version,
       installed_version,
       comment
FROM pg_available_extensions
ORDER BY name;

SELECT name, version, installed, superuser, trusted
FROM pg_available_extension_versions
ORDER BY name, version;

“源端安装了 3.4,目标仓库有 3.5”并不能证明可直接升级;中间 SQL update scripts 可能缺失,C ABI 也可能不支持目标 major。

pg_upgrade 前先装文件,不要重建 SQL 对象

官方 pg_upgrade 流程要求先在新版本安装匹配的 extension shared libraries 与支持 文件。不要在空的新 cluster 预先执行:

CREATE EXTENSION postgis;

旧 cluster 的 extension catalog 与对象会随升级迁入,提前 CREATE 会重复。正确顺序 通常是:

锁定 old extension inventory
  -> 准备 target-major packages on every future node
      -> 检查 control / SQL / shared library
          -> 配置必要 preload,但暂不让错误配置影响 managed cluster
              -> pg_upgrade --check
                  -> pg_upgrade
                      -> 按生成脚本和扩展文档 ALTER EXTENSION UPDATE

pg_upgrade 会检查它能发现的 required libraries,也会为可用 extension update 生成 脚本;但官方明确指出外部模块的所有 binary compatibility 无法由它完全检查。包含自定义 background worker、WAL resource manager、shared memory 或特殊 table access method 的扩展,必须按扩展自己的 major-upgrade 文档测试。

preload 是启动依赖

盘点:

SELECT name, setting, source, pending_restart
FROM pg_settings
WHERE name IN (
  'shared_preload_libraries',
  'session_preload_libraries',
  'local_preload_libraries'
);

shared_preload_libraries 中任一 .so 缺失或与新 server ABI 不匹配,目标可能根本 起不来。先在隔离 cluster 加载:

postgres -D /isolated/new -C shared_preload_libraries

再实际启动并检查 log。不要在正式窗口里通过“先把所有 preload 清空”绕过;这样启动的 server 可能缺少审计、监控、时序、列存或业务所依赖的语义。

HA 集群中每个候选 primary/replica 都必须有同一套兼容库。只在当前 primary 安装, failover 才会暴露缺包。

30.3.2 扩展升级脚本与不可降级路径

先证明存在完整 update path

SELECT source, target, path
FROM pg_extension_update_paths('postgis')
WHERE source = (
  SELECT extversion
  FROM pg_extension
  WHERE extname = 'postgis'
)
ORDER BY target;

目标是看到从当前 installed version 到批准 target version 的路径。然后显式指定:

ALTER EXTENSION postgis UPDATE TO '3.x.y';

不带 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,发布决策应写:

before ALTER EXTENSION: old cluster / backup can still be rollback source
after extension writes new format: rollback requires restore or logical transform

不要尝试把 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:

extension: postgis
business_owner: geo-platform
technical_owner: dba
installed_databases: [maps, routing]
installed_version: 3.x
target_version: 3.y
postgresql_majors: [17, 18]
os_arch: ubuntu24-arm64
packages:
  old: ...
  new: ...
preload: false
stored_types: [geometry, geography]
indexes: [gist, spgist]
upgrade_path_evidence: ...
downgrade_path: none
portable_export: EWKB/EWKT
exit_cost: high

将 ADR 与现场 catalog 对账:

ADR 有、catalog 无 -> 是否已退役但配置/包残留
catalog 有、ADR 无 -> 未治理依赖,升级门禁失败
version 不同 -> 漂移,先解释
preload 不同 -> 启动风险
package 无 target major -> 路径不可行

还要找出 extension members 与业务依赖:

SELECT e.extname,
       pg_describe_object(d.classid, d.objid, d.objsubid) AS member
FROM pg_extension AS e
JOIN pg_depend AS d
  ON d.refclassid = 'pg_extension'::regclass
 AND d.refobjid = e.oid
 AND d.deptype = 'e'
ORDER BY e.extname, member;

业务对象依赖 extension type/function/operator 的方向还需从 pg_depend 反查。只有列出 stored types、indexes、views、generated expressions 和 functions,才能知道升级失败 影响哪些对象。

退出策略在升级时兑现

ADR 若声明“可移除”,彩排应实际证明:

导出为 core PostgreSQL / portable representation
  -> 在无该 extension 的目标恢复
      -> 比较业务结果
          -> 重建目标索引
              -> 回放增量或冻结切换

若做不到,就把 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 encoding
database default locale provider and locale
database recorded/actual collation version
column/expression-level COLLATE
user-defined pg_collation objects
libc / ICU / builtin provider and version
operating-system image and architecture

在每个 database 中:

SELECT c.oid::regcollation AS collation,
       c.collprovider,
       c.collisdeterministic,
       c.collcollate,
       c.collctype,
       c.colllocale,
       c.collversion AS recorded_version,
       pg_collation_actual_version(c.oid) AS actual_version
FROM pg_collation AS c
WHERE c.collversion IS DISTINCT FROM
      pg_collation_actual_version(c.oid)
ORDER BY 1;

对数据库默认 collation,还要检查 pg_database.datcollversionpg_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。若不一致,会提示:

rebuild affected objects
then REFRESH VERSION

它不会自动知道所有外部语义,也不会替你安排锁和空间。OS patch、容器基础镜像、 跨发行版迁移与 pg_upgrade 都可能触发这条门禁,所以 collation 检查不应只在 PostgreSQL major upgrade 执行。

30.4.2 排序变化对唯一性和索引顺序的影响

B-tree 的正确性依赖比较函数稳定

假设旧规则认为:

a < ä < b

新规则变成:

a < b < ä

旧 index page 仍按第一种顺序排列。binary search 使用新 comparator 在旧顺序上导航, 可能漏行、返回错误 range,ORDER BY 也可能错误地相信 index 已有正确顺序。amcheck 文档明确把“索引 tuple 是否按 collation 逻辑顺序”列为 B-tree invariant。

UNIQUE 更危险。若旧规则认为两个值不同,新规则认为相等:

旧索引允许两行
  -> 新规则下 REINDEX UNIQUE
      -> duplicate key,重建失败

此时不能先刷新版本并继续。需要业务 owner 决定:

  • 规范化并合并重复值;
  • 改为 deterministic collation;
  • 改唯一键设计,加入稳定业务 ID;
  • 保留旧 provider/locale,推迟环境变化。

若新规则只改变顺序、不改变相等性,仍需重建所有依赖排序的结构。

受影响的不只普通文本索引

盘点:

B-tree index and UNIQUE/PRIMARY constraints with collatable keys
expression index using collation-sensitive functions/operators
partition bounds and partitioned indexes
materialized views with ordered/normalized derived data
full-text and pg_trgm indexes called out by release notes
application pagination/bookmark built on locale ordering
cached sorted projections outside PostgreSQL

hash index 不保存排序,但 collation-sensitive equality 和业务去重仍需按实际 operator 语义验证。不能把“只重建所有 text B-tree”当成对任意扩展 operator class 的完整规则。

query corpus 要覆盖等价类

建议保存:

SELECT value
FROM upgrade_collation_corpus
ORDER BY value COLLATE app.customer_name, corpus_id;

SELECT left_value,
       right_value,
       left_value = right_value COLLATE app.customer_name AS equal,
       left_value < right_value COLLATE app.customer_name AS less
FROM upgrade_collation_pairs
ORDER BY pair_id;

语料包含 case、accent、组合字符、数字字符串、标点、emoji、各业务语言和空白。比较 旧/新结果时,先区分“批准的排序变化”与“会破坏唯一性/分页的变化”。

30.4.3 识别受影响对象并规划重建

先列依赖,再排动作

官方给出的通用依赖查询:

SELECT pg_describe_object(
         d.refclassid, d.refobjid, d.refobjsubid
       ) AS collation,
       pg_describe_object(
         d.classid, d.objid, d.objsubid
       ) AS dependent_object
FROM pg_depend AS d
JOIN pg_collation AS c
  ON d.refclassid = 'pg_collation'::regclass
 AND d.refobjid = c.oid
WHERE c.collversion <>
      pg_collation_actual_version(c.oid)
ORDER BY 1, 2;

版本字段可能为 NULL,因此生产查询通常同时使用 IS DISTINCT FROM,再根据 provider 判断哪些 NULL 是预期。对 index 可进一步从 pg_index.indcollation 精确列出:

SELECT i.indexrelid::regclass AS index_name,
       i.indisunique,
       pg_relation_size(i.indexrelid) AS bytes
FROM pg_index AS i
JOIN pg_collation AS c
  ON c.oid = ANY(i.indcollation)
WHERE c.oid = 'app.en_numeric'::regcollation
ORDER BY bytes DESC;

计划每个对象的:

rebuild command and whether CONCURRENTLY is supported
lock and blocking behavior
temporary and final disk headroom
WAL volume and replica lag
unique collision handling
estimated duration from clone rehearsal
validation query and owner

顺序不能颠倒

正确顺序:

identify all affected objects
  -> validate new equality/sort semantics
      -> resolve unique conflicts
          -> rebuild every affected object
              -> run amcheck / query corpus
                  -> ALTER COLLATION ... REFRESH VERSION
                      -> verify mismatch set is empty

REFRESH VERSION 只把 catalog 中的 recorded version 更新为当前 provider version; 官方明确说明它不会检查对象是否已正确重建。若先 refresh,warning 消失了,旧索引 却还在,反而销毁了最显眼的故障信号。

数据库默认 collation 使用:

ALTER DATABASE app REFRESH COLLATION VERSION;

也同样必须在所有依赖对象重建后执行。

正式实验的反例

本章 runner 在一次性 PG17 cluster 中精确把:

app.en_numeric.collversion = pg36-injected-stale
actual ICU version         = 153.121
affected index             = app.orders_order_code_key

门禁立即变成 blocked。runner 只允许:

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

完成后 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

SELECT datname,
       pg_encoding_to_char(encoding) AS encoding,
       datlocprovider,
       datcollate,
       datctype,
       datlocale,
       datcollversion,
       datallowconn
FROM pg_database
ORDER BY datname;

SELECT spcname, pg_tablespace_location(oid)
FROM pg_tablespace
ORDER BY spcname;

SELECT rolname, rolsuper, rolreplication, rolbypassrls
FROM pg_roles
ORDER BY rolname;

随后逐 database 采集 extension、collation、schema、owner、privilege、large object、 publication/subscription、FDW/server/user mapping、database/role settings。pg_upgrade 处理的是整个 cluster,漏掉一个平时不连接的 database,也可能在 schema dump/restore 阶段让全局升级失败。

第二组:不完整与待完成状态

SELECT i.indexrelid::regclass AS index_name,
       i.indisready,
       i.indisvalid,
       i.indislive
FROM pg_index AS i
WHERE NOT i.indisready
   OR NOT i.indisvalid
   OR NOT i.indislive
ORDER BY 1;

SELECT conrelid::regclass AS relation,
       conname,
       contype,
       convalidated
FROM pg_constraint
WHERE NOT convalidated
ORDER BY 1, 2;

SELECT transaction, gid, prepared, owner, database
FROM pg_prepared_xacts
ORDER BY prepared;

invalid index 可能来自失败的 CREATE INDEX CONCURRENTLY;它是否删除、重建或保留, 要在升级前决定。NOT VALID constraint 可以是有意的渐进变更,但必须进入 inventory。 prepared transaction 是未完成的分布式业务事实,不能在停机时当普通长连接粗暴清掉。

还要检查 logical replication:

SELECT slot_name, slot_type, database, active,
       restart_lsn, confirmed_flush_lsn,
       wal_status, invalidation_reason
FROM pg_replication_slots
ORDER BY slot_name;

SELECT subname, subenabled, subslotname
FROM pg_subscription
ORDER BY subname;

PG17+ 的某些 logical slot/subscription 状态可由 pg_upgrade 迁移,但必须满足官方升级 前提;老版本、无效 slot 和复杂拓扑不能靠默认推断。

第三组:活动与变更冻结

SELECT pid, datname, usename, application_name,
       state, xact_start, backend_xid, backend_xmin,
       wait_event_type, wait_event
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;

正式停机前应:

冻结 DDL 与 extension 变更
停止 scheduler / ETL / schema migrator
drain application writers and long transactions
resolve prepared transactions by their coordinator
ensure replicas caught up
record final configuration and object manifest
stop old primary cleanly

pg_upgrade --check 还会检查 version、control data、prepared transaction、部分不支持 类型、required libraries、logical slot/subscription 等条件。它是必要门禁,但不覆盖 应用查询与业务不变量。

一个容易遗漏的原生限制是:pg_upgrade 不支持用户列使用若干保存 OID 的 reg* 类型,如 regcollationregconfigregprocregprocedureregclassregroleregtype 可升级。preflight 应按目标版本文档扫描,而不是等正式窗口报错。

30.5.2 amcheck、checksum 状态与备份恢复证据

在线先做结构检查

对已选重要 B-tree:

CREATE EXTENSION IF NOT EXISTS amcheck;

SELECT bt_index_check(
         index => i.indexrelid,
         heapallindexed => i.indisunique,
         checkunique => i.indisunique
       )
FROM pg_index AS i
WHERE i.indexrelid IN (
  'app.orders_pkey'::regclass,
  'app.orders_order_code_key'::regclass
);

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 --check --progress -D /pg/data/17

pg_checksums 要求 server clean shutdown;check 会扫描 cluster 文件,发现至少一个 checksum failure 时返回非零。启用/禁用 checksum 更是会修改数据块或 control file, HA 拓扑必须所有节点一致处理,不能把它顺手塞进 major upgrade。

PostgreSQL 18 initdb 默认开启 checksums,而 pg_upgrade 要求 old/new 设置匹配。 本章实验显式构造:

old PG17 checksums on
new PG18 checksums off

--check 返回 1;runner 删除这个不兼容的目标,重新以 checksums on 初始化,才允许 继续。

“有备份”必须变成一次可恢复证据

升级票据至少绑定:

backup_id: ...
source_system_identifier: ...
start_lsn: ...
stop_lsn: ...
timeline: ...
manifest_verified: true
required_wal_present: true
restore_target: isolated-pg17
restore_completed_at: ...
logical_manifest_equal: true
rto_measured: ...

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,也不证明结果正确。 验证顺序:

  1. error/SQLSTATE 与行结果;
  2. 行数、排序、NULL、类型与精度;
  3. 业务不变量;
  4. plan shape、估算与实际行数;
  5. latency、吞吐、CPU、I/O、WAL 与锁。

pg_stat_statements、APM 和业务清单建立代表性 query corpus:

top total time
top mean/p99 latency
top calls
largest temp/WAL/I/O
关键交易与结算
低频但高风险管理语句
DDL / migration / backup / exporter queries

参数分布很重要。只用一个平均 customer_id 做 EXPLAIN,无法发现 skew、NULL、热点和 极端时间范围。

保存可比的证据格式

EXPLAIN (
  ANALYZE,
  BUFFERS,
  WAL,
  SETTINGS,
  VERBOSE,
  FORMAT JSON
)
SELECT ...;

对写 SQL 应在可回滚、隔离的克隆上执行。记录:

server version and system identifier
schema/data snapshot identity
session GUC
parameter values or distribution class
cold/warm cache condition
concurrency
plan JSON
result digest
latency distribution and resource counters

不要逐字比较 cost;新版本 cost model、节点和统计可能合理变化。设置门禁:

result: exact
business_invariants: zero violations
p95_latency: <= old * 1.15
p99_latency: <= old * 1.25
error_rate: no regression
temp_bytes: <= budget
wal_bytes_for_write_corpus: <= budget
plan_regression: reviewed, not byte-identical

本章 fixture 查询 order_code = 'order-5000' 在 PG17 和 PG18 都返回同一行,两个版本 都使用 orders_order_code_key Index Scan。实验记录完整 plan JSON,但 validator 只强制 业务结果相等和 plan 存在,不把“必须同一个节点”误写成普遍升级合同。

业务不变量是最终解释层

通用数据摘要之外,还应验证:

账务借贷平衡
订单状态合法且转移可达
租户间无交叉
外键孤儿为零
sequence next value 安全
任务水位和队列 offset 一致
权限矩阵不扩大
collation-sensitive pagination 稳定

这些规则应在 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 结构

它们不是相互替代关系:

checksums pass + amcheck fails
  -> page 未被随机篡改,但 index 逻辑结构可能不满足 invariant

amcheck passes + checksum fails elsewhere
  -> 已检对象逻辑可读,但其他 page 物理完整性有问题

both pass + restore fails
  -> backup/WAL/config/secret/extension 链仍可能不完整

all pass + business invariant fails
  -> 数据在物理和结构层可读,业务事实仍然错误

升级发布至少需要这四条独立证据线:

physical integrity
structural integrity
recoverability
logical/business equivalence

把它们压成一个“数据库健康检查已通过”复选框,会丢掉每项证据真正覆盖的边界。


上一节: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。无论哪种,记录:

source_system_identifier: ...
source_backup_or_snapshot: ...
source_lsn_and_timeline: ...
captured_at: ...
data_scope: full | subset | synthetic
anonymization_version: ...
excluded_objects: ...
size_manifest: ...

脱敏不能破坏 join、skew、长度、locale 与唯一性分布,否则兼容测试会得到过于乐观的 结果。隔离网络、独立 secret、禁止邮件/支付/webhook/CDC 等外部副作用同样重要。

配置不是复制旧文件

Pigsty 中应把新 cluster 作为独立 inventory 对象,显式声明:

pg_cluster: pg-new
pg_version: 18
pg_packages:
  - pgsql-main
  - pgsql-common
pg_extensions:
  - ...
pg_conf: oltp.yml

实际变量名和包 alias 以当前 Pigsty 版本为准。关键原则是:

old rendered config -> inventory of intent
release-note parameter diff -> migration decisions
new-version template -> new rendered config
catalog/native views -> effective-value verification

不要把旧 postgresql.confpostgresql.auto.confpg_hba.conf 原样盖到新版本上。 这样会带入已删除参数、错误 include、旧路径与旧认证边界,还会绕过 Pigsty/Patroni 的 配置 ownership。

需要版本化的输入包括:

  • Pigsty inventory commit 与模板/角色版本;
  • PostgreSQL/extension/OS package lock 与摘要;
  • effective pg_settingspg_file_settings、HBA 解析;
  • locale/ICU/libc/architecture;
  • Patroni、pgBackRest、PgBouncer、HAProxy、exporter 版本与配置;
  • schema/extension ADR、query corpus 和业务 invariant 版本。

不复制生产身份

克隆环境必须重写:

cluster_name / Patroni scope / DCS path
systemd/service identifiers
ports, VIP, DNS and HAProxy service
backup stanza/repository write target
archive_command and restore_command
replication slots/subscriptions
application secrets and external endpoints
monitoring labels

否则一次彩排可能向生产 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 有两种平台映射

新集群 + 逻辑复制

用新 pg_version 新建独立 Pigsty cluster
  -> 按第 29 章迁移 schema/data/incremental
      -> Pigsty service/proxy/dashboard 先行验证
          -> 业务切流

这通常最符合生产最小停机目标,也让旧 cluster 保持完整。

隔离 pg_upgrade / in-place 项目

需要同时处理:

Patroni service lifecycle
old/new data and binary paths
primary/standby upgrade or rebuild
pgBackRest stanza and backup baseline
extension packages on every node
generated PostgreSQL config and HBA
service discovery / proxy / exporter
rollback mode

不要绕开 Patroni,在正在受管的 production data directory 上直接照抄一条裸 pg_ctl/pg_upgrade 命令。本章实验之所以能使用它们,是因为所有 data directory 都在 随机 /tmp 下,从未被 Pigsty 管理。

服务接入验证使用新连接

升级后的原生身份:

SELECT current_database(),
       current_user,
       current_setting('server_version'),
       current_setting('cluster_name'),
       pg_is_in_recovery(),
       inet_server_addr(),
       inet_server_port();

SELECT system_identifier FROM pg_control_system();

再从每种入口重复:

direct primary
read service
primary service
PgBouncer transaction/session pools
HAProxy/VIP/DNS
actual application runtime
backup/exporter accounts

连接成功后运行 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 负责关联:

service availability and pool
TPS / query latency / errors
CPU / memory / I/O / disk
WAL / checkpoint / replication
autovacuum and table/index
backup and exporter health

每个 release gate 同时保留原生证据。例如面板显示 index latency 正常,还要保存:

SELECT relid, indexrelid, idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes
WHERE indexrelid = 'app.orders_order_code_key'::regclass;

以及对应 EXPLAINamcheck 与 query result。面板没有报警不能证明 exporter SQL 没有 因 catalog 变化而停止采集。

建立差异解释账本

signal: buffer_cache_hit_ratio
old: 99.4%
new_first_10m: 71.2%
new_after_warmup: 99.1%
classification: expected-cold-cache
action: none
owner: dba
evidence: dashboard-window + pg_stat_io snapshot

每个差异必须归类:

expected reset/warmup
approved new-version behavior
configuration drift
statistics/plan regression
monitoring incompatibility
unknown -> release blocked

未知差异不能因为维护窗口快结束就自动变成 expected。

本章实验的平台边界

runner 在 pg-meta 主机上读取 managed cluster 的:

cluster_name
server_version
system_identifier
primary identity

并在实验前后要求完全相同。临时 PG17/18 使用私有 Unix socket,不接入 Pigsty exporter、 HAProxy 或 PgBouncer,所以它证明的是没有误伤 managed cluster,不是“Pigsty 全套 服务已经兼容升级”。

生产彩排必须另行让新 cluster 进入 Pigsty 监控与服务体系,执行上面的面板/原生双向 验证,并从新版本建立可恢复的备份基线。


上一节:升级前检查与业务验证 · 返回本章目录 · 下一节:实战:前滚、回退与发布决策 · 查看全书目录 · 查看索引中心

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


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