用 pg_upgrade 将 PostgreSQL 9.6 原地升级到 17:停机切换、配置迁移与校验清单

2026-07-16 24 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:12 分钟

从 PostgreSQL 9.6 跨越多个大版本升级到 17,真正困难的通常不是数据文件转换,而是配置兼容、扩展处理、回滚设计,以及升级后的统计信息和索引校验。

对于多 TB 集群,pg_dump 加恢复的停机时间往往无法接受;原生逻辑复制又要求源端至少是 PostgreSQL 10。面对 9.6 这样的老集群,pg_upgrade --link 是一个更实际的选择:它通过硬链接复用数据文件,使核心升级步骤不再随着数据库容量线性增长。不过,停机窗口仍会受到检查、启动、统计信息收集和业务验证耗时的影响,不能只估算 pg_upgrade 命令本身。

几种迁移方式的差异很明确:

方案 优点 主要限制
Dump/Restore 流程直观,能够顺便整理数据 数据越大,导出和恢复越久
原生逻辑复制 可以把停机压缩到最终切换阶段 PostgreSQL 9.6 不能直接使用 PostgreSQL 10 起提供的原生逻辑复制路径
pg_upgrade 复制模式 保留原目录作为独立副本 需要额外磁盘空间,数据复制耗时
pg_upgrade --link 核心转换快,额外空间较少 升级后不能再启动旧集群,回滚依赖升级前快照或备份

--link 创建的是硬链接,旧目录和新目录最初指向相同的底层数据块。因此,升级成功后必须把旧集群视为已经被污染,不能尝试重新启动 PostgreSQL 9.6。需要可靠回滚时,应在执行正式升级前创建经过验证的文件系统或存储卷快照。

硬链接还要求旧、新数据目录位于支持硬链接的兼容文件系统范围内。维护窗口前可以这样实践,先验证两个目录是否允许创建硬链接:

sudo -u postgres touch /pgdata/9.6/.link-test
sudo -u postgres ln /pgdata/9.6/.link-test /pgdata/17/.link-test
sudo -u postgres ls -li /pgdata/9.6/.link-test /pgdata/17/.link-test
sudo -u postgres rm /pgdata/9.6/.link-test /pgdata/17/.link-test

两条 ls 记录应显示相同的 inode。测试失败时,不要继续使用 --link,应调整存储布局或改用复制模式。

新集群要重新配置,不要整份复制旧配置

在 Ubuntu 上安装 PostgreSQL 17,并使用与旧集群一致的 locale 初始化新目录。下面的名称、端口和路径需要按实际环境修改:

sudo apt update
sudo apt install postgresql-17 postgresql-client-17 postgresql-contrib-17
/usr/lib/postgresql/17/bin/psql --version

sudo mkdir -p /pgdata/17/prod
sudo chown -R postgres:postgres /pgdata/17/prod
sudo chmod 700 /pgdata/17/prod

sudo pg_createcluster 17 prod \
  --datadir=/pgdata/17/prod \
  --port=5433 \
  --locale=C.UTF-8 \
  --start

不要直接把 9.6 的 postgresql.conf 覆盖到 17。跨越十余个版本后,一批参数已经删除、改名或改变单位。更稳妥的做法是以 PostgreSQL 17 生成的配置为基础,逐项迁移这些业务相关设置:

  • 连接:listen_addressesmax_connections
  • 内存:shared_bufferswork_memtemp_buffers
  • WAL:wal_levelmax_wal_sizemin_wal_size
  • 归档:archive_modearchive_command
  • 复制:max_wal_sendersmax_replication_slots
  • 超时:statement_timeoutlock_timeout
  • 时区和日志参数

需要重点处理的变化包括:

  • PostgreSQL 12 起不再使用 recovery.conf。恢复参数进入 postgresql.conf,备用节点通过 standby.signal 标记。
  • wal_keep_segments 改为 wal_keep_size,单位也从 WAL 文件数量变成容量。默认 16 MB WAL 段下,可将旧值乘以 16 后按 MB 配置,但仍要结合实际 WAL 段大小确认。
  • min_parallel_relation_size 被拆分为 min_parallel_table_scan_sizemin_parallel_index_scan_size
  • PostgreSQL 16 移除了 promote_trigger_file,应使用 pg_ctl promotepg_promote()
  • PostgreSQL 17 移除了 old_snapshot_thresholdtrace_recovery_messagesdb_user_namespace
  • PostgreSQL 14 起 password_encryption 默认使用 scram-sha-256。应同步检查 pg_hba.conf、客户端驱动和已有用户密码,而不是只修改一个参数。
  • PostgreSQL 15 起 log_checkpoints 默认开启,日志容量规划应把新增记录考虑进去。

复制旧 pg_hba.conf 可以保留访问规则,但这不等于认证兼容性已经验证。尤其是从 md5 迁移到 SCRAM 时,应在预演环境测试所有应用、备份程序和运维客户端。

把检查和正式切换写成可复用的运行手册

部分扩展不能直接跨版本保留。例如存在 repmgr 时,需要先停止 repmgrd,并在旧集群中删除扩展;升级后再安装适用于 PostgreSQL 17 的版本并重新配置。

sudo systemctl stop repmgrd
sudo -u postgres psql -p 5432 -d repmgr \
  -c 'DROP EXTENSION repmgr CASCADE;'

CASCADE 可能删除依赖对象,执行前必须在测试环境确认影响,并保留对象清单和重建脚本。

