数据库运维13 min read次阅读

PG 大版本不能原地升:用 pg_upgrade 把 12 升到 16,停机从小时级压到分钟级

我们 HA-System 那套 PG 还停在 9.2,CentOS 7 自带的老版本。去年开始陆续有麻烦:9.2 早过了 EOL,安全补丁没了;上游要给个 JSONB 索引的新功能,9.2 压根没有;连 psql 客户端新版连上去都一堆 warning。想升,卡在「大版本不能原地升」这件事上——PG 的小版本(比如 15.2→15.4)catalog 不变,升个包重启就行;可一旦跨了大版本(15→16),系统表结构动了,旧数据目录新二进制读不了,直接起会报 catalog version mismatch

老办法是 pg_dumpall 全量导出、新版本初始化后导入。库小的时候没问题,库一大就傻眼:导出要时间、导入更要时间,几千张表、几百 G,导入时还要重建索引、跑约束,停机窗口轻轻松松半天起步。pg_upgrade 解决的就是这个:它不重新灌数据,而是把旧版本的数据文件直接复用给新版本,只在 catalog 层面做必要的改写,停机时间从「导出+导入」降成「改 catalog + 拷贝/建链」。

下面这套在 RHEL 系(含 Alibaba Cloud Linux)上验证过,从 12 升 16。路径以 /var/lib/pgsql/<ver>/data 为例(PGDG 官方包布局),你自己机器上把版本号换一下。

这是 pg_upgrade 最该想清楚的选择,直接决定停机时间和回滚难度。

  • 默认 copy 模式:把旧数据目录整个复制到新数据目录。慢(等于拷一遍全库数据),但旧库原封不动地留着。要是升级后发现问题,把服务指回旧库就能回去——回滚最干净。
  • --link 模式:不拷数据,直接给旧数据文件建硬链接到新目录。快到飞起(几百 G 也就几秒),但代价是硬链接两边共享同一份 inode——pg_upgrade 改写新目录的 catalog 时,旧目录里对应的文件其实也被改了(因为指向同一个文件)。换句话说,用了 link 模式,旧数据目录就不再是「升级前原样」了,回滚不能只靠切回去,得靠事前备份。

经验法则:停机窗口够(夜间维护几小时)、库又大,用 copy 最省心,回滚无脑;停机窗口紧(十几分钟)、盘又是一个文件系统、且你已在升级前对旧库做了物理备份(basebackup 或快照),用 link 把停机压到最短。我们线上几百 G 的库走 link,实际停机约 3 分钟(停旧库→upgrade→起新库→analyze),比 dump/restore 的 4 小时省太多了。

升级前:先 --check,别直接冲

pg_upgrade 有个 --check 模式,只做兼容性校验、不动任何文件。升级前务必先跑它,把问题挡在真正动手之前。

先在目标机装好新版本和同款扩展:

# PGDG 源(RHEL 系)
dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
dnf install -y postgresql16-server postgresql16-contrib
# 你旧库用到的扩展,新版本也必须装齐(下面细说)
dnf install -y postgresql16-pg_stat_statements

# 初始化新集群的数据目录(不启动)
/usr/pgsql-16/bin/postgresql-16-setup initdb
# 或手动:/usr/pgsql-16/bin/initdb -D /var/lib/pgsql/16/data -U postgres

停掉旧库,再跑 check:

systemctl stop postgresql-12

su - postgres -c "/usr/pgsql-16/bin/pg_upgrade \
  --old-datadir /var/lib/pgsql/12/data \
  --new-datadir /var/lib/pgsql/16/data \
  --old-bindir /usr/pgsql-12/bin \
  --new-bindir /usr/pgsql-16/bin \
  --check"

