pg_upgrade 实战
pg_upgrade 是 PostgreSQL 官方内置的大版本原地升级工具。它的核心设计非常聪明:数据文件根本不「迁移」——pg_upgrade 在新版本集群里重建一套新的系统目录(元数据),然后把旧集群的数据文件「认领」为新集群的数据文件。正因为跳过了数据本身的搬运与格式转换,pg_upgrade 才能做到「分钟级完成 TB 级升级」,这也是它在中小规模场景碾压其他方案的根本原因。
上一页 版本升级概述 已经把方案选型、升级前检查与回滚设计讲完,本页聚焦 pg_upgrade 本身:它的内部原理(元数据重建、硬链接/拷贝/reflink 三种文件处理模式)、完整参数表、从停应用到验证的逐命令实战、扩展与用户自定义表空间的特殊处理、失败恢复流程,以及用 -j 并行与 --link 压缩窗口时间的性能技巧。读完本页,你应该能独立完成一次带完整演练的 pg_upgrade 变更。
前置知识
建议先阅读:版本升级概述
基础概念
pg_upgrade 的工作原理
一次 pg_upgrade 在内部经历几个阶段(对应输出里的 Performing Consistency Checks / Upgrading 等步骤):
┌────────────────┐ ┌────────────────┐
│ 旧集群 (16) │ │ 新集群 (17) │
│ /data/pg16 │ │ /data/pg17 │
│ (已停止) │ │ (已 initdb) │
└───────┬────────┘ └───────┬────────┘
│ │
│ 1. 版本与兼容性检查(--check 也可单独跑) │
│ 2. 以只读方式启动两套临时 postmaster │
│ 3. 从旧集群导出元数据定义 │
│ 4. 在新集群重建数据库/表/索引等目录条目 │
│ 5. 文件认领:拷贝 或 硬链接 或 reflink │
│ 6. 把 WAL 位置与 clog 等 log 元数据搬到新集群│
│ 7. 冻结新集群数据(设置恢复位置等收尾) │
▼ ▼
旧目录原样保留 新集群可直接用 17 的二进制启动几个关键事实:
- 它不重写堆数据。表的页面、索引的页面原封不动(三种模式都如此),变的只是「这些文件属于谁」的元数据层。
- 统计信息不搬。
pg_statistic属于运行时统计,新集群需要重新 ANALYZE(用vacuumdb --all --analyze-in-stages快速补齐,细节见实战部分)。 - WAL 位置要接续。新集群的 WAL 位置被调整到与旧集群一致(保证复制槽、归档等外部引用的连续性),这也是为什么 pg_upgrade 会把
pg_wal与pg_commit_ts等 log 相关目录搬进新集群。 - 执行期间它自己会以临时端口拉起两套 postmaster(旧集群只读挂载),所以环境里要预留端口与 socket 目录,这是
--socketdir与-o/-O存在的原因。
三种文件处理模式
| 模式 | 参数 | 机制 | 磁盘 | 速度 | 旧目录升级后 |
|---|---|---|---|---|---|
| 拷贝 | 默认 | 把每个数据文件复制到新集群目录 | 双份 | 慢(随数据量线性) | 完整独立,可直接回滚 |
| 硬链接 | -k / --link | 为旧文件在新目录创建硬链接,共享同一份 inode | 几乎零新增 | 快(只建目录项) | 与新集群共享文件,必须封存只读 |
| reflink 克隆 | --clone(PG15+) | 调用文件系统的 COW 克隆(如 XFS reflink、Btrfs/APFS 支持时) | 逻辑双份、物理共享 | 快(近实时) | 写时复制隔离,旧目录保持可用 |
--clone(PostgreSQL 15 引入)利用文件系统的写时复制能力:克隆出来的文件在新旧目录各有一个「名字」,但物理块共享;此后任何一方写入都触发块级复制,互不干扰。它兼得 link 模式的速度与拷贝模式「旧目录独立可用」的安全性——前提是文件系统支持(XFS 需开启 reflink=1,Btrfs/APFS 原生支持,ext4 不支持)。三种模式的选择逻辑:
文件系统支持 reflink? ──是──► --clone(速度与安全兼得)
│否
磁盘空间能放下第二份数据? ──是──► 默认拷贝(最稳)
│否
──► --link(快但旧目录从此与新集群纠缠,回滚策略重新评估)必须理解的前置状态要求
pg_upgrade 要求执行时的环境状态:
- 两个集群都已停止(它自己会以临时 postmaster 拉起,但拉起前必须处于干净停止状态);
- 新旧数据目录都存在:新集群必须先
initdb完成(且默认编码/locale 与旧集群一致,除非显式指定); - 新旧二进制都在:旧大版本的服务端二进制在升级期间必须仍然可用(不能先卸载旧包);
- 没有准备好的两阶段事务、没有残留的空闲事务;
- 旧集群中不能有
reg*类型列引用已删除对象等兼容性问题(--check全部负责)。
语法与用法
参数总表
| 参数 | 说明 |
|---|---|
-b dir / --old-bindir=dir | 旧版本二进制目录 |
-B dir / --new-bindir=dir | 新版本二进制目录 |
-d dir / --old-datadir=dir | 旧集群数据目录 |
-D dir / --new-datadir=dir | 新集群数据目录(须已 initdb) |
-c / --check | 只做兼容性检查,不实际升级(dry-run,必跑) |
-k / --link | 硬链接模式替代拷贝 |
--clone | reflink 克隆模式(PG15+,需文件系统支持) |
-j n / --jobs=n | 并行进程数(拷贝/链接文件与建库时并行) |
-o "opts" / --old-options=opts | 传给旧集群临时 postmaster 的选项 |
-O "opts" / --new-options=opts | 传给新集群临时 postmaster 的选项 |
-p port / --old-port=port | 旧集群临时启动端口 |
-P port / --new-port=port | 新集群临时启动端口 |
--socketdir=dir | 临时 postmaster 的 socket 目录 |
-U user / --username=user | 连接用户(须为安装用户/超级用户) |
-v / --verbose | 详细输出 |
-r / --retain | 保留 sql 与 log 文件供排障(失败分析时有用) |
最小可用命令
pg_upgrade \
-b /usr/lib/postgresql/16/bin \
-B /usr/lib/postgresql/17/bin \
-d /var/lib/postgresql/16/main \
-D /var/lib/postgresql/17/maininitdb 新集群的注意点
# 与旧集群同参数初始化(关键:locale、编码、数据校验和、wal 段大小)
/usr/lib/postgresql/17/bin/initdb \
-D /var/lib/postgresql/17/main \
--locale=C.UTF-8 \
--encoding=UTF8
# 旧集群的对应参数可这样核对:
psql -Atc "SHOW lc_collate; SHOW lc_ctype;" # 集群级 locale(PG15 前为实例级)
psql -Atc "SHOW data_checksums; SHOW wal_segment_size;"initdb 时没有对齐的集群级参数,pg_upgrade 在检查阶段就可能报错(如 locale 不匹配),即使侥幸通过也会埋下排序规则漂移的隐患。
与 pg_upgrade 配套的收尾命令
# pg_upgrade 成功输出会提示的收尾动作(详见官方文档):
# 较新版本中由 vacuumdb 直接完成统计分阶段补齐:
vacuumdb --all --analyze-in-stages -d 每个库
# 或对整个实例:
vacuumdb --all --analyze-in-stages--analyze-in-stages 以递增采样率分三个阶段跑 ANALYZE,第一阶段就能给出「够用」的统计,让业务尽快有可用计划,后续阶段在后台逐步精化——这是替代旧版 analyze_new_cluster.sh 脚本的标准动作。
详细说明
--check 都检查什么
--check 模式跑一遍除文件认领外的所有检查,是升级演练的核心工具。它会验证:
| 检查项 | 含义 |
|---|---|
| 集群版本 | 新旧版本二进制与目录匹配 |
| 安装用户 | 两集群的初始化用户一致 |
| 数据库连接设置 | 每个库可连接、pg_hba 不阻碍 |
| prepared transactions | 必须为空 |
reg* 类型 | 用户数据里不能有悬空的 OID 引用类型 |
isn/intarray 等扩展的已知坑 | 特定扩展版本组合的兼容性 |
| 表空间结构 | 用户自定义表空间路径可达 |
| 数据文件 | (配合 --link)目标 inode 可链接 |
实践中 --check 要在演练环境反复跑,正式窗口前最后一次在目标环境跑。它的通过不代表升级一定成功,但它能拦截绝大多数「结构性」失败。
--link 模式的深层风险
link 模式的本质是「新旧目录项指向同一物理块」。这带来一个反直觉的后果:升级完成后,旧数据目录不再是你的回滚保险。如果升级后在旧目录上启动旧版本实例(哪怕只是误操作),旧版本的写操作会直接修改新集群正在使用的物理文件,造成两个集群同时损坏。
因此选择 --link 时必须配套三条纪律:
- 旧集群的 systemd 服务立即 disable,把旧数据目录
chown root或chmod a-w封存; - 回滚方案改写:回滚不再依赖旧目录「直接启动」,而是依赖「升级前的备份基线」或「升级行为不可逆,只能前滚修复」的预案;
- 观察期结束后再清理旧目录,且清理动作本身要过变更评审。
--clone 模式的适用条件与验证
PG15+ 的 --clone 依赖操作系统支持。执行前验证文件系域能力:
# XFS:确认 reflink 已启用(格式化时 -m reflink=1)
xfs_info /data | grep reflink
# 输出含 reflink=1 即支持
# ext4:不支持,会退化为普通拷贝(pg_upgrade 会给出提示或直接报错)reflink 克隆后的两份文件在写入时才真正分离,因此「升级后旧目录是否可读」是安全的,「旧目录是否可写」则取决于具体文件系统的 COW 语义——保守起见,clone 模式同样建议把旧目录封存只读。
-j 并行的加速逻辑
-j 控制的是「文件拷贝/链接」与「建库、建对象」两个阶段的并行度。加速效果取决于文件数量分布:
- 很多小表:并行收益显著(瓶颈在文件操作次数);
- 单个巨大表:并行无效(一个文件一个进程,串行拷贝);
- 索引重建阶段不存在:pg_upgrade 不重建索引(文件级认领),这也是它快的原因——对比
pg_dump恢复时索引重建占总时间大头。
经验值:-j 设为 CPU 核数或稍高,配合 --link 时瓶颈转为主要单文件操作,-j 收益下降但仍能加速多库多表的目录操作。
用户自定义表空间的处理
含自定义表空间的集群升级时,pg_upgrade 对路径结构有严格要求:
# 旧集群的表空间是 $PGDATA/pg_tblspc 下的符号链接
ls -l /var/lib/postgresql/16/main/pg_tblspc/
# 41012 -> /data/pg_tsp_hot
# 新集群 initdb 后,pg_upgrade 会按旧集群的结构在新集群里建立同样的链接
# 约束:新集群的 pg_tblspc 初始为空,表空间目标目录由 pg_upgrade 重建链接指向注意事项:
- 新旧集群的表空间目录路径由 pg_upgrade 沿用(新集群的链接同样指向这些目录),因此目标文件系统必须对新版本进程可写;
- 表空间内的文件同样经历拷贝/链接/克隆三种模式;
- 若使用 link/clone 模式,表空间目录同样与新集群共享文件,封存策略必须覆盖所有表空间路径,不只是
PGDATA。
扩展的处理:升级与对齐是两件事
pg_upgrade 把扩展的元数据原样搬进新集群(pg_extension、依赖关系等),但它不会自动执行新版本扩展提供的新 SQL。扩展的对齐要分两步走:
-- 1. 升级完成后检查哪些扩展可以更新版本
SELECT e.extname,
e.extversion AS installed,
a.default_version AS available
FROM pg_extension e
JOIN pg_available_extensions a ON a.name = e.extname
WHERE e.extversion <> a.default_version
ORDER BY 1;
-- 2. 逐个对齐(扩展内部会执行它自己的 upgrade 脚本)
ALTER EXTENSION pg_stat_statements UPDATE;
ALTER EXTENSION postgis UPDATE;同时记住升级前后各一份扩展清单对比(上一页的预检脚本),二进制层(.so)由操作系统包升级提供——先升级 OS 包再跑 pg_upgrade,保证新集群临时 postmaster 加载的是新版本编译的库。
失败恢复:pg_upgrade 中途失败怎么办
pg_upgrade 的失败处理原则是「旧集群永远完好,新集群废弃重建」:
# 1. 确认旧集群状态:升级失败不会写旧集群(除 link 模式的文件共享外)
# 检查旧目录未被启动/修改后,可随时用旧版本二进制启动恢复业务
# 2. 处理新集群:直接删除,重新 initdb
rm -rf /var/lib/postgresql/17/main # 确认路径无误!
/usr/lib/postgresql/17/bin/initdb -D /var/lib/postgresql/17/main ...
# 3. 定位失败原因:
# - 保留日志(--retain 或默认输出)中的 ERROR
# - 典型原因:磁盘满、权限、扩展 .so 缺失、locale 不一致、检查项遗漏
# 4. 修复后重新走 initdb → --check → pg_upgrade 全流程两个特殊提醒:其一,link 模式失败后重新执行前,必须删除新集群重新 initdb——半链接状态的新集群不能直接重跑;其二,若失败发生在「文件认领」之后的收尾阶段,新集群可能「看起来能用」,也必须废弃重来——半成品的元数据一致性无法保证。
升级演练:把窗口变成复读机
正式升级前,在克隆环境完整演练至少两轮:
# 演练环境:用文件系统快照克隆生产数据目录(或 pg_basebackup)
# 第一轮:记录每步耗时,校准窗口预算
time pg_upgrade ... --link
# 第二轮:从快照恢复后,带上全部收尾动作 + 冒烟验证 + 回滚演练演练的产出物就是 runbook:每步命令、预期输出、耗时、验证点、回滚触发条件。窗口当天照单执行,把人为决策降到零。
什么时候不选 pg_upgrade
pg_upgrade 虽快,但以下场景需要重新评估选型:
| 场景 | 问题 | 更合适的方案 |
|---|---|---|
| 应用要求全过程可写 | pg_upgrade 窗口内数据库不可用 | 逻辑复制迁移 |
| 需要借机重构表结构/清理历史包袱 | 原地升级原样保留一切 | pg_dumpall 重建或逻辑复制(新库按新结构初始化) |
| 跨操作系统/字节序迁移 | pg_upgrade 要求同平台兼容(架构、大小端) | pg_dump / 逻辑复制 |
| 多个小版本滞留 + 环境漂移严重 | 原地升级保留所有漂移,风险叠加 | 重建式迁移(顺带对齐环境基线) |
| 无法保证执行期间文件系统稳定性 | 拷贝模式中途失败要重来 | 逻辑复制(可断点追赶) |
关于跨架构:pg_upgrade 官方要求新旧集群在同一架构与字节序下(x86_64 → x86_64 可以,x86 → ARM 需实测验证;大小端不同则明确不可)。跨云/跨机房迁移场景优先考虑逻辑复制或 pg_dump。
升级后的验证清单
Upgrade Complete 之后、放流量之前,按固定清单逐项验证(可做成脚本,见实战示例):
□ server_version 确认为目标版本
□ 数据库清单与旧集群一致(\l 对比)
□ 角色与权限一致(\du 对比;globals 齐全)
□ 核心表行数抽样对比(升级前在旧库记录基准)
□ 序列当前值抽样(last_value 对比)
□ 约束与索引数量对比(每库 count(pg_constraint) / count(pg_index))
□ 扩展清单与版本对比(pg_extension)
□ 写入探测(事务内 INSERT 后 ROLLBACK)
□ 复制拓扑按计划恢复(槽、备库、逻辑订阅)
□ pg_stat_statements 已重置并开始积累新基线其中「约束与索引数量对比」是最容易被跳过但价值很高的一项:pg_upgrade 失败遗留的半成品、或人为在建对象时的意外,都会在这两个计数的差异里现形。
容器与发行版部署的差异
不同部署形态下,pg_upgrade 的操作面略有差异,runbook 要按实际形态裁剪:
- Debian/Ubuntu(pg_ctlcluster 体系):数据目录与配置按版本目录并存(
/etc/postgresql/16/main与/etc/postgresql/17/main),initdb 由pg_createcluster完成,pg_upgrade 的-d/-D指向/var/lib/postgresql/<v>/main,配置在/etc下需要手工对齐。 - RHEL 系(PGDG 包):
/var/lib/pgsql/<v>/data并存,postgresql-setup --upgrade类封装本质也是调用 pg_upgrade,建议绕开封装直接手工执行以获得完整控制。 - 容器化部署:镜像里同时提供两个版本的二进制时,pg_upgrade 在同容器内执行(挂载两份数据目录);Kubernetes StatefulSet 场景通常采用「新版本新集群 + 逻辑复制/恢复初始化」而不是原地 pg_upgrade,因为原地升级与有状态工作负载的滚动语义冲突。
- 源码编译:新旧二进制目录并存(
--prefix区分),其余流程与包管理一致。
无论哪种形态,核心动作(停库 → --check → pg_upgrade → 收尾)不变,变的只是二进制与目录的路径组织。
演练环境的具体搭法
「克隆环境演练」说起来一句话,落地时有三条常见路径,按保真度排序:
# 路径 A:文件系统快照(保真度最高,分钟级完成克隆)
# 适用于可短暂冻结写入的演练,或从备份存储拉起的场景
# 例:LVM 快照 / 云盘快照后挂到演练机
# 路径 B:pg_basebackup 物理复制(保真度高,可在线取)
pg_basebackup -h 生产主库 -U repl -D /rehearsal/pg16 \
-X stream -P --checkpoint=fast
# 路径 C:生产级备份恢复(顺带验证备份可用性,一举两得)
# 用最近的物理备份 + WAL 归档恢复到演练机(PITR 到最新)搭好后演练前做一次「指纹登记」,确保演练对象与生产一致:
# 演练环境登记:规模指纹
du -sh /rehearsal/pg16 && \
psql -c "SELECT pg_size_pretty(sum(pg_database_size(datname))) FROM pg_database WHERE datallowconn;"
# 与生产数据目录大小对比,偏差 < 5% 才算有效演练演练机磁盘规格(尤其 IOPS 与吞吐)尽量对齐生产——拷贝模式的耗时主要由磁盘吞吐决定,演练机磁盘慢一档,实测时间就会误导窗口申报。
实战示例
示例一:完整实战——PG16 → PG17(link 模式)
以下是一次标准升级的完整命令序列(环境:单主实例,数据 800 GB,窗口 60 分钟)。
窗口外准备(提前数天):
# 1. 安装新版本包(旧版本包保留)
apt-get install postgresql-17
# 2. initdb 新集群(参数与旧集群对齐)
/usr/lib/postgresql/17/bin/initdb \
-D /var/lib/postgresql/17/main \
--locale=C.UTF-8 --encoding=UTF8
# 3. 拷贝并审查配置:postgresql.conf / pg_hba.conf 逐项合并新版本默认值差异
diff /etc/postgresql/16/main/postgresql.conf /etc/postgresql/17/main/postgresql.conf
# 4. 演练环境完整走一遍流程(含 --check 与正式升级),记录耗时窗口内执行:
# 1. 停应用流量(维护页/连接池拒绝新连接),等待存量事务排空
psql -c "SELECT count(*) FROM pg_stat_activity WHERE xact_start IS NOT NULL;"
# 2. 备份基线(窗口初,物理基线或逻辑全量,二选一按预案)
pg_dumpall -f /backup/pre_upgrade_$(date +%F).sql # 演练测得需 25 分钟则提前到窗口外增量方式
# 3. 优雅停止两个集群
pg_ctlcluster 16 main stop
pg_ctlcluster 17 main stop
# 4. dry-run 最后确认
pg_upgrade \
-b /usr/lib/postgresql/16/bin -B /usr/lib/postgresql/17/bin \
-d /var/lib/postgresql/16/main -D /var/lib/postgresql/17/main \
--socketdir /var/run/postgresql \
-c
# 5. 正式升级(link + 并行)
time pg_upgrade \
-b /usr/lib/postgresql/16/bin -B /usr/lib/postgresql/17/bin \
-d /var/lib/postgresql/16/main -D /var/lib/postgresql/17/main \
--socketdir /var/run/postgresql \
--link -j 8Performing Upgrade
------------------
Setting locale and encoding for new cluster ok
Creating databases in the new cluster ok
Setting old cluster OID to new cluster databases ok
Setting schema and data of next system catalog snapshot ok
Restoring database schemas in the new cluster ok
Copying user relation files ok
Setting next OID for new cluster ok
Sync data directory to disk ok
Creating script to analyze new cluster ok
Upgrade Complete
----------------
Optimizer statistics are not transferred by pg_upgrade.
Once you start the new server, consider running:
./vacuumdb --all --analyze-in-stages# 6. 统计快速补齐(数分钟)
/usr/lib/postgresql/17/bin/vacuumdb --all --analyze-in-stages
# 7. 启动新集群并冒烟验证
pg_ctlcluster 17 main start
psql -c "SHOW server_version;"
psql -c "SELECT count(*) FROM orders;" # 核心表行数抽样
# 8. 扩展对齐
psql -c "SELECT extname, extversion FROM pg_extension ORDER BY 1;"
psql -c "ALTER EXTENSION pg_stat_statements UPDATE;"
# 9. 封存旧目录(link 模式强制)
systemctl disable postgresql@16-main 2>/dev/null || true
chmod -R a-w /var/lib/postgresql/16/main
# 10. 放开应用流量,进入观察期窗口外收尾(当天/次日):
# 11. 重建备库(高可用恢复,见物理备份页)
pg_basebackup -h 主库 -U repl -D /var/lib/postgresql/17/standby \
-X stream -C -S standby_b -R -P
# 12. 观察期 3~7 天:pg_stat_statements 对比、日志错误扫描、巡检对比
# 13. 观察期结束且决定不回滚后,清理旧目录(走变更评审)示例二:含用户自定义表空间的升级
# 旧集群有一个自定义表空间 /data/pg_tsp_hot
ls -l /var/lib/postgresql/16/main/pg_tblspc/
# 41012 -> /data/pg_tsp_hot
# 新集群 initdb(表空间目录 pg_upgrade 会自建链接)
/usr/lib/postgresql/17/bin/initdb -D /var/lib/postgresql/17/main --locale=C.UTF-8
# --check 会校验表空间可达性;正式升级时表空间内文件同样走 link
pg_upgrade -b .../16/bin -B .../17/bin \
-d /var/lib/postgresql/16/main -D /var/lib/postgresql/17/main \
--link -j 8
# 升级后验证新集群的表空间链接结构
ls -l /var/lib/postgresql/17/main/pg_tblspc/
# 应与旧集群结构一致,指向同一批表空间目录link 模式下,表空间目录(/data/pg_tsp_hot)内的文件同样与新集群共享——封存策略必须把这些目录一并 chmod a-w。
示例三:升级失败的恢复演练
演练环境故意制造一次失败(表空间目录权限错误),验证恢复流程:
# 制造失败:表空间目录对 postgres 不可写
chmod 000 /data/pg_tsp_hot
pg_upgrade ... --link
# 输出中 Copying user relation files 阶段报错退出failure, exiting
Consult the last few lines of "pg_upgrade_dump_16384.log" for the probable cause of the failure.# 恢复步骤
# 1. 旧集群完好性确认:用旧二进制可正常启动(演练环境验证)
pg_ctlcluster 16 main start && psql -c "SELECT 1;" && pg_ctlcluster 16 main stop
# 2. 废弃新集群
rm -rf /var/lib/postgresql/17/main
# 3. 修复根因
chmod 750 /data/pg_tsp_hot
# 4. 重新 initdb + --check + pg_upgrade
/usr/lib/postgresql/17/bin/initdb -D /var/lib/postgresql/17/main --locale=C.UTF-8
pg_upgrade ... --link -j 8 # 本次成功示例四:克隆模式(--clone)的启用与验证
# 确认文件系统支持(XFS 示例)
xfs_info /data | grep -o 'reflink=1'
# initdb + pg_upgrade 一路用 clone
pg_upgrade \
-b /usr/lib/postgresql/16/bin -B /usr/lib/postgresql/17/bin \
-d /var/lib/postgresql/16/main -D /var/lib/postgresql/17/main \
--clone -j 8
# 升级耗时对比(同一演练环境实测示例)
# 默认拷贝: 42 分钟 --link: 6 分钟 --clone: 7 分钟
# clone 模式磁盘逻辑占用双份、物理共享;旧目录封存策略与 link 相同示例五:多库实例的升级后验证脚本
一个承载多个业务库的实例,升级后的验证不能只查 postgres 库。用脚本遍历所有库执行对比:
#!/usr/bin/env bash
# verify_upgrade.sh —— 升级后多库验证(升级前先在旧库跑一遍留基准)
set -u
echo "=== 数据库清单 ==="
psql -Atc "SELECT datname FROM pg_database WHERE datallowconn ORDER BY 1;"
echo "=== 角色清单 ==="
psql -Atc "SELECT rolname FROM pg_roles WHERE rolname NOT LIKE 'pg\_%' ORDER BY 1;"
# 逐库收集对象计数(升级前跑一遍存为 baseline.txt 供 diff)
for DB in $(psql -Atc "SELECT datname FROM pg_database
WHERE datallowconn AND datname NOT LIKE 'template%';"); do
echo "--- database: $DB ---"
psql -d "$DB" -Atc "
SELECT 'tables=' || count(*) FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema')
UNION ALL
SELECT 'indexes=' || count(*) FROM pg_indexes WHERE schemaname NOT IN ('pg_catalog','information_schema')
UNION ALL
SELECT 'views=' || count(*) FROM pg_views WHERE schemaname NOT IN ('pg_catalog','information_schema')
UNION ALL
SELECT 'sequences='|| count(*) FROM pg_sequences
UNION ALL
SELECT 'ext=' || count(*) || ':' || string_agg(extname || '/' || extversion, ',')
FROM pg_extension;"
done
echo "=== 序列基准(切换前在旧库取,对比新库) ==="
for DB in appdb reportdb; do
psql -d "$DB" -Atc "
SELECT schemaname || '.' || sequencename || '=' || last_value
FROM pg_sequences WHERE last_value IS NOT NULL;"
done=== 数据库清单 ===
postgres
appdb
reportdb
--- database: appdb ---
tables=142
indexes=287
views=19
sequences=38
ext=5:plpgsql/1.0,pg_stat_statements/1.10,pg_trgm/1.6,...升级前在旧库执行输出存为 baseline_old.txt,升级后在新库执行输出 baseline_new.txt,diff 两份文件——所有对象计数的差异必须逐条有解释(预期内的差异只有 pg_stat_statements 等扩展版本号)。这份对比就是「数据没丢、结构没变」的机器证明,直接归档进变更单。
注意事项
统计信息不随升级迁移。 pg_upgrade 明确不搬 pg_statistic,新集群启动后的第一批查询是在「无统计」状态下规划的。vacuumdb --all --analyze-in-stages 必须在业务流量进入前完成,业务高峰前再做一轮完整 ANALYZE。
旧版本二进制在升级完成前不能卸载。 pg_upgrade 的临时 postmaster 由 -b 指定的旧二进制拉起;升级 OS 包时保留旧大版本的包(多数发行版按大版本并存安装,天然满足)。
两个集群必须处于「干净停止」状态。 直接 kill -9 后立即跑 pg_upgrade 会因 WAL/恢复状态不一致被检查拦截。升级前用 pg_ctl stop -m fast 优雅关闭,并确认日志中无未完成的恢复提示。
--link 之后旧目录是「活的雷区」。 它与新集群共享物理文件,任何针对旧目录的启动、vacuum、甚至误挂载都可能损坏新集群。封存动作(停服务、去写权限)要在升级完成的第一时间执行,并写进 runbook。
pg_upgrade 不处理认证与连接的兼容性。 pg_hba.conf、用户密码、连接池配置需要人工对齐新集群;若涉及认证方式变更(如 md5 → SCRAM),提前在旧集群完成密码重置再升级。
执行期间的端口与 socket 冲突。 pg_upgrade 会以临时端口拉起两个 postmaster;若默认端口被占用或 socket 目录不可写,用 -p/-P 与 --socketdir 显式指定。容器/受限环境下这点尤其常见。
--clone 不是所有文件系统都支持。 ext4 不支持 reflink;在不支持的文件系统上使用会报错或退化(取决于实现版本),执行前先验证(xfs_info 等),不要赌。
升级后先别急着清理。 新集群的 pg_upgrade_internal.log、升级输出与旧目录在观察期内全部保留;「升级完成」的定义是观察期结束 + 回滚窗口关闭,而不是 pg_upgrade 输出 Upgrade Complete。
「能启动」不等于「升级成功」。 半完成状态下新集群可能勉强可以启动、甚至能查到部分数据,但元数据一致性已不可信。判定标准只有一个:pg_upgrade 自己输出 Upgrade Complete;除此之外的一切状态都按失败处理(废弃新集群重来)。
升级会重置 pg_stat_statements 等运行时统计。 升级前的慢查询基线在旧集群里,升级后一切从零。如果依赖它做容量与性能回归对比,升级前把 pg_stat_statements 的关键指标导出存档,观察期后用同样口径重新采集对比。
最佳实践
- 每一步先
--check,正式执行前的最后一次--check在目标环境跑。 它拦截的是结构性失败(版本、表空间、prepared tx、扩展),性价比无可替代。 - 模式选择按「clone > link > 拷贝」的优先级评估。 文件系统支持 reflink(PG15+
--clone)就优先 clone;否则磁盘够用选拷贝(回滚最干净);磁盘紧张才选 link 并重构回滚预案。 -j并行度对齐 CPU 核数,并用演练校准总耗时。 窗口预算必须来自实测(克隆环境跑两轮),不要按官方文档的「很快」做排期。- initdb 参数逐项对齐旧集群。 locale、编码、checksum、wal 段大小;不一致项要么在 initdb 时对齐,要么在
--check报错后回退重初始化,绝不带着侥幸进入正式升级。 - 配置文件手工合并而不是整份覆盖。 新版本的
postgresql.conf有新增/废弃参数,逐项合并并记录 diff;pg_hba.conf同样按新集群路径核对。 - 升级窗口的动作顺序固定为:排空事务 → 备份基线 → 停库 → --check → 升级 → vacuumdb 分阶段分析 → 冒烟 → 封存旧目录 → 放流量。 每步的验证点写入 runbook,演练时逐条打勾。
- 扩展对齐分两步:先核对
.so产物存在,再ALTER EXTENSION UPDATE。 OS 包升级 → pg_upgrade → 扩展 SQL 对齐,三者的顺序不可颠倒。 - 高可用集群的升级计划必须包含备库重建与验证。 主库升级完成后集群处于单点状态,
pg_basebackup重建备库并确认复制追平(配合 pg_basebackup 物理备份)才算变更收尾。 - 观察期内用巡检与
pg_stat_statements做前后对比。 性能回归(个别查询计划变化)是升级后最常见的问题,发现后优先用ANALYZE/ 计划修正解决,个别场景考虑会话级SET回退行为。 - 失败恢复原则烂熟于心:旧集群不动、新集群废弃重来。 任何「修复半升级集群」的尝试都比删除重建危险;把这条写进 runbook 的失败分支。
- 每次升级产出 runbook 并归档,下次升级先读上一份。 团队的升级能力来自演练与复盘的积累,而不是工具本身的易用性。
下一节
继续学习:psql 元命令进阶(\pset)
参考链接
- PostgreSQL 官方文档:pg_upgrade
- PostgreSQL 官方文档:Upgrading a PostgreSQL Cluster(总览)
- PostgreSQL 官方文档:initdb
- PostgreSQL 官方文档:vacuumdb(--analyze-in-stages)
- PostgreSQL 官方文档:pg_basebackup
- PostgreSQL 官方文档:pg_upgrade 相关的 Release Notes(--clone 等特性引入记录)
- PostgreSQL 官方文档:ALTER EXTENSION
- PostgreSQL 官方文档:文件系统克隆与 copy_file_range 说明(pg_upgrade --clone)
- PostgreSQL 官方文档:Backup and Restore
- PostgreSQL Wiki:Upgrading a PostgreSQL Cluster