下面是一份可以改造的切换脚本。它假设旧集群名为 main,新集群名为 prod,数据分别位于 /pgdata/9.6/main/pgdata/17/prod。正式执行前应先把 MODE=check 跑通,再在维护窗口设置 MODE=upgrade

#!/usr/bin/env bash
set -euo pipefail

MODE="${MODE:-check}"
OLD_CLUSTER="main"
NEW_CLUSTER="prod"
OLD_DATA="/pgdata/9.6/${OLD_CLUSTER}"
NEW_DATA="/pgdata/17/${NEW_CLUSTER}"
OLD_BIN="/usr/lib/postgresql/9.6/bin"
NEW_BIN="/usr/lib/postgresql/17/bin"
WORK_DIR="/var/lib/postgresql/upgrade-9.6-to-17"

sudo install -d -o postgres -g postgres -m 700 "$WORK_DIR"
sudo pg_ctlcluster 9.6 "$OLD_CLUSTER" stop
sudo pg_ctlcluster 17 "$NEW_CLUSTER" stop

args=(
  "--old-datadir=$OLD_DATA"
  "--new-datadir=$NEW_DATA"
  "--old-bindir=$OLD_BIN"
  "--new-bindir=$NEW_BIN"
)

if [[ "$MODE" == "check" ]]; then
  args+=(--check)
elif [[ "$MODE" == "upgrade" ]]; then
  args+=(--jobs=8 --link)
else
  echo "MODE must be check or upgrade" >&2
  exit 2
fi

sudo -u postgres bash -c \
  'cd "$1" && exec /usr/bin/time -f "Elapsed: %E" "$2" "${@:3}"' \
  bash "$WORK_DIR" "$NEW_BIN/pg_upgrade" "${args[@]}"

检查模式的输出必须以集群兼容结论结束。检查成功不代表可以省略备份,也不代表应用 SQL 与 PostgreSQL 17 完全兼容;它只说明 pg_upgrade 已检查的升级条件满足要求。

--jobs=8 应按 CPU 和 I/O 能力调整。由于两个数据库集群必须停止,正式切换期间不应再有业务流量;所谓预留资源,主要是为了同机运行的监控、备份代理或其他服务。

启动之前和放量之前是两个不同关口

升级完成后启动新集群:

sudo pg_ctlcluster 17 prod start
sudo pg_ctlcluster 17 prod status

截至 PostgreSQL 17,优化器统计信息不会由 pg_upgrade 完整转移。应在业务放量前执行生成的分析脚本,否则查询优化器可能因为缺少统计信息而选择代价很高的执行计划:

sudo -u postgres env PGPORT=5433 \
  /usr/bin/time -f 'Analyze time: %E' \
  /var/lib/postgresql/upgrade-9.6-to-17/analyze_new_cluster.sh

如果某张大表因超时失败,可以单独补做:

sudo -u postgres psql -p 5433 -d appdb -v ON_ERROR_STOP=1 \
  -c 'SET statement_timeout = 0; ANALYZE VERBOSE public.orders;'

从 10 之前的版本升级时,哈希索引需要重建。pg_upgrade 会生成相应脚本并指出受影响对象。并非每次大版本升级都需要全库 REINDEX;应根据生成的报告处理,在线环境可评估使用 REINDEX ... CONCURRENTLY 来降低锁表影响。

上线前的校验查询

版本、无效索引和扩展版本是最低限度的检查项:

SELECT version();

SELECT
    s.schemaname,
    s.relname AS tablename,
    s.indexrelname AS indexname
FROM pg_stat_user_indexes AS s
JOIN pg_index AS i ON i.indexrelid = s.indexrelid
WHERE i.indisvalid = false;

SELECT
    name,
    default_version,
    installed_version,
    CASE
        WHEN default_version <> installed_version THEN 'NEEDS UPDATE'
        ELSE 'OK'
    END AS status
FROM pg_available_extensions
WHERE installed_version IS NOT NULL
ORDER BY name;

无效索引查询应返回零行。扩展显示 NEEDS UPDATE 时,先确认 PostgreSQL 17 对应的扩展包已经安装,再按扩展文档执行:

ALTER EXTENSION extension_name UPDATE;

同时观察启动日志和应用冒烟测试:

sudo tail -f /var/log/postgresql/postgresql-17-prod.log

应用验证至少应覆盖登录认证、关键读写事务、定时任务、报表查询、复制槽、归档和备份。对核心表补充行数或业务聚合值对比,比单纯确认数据库能够启动更有价值。

删除旧集群之前保留观察期

pg_upgrade 会生成 delete_old_cluster.sh,但不应在刚启动新集群后立即执行。先定义观察期,确认监控、备份、归档、扩展和应用性能稳定,再清理旧目录:

sudo pg_dropcluster 9.6 main --stop
sudo rm -rf /pgdata/9.6/main

删除前再次确认快照或备份能够恢复。使用 --link 后,旧数据目录不是可直接启动的回滚方案;真正的回滚依赖升级前保存的独立副本。

这类跨版本升级的核心运行时间可能很短,但工程工作集中在前后两端:预演配置和扩展兼容性,冻结业务写入,保留可验证的回滚点,重建统计信息和必要索引,再用数据库查询与应用流量共同验证结果。把这些步骤做成可重复执行的运行手册,才能让正式维护窗口变成一次受控切换,而不是现场排障。


相关推荐