check 通过会打印 Clusters are compatible。常见它会直接拦下来的几类问题:

  1. 扩展版本不对:旧库装了 postgis、timescaledb 这类 C 扩展,而新版本没装或版本不匹配。pg_upgrade 会因为「新集群里找不到这个扩展」或「扩展的 control 文件版本不符」直接报错。解决办法是先把新集群的扩展装到兼容版本。
  2. locale / 编码不一致:旧库是 en_US.UTF-8、新库 initdb 时用了 C,check 会报 lc_collate 不匹配。新集群 initdb 时务必用和旧库完全相同的 locale(initdb --locale=en_US.UTF-8,或省事直接 --locale=C.UTF-8 但得和旧的一致)。
  3. 表空间路径:旧库有自定义表空间指向 /data/pgts,新机器上这个路径不存在或权限不对,check 会提示。提前把目录建好、属主给 postgres。
  4. 旧的 pg_hba / 参数不兼容:这个 check 不拦,但升级后新版本可能不认旧配置项(比如某些 GUC 改名/废弃),要手动处理。

真正开跑:升级

check 干净后,去掉 --check 就是正式升级。想并行加速大库,加 --jobs(按 CPU 数,别超过实际核数):

su - postgres -c "/usr/pgsql-16/bin/pg_upgrade \
  --old-datadir /var/lib/pgsql/12/data \
  --new-datadir /var/lib/pgsql/16/data \
  --old-bindir /usr/pgsql-12/bin \
  --new-bindir /usr/pgsql-16/bin \
  --link \
  --jobs 8"

跑完会生成两个脚本在 $HOME(postgres 的家目录,一般是 /var/lib/pgsql):

  • analyze_new_cluster.sh:对新库做统计信息收集。这步必须跑,否则新库 planner 没有统计信息,查询计划会烂到怀疑人生。可以升级后手动 vacuumdb --all --analyze-only 代替,效果一样。
  • delete_old_cluster.sh:确认新库没问题后再执行,删掉旧数据目录回收空间(link 模式下它其实删的是那份共享文件,谨慎)。

升级成功后起新库:

systemctl start postgresql-16
# 别忘了禁用/移除旧版本的服务,避免开机又起 12
systemctl disable postgresql-12

扩展兼容:最容易翻车的地方

PG 的扩展分两类,升级时待遇完全不同。

纯 SQL 扩展(比如 pg_stat_statements 的某些版本、pg_trgm)只要新集群装了同扩展就能带过去。但C 语言写的扩展(PostGIS、TimescaleDB、pgcrypto 的某些函数、各种第三方 FDW)要求新旧两集群都装了兼容版本的扩展库文件,否则 pg_upgrade 在 Finalizing 阶段会报错回退。

最典型的是 TimescaleDB:它不仅是扩展,还有自己的后台迁移逻辑。正确姿势是先在旧库跑 timescaledb_pre_restore、升级后再 timescaledb_post_restore(具体版本看官方升级指南),而且新旧 TimescaleDB 大版本要对齐,不能拿 12 上的 TimescaleDB 2.5 直接升到 16 上的 2.15 还指望无缝——跨大版本得按官方矩阵逐级来。

PostGIS 同理:旧库是 PostGIS 2.5、新库装 3.4,pg_upgrade 会因为 postgis 扩展的 control/sql 文件版本差太多拒绝。要么新库也装 2.5 先升上去再 ALTER EXTENSION postgis UPDATE,要么升级前就把旧库 PostGIS 升到和新库一致的版本。

踩过的坑:一次升级前没在新集群装 pg_stat_statements,check 阶段没拦(因为它在旧库是「已创建扩展」状态,check 只查兼容性不查缺失),正式 upgrade 跑到一半报 could not find ... pg_stat_statements.control,回退重来,白白浪费一次停机窗口。所以新集群的扩展集要和旧库 SELECT * FROM pg_extension 列出来的逐一对齐,动手前先核对一遍。

那些 pg_upgrade 不替你搬的东西

这点太容易被忽略。pg_upgrade 只搬「数据库对象」:库、表、索引、数据、扩展、角色(其实是全局对象会搬)。下面这些它不碰,得你自己迁移:

  • postgresql.conf / postgresql.auto.conf:新集群用的是新版本默认的配置文件。你旧库调过的 shared_buffersmax_connectionswal_level 全都没带过来。升级前把旧配置 diff 出来,逐条合进新配置,别直接覆盖(新版可能新增/废弃了参数)。
  • pg_hba.conf / pg_ident.conf:访问控制规则不搬。旧库的 IP 白名单、replication 账号条目,得手动复制。我们一次升级后应用连不上,查了半小时才发现 pg_hba 没搬、默认只放行本地。
  • replication slots / 订阅(subscription):逻辑复制槽、物理复制槽不会被复制。如果你旧库是某个下游的发布端(publication),升级后下游会断,得在新库重建 slot 或重新建订阅。这点在做主从架构升级时要专门排期。
  • crontab 里的 pg_dump 等外部调度:指向旧版本的 psql/pg_dump 路径要改。
  • 第三方工具的连接串:监控 agent、备份脚本里写死的 host=/var/lib/pgsql/12 或端口,升完得改。

回滚方案

这是运维最该想在前面的。

  • copy 模式:旧数据目录完全没动。升级后新库任何不对劲,直接 systemctl stop postgresql-16,把服务指回 12 起旧库即可,数据就是升级前那一刻的原样。零风险。
  • link 模式:旧目录已和新库共享文件并被改写,不能靠切回旧库回滚。唯一的退路是升级前对旧库做的物理备份(pg_basebackup 全量 + 当时的 WAL,或云盘快照)。所以走 link 前,先确认备份在手,并且验证过能恢复——别等出事才第一次试恢复。

我们线上那次 12→16 走的是 link,升级前用 pg_basebackup -X stream 备了一份到异地,analyze 跑完做了几笔冒烟查询(建连、跑一条核心报表、插一条测试数据回滚)确认无误,才 rm 掉旧目录。整个过程维护窗口 15 分钟,业务侧基本无感。

一个容易忽略的顺序问题

升级要在低峰 + 应用停写时做。pg_upgrade 做 link/拷贝期间旧库必须全程停着,且这期间不能有新写入,否则新库数据就不一致了。标准流程是:应用侧切只读或停服 → 确认无连接(SELECT count(*) FROM pg_stat_activity WHERE state='active' 应为 0,除自己)→ 停旧库 → check → upgrade → 起新库 → analyze → 冒烟 → 恢复服务。

还有,大版本升级往往伴随默认行为变化,不只是性能参数。比如 15 开始 public schema 默认不再允许普通用户建表(REVOKE CREATE ON SCHEMA public FROM PUBLIC 成了默认),12 升 16 后老应用如果习惯往 public 建表会直接报权限错。这类「语义级」的坑比命令本身更阴,升级前翻一遍目标版本的 release notes 的「Migration」章节,比闷头跑命令重要得多。

升完别急着关终端。留十分钟看新库日志有没有 WARNING: relation ... has no statistics 之类,确认 analyze 真跑过了;再看监控里连接数、慢查询有没有异常。pg_upgrade 让「升级」这件事从体力活变成了几分钟的例行操作,但它搬得动数据,搬不动你那些散落在配置文件、白名单、复制槽里的隐性依赖——那些,得你自己在升级单里一条条列清楚。

分享:

相关文章

PostgreSQL 物理备份与时间恢复(PITR):一次 basebackup 到任意时间点的完整链路
数据库运维12 min read

PostgreSQL 物理备份与时间恢复(PITR):一次 basebackup 到任意时间点的完整链路

pg_dump 是逻辑备份,做不了时间点恢复,库一大还慢得离谱。真要扛误删表、误更新全表这种事故,得靠物理备份加 WAL 归档的 PITR。从建备份账号、开归档、跑 pg_basebackup,到真的把库恢复到「出事前一秒」,把每一步命令和每个坑都摊开讲。

评论区