同一大版本内的次版本升级,不需要 pg_upgrade,编译新版本覆盖二进制即可。本文给出完整流程、每步的判定标准,以及七个实际踩过的坑。适用于源码编译部署、离线内网、不允许停机太久的场景。
一、先弄清楚什么时候需要 pg_upgrade
PostgreSQL 的版本号是三段式:主版本.次版本,例如 14.23。第一个数字是主版本(major),后面的是次版本(minor)。
两种情况,处理方式完全不同:
| 场景 | 示例 | 数据目录格式 | 处理方式 |
|---|---|---|---|
| 主版本升级 | 13.x → 14.x | 不兼容,需要转换 | pg_upgrade 或 pg_dumpall 导出再导入 |
| 次版本升级 | 14.23 → 14.24 | 兼容,无需转换 | 编译新版本,覆盖二进制,直接启动 |
次版本升级不改数据目录格式,PostgreSQL 官方把次版本限定为安全修复与缺陷修复,不引入格式变更。所以升级的本质就是把 /usr/local/pgsql/bin 下的程序换成新版本,数据文件一个字节不动。
这种方式的三个特点:
- 快:不涉及数据转换,停服时间取决于备份与编译耗时,通常 20 到 40 分钟
- 安全:数据文件全程不参与,只要参数不出错,数据不会损坏
- 风险集中:所有风险集中在两个环节,参数配错与属主搞错
本文按六步走,每一步给出判定标准,出现不符合的情况就停下来处理。
二、升级前必须做的三件保险
2.1 全量备份
数据库运行中执行,不要先停库。停库之后备份当然也能做出文件,但那样一旦备份出了问题、库又停了,就没有退路了。
su postgres
pg_dumpall -U postgres > /data/pgdata/full_backup_$(date +%Y%m%d).sql
三个细节:
pg_dumpall 而不是 pg_dump 。前者备份整台实例,包含全部数据库、数据库角色、建库语句;后者只备份单个库,且不含 CREATE DATABASE。回滚时需要整实例恢复,必须用 pg_dumpall。
-U postgres 不能省 。本地 socket 连接默认用当前系统用户名当角色名,以 root 身份执行会报 role "root" does not exist。
$(date +%Y%m%d) 自动生成日期,便于区分多次备份,也避免手工输入出错。
命令成功时屏幕上什么都不会显示 ,直接回到提示符。这不是卡住,是 pg_dumpall 的默认行为。数据量大时可能静默几分钟到十几分钟,期间不要按 Ctrl+C,中断会得到一个不完整的备份文件。
2.2 备份有效性校验
命令没报错,不代表备份可用。必须核对三项。
ls -lh /data/pgdata/full_backup_*.sql
预期:
-rw-r--r--. 1 postgres postgres 4.8K 10月 21 14:51 full_backup_20261021.sql
第一项,文件大小非零。 这是最容易出事的地方。shell 的输出重定向是「先创建文件、再执行命令」,命令失败时文件已经留下了,形成一个 0 字节的空文件,名字和正常备份完全一样。之后如果拿这个文件去恢复,等于没有备份。
第二项,属主是 postgres。 如果显示 root root,后续用 postgres 身份覆盖会失败,因为目录权限只作用于新建动作,不改变已有文件的权限。修正:
chown postgres:postgres /data/pgdata/full_backup_20261021.sql
第三项,内容含建库语句。
grep -c "CREATE DATABASE" /data/pgdata/full_backup_*.sql
返回大于 0 的数字即正常,返回 0 说明备份不完整需要重做。
再抽查文件头:
head -20 /data/pgdata/full_backup_*.sql
应该看到 PostgreSQL database dump 字样和版本标记(本场景是 Dumped from database version 14.23)。
口诀:备份三查,查大小、查属主、查内容。
2.3 回滚条件
两种,至少满足一种:
虚拟机快照 。在虚拟化平台对服务器打快照,名称带日期便于识别,例如 before_pg1424_20261021。恢复时间通常十分钟以内,是最可靠的退路。
备份文件 + 旧版本源码 。没有快照条件时,备份文件与旧版本源码目录是唯一的退路。旧版本源码目录(如 /opt/postgresql-14.23)在验证通过前不能删。
两者都没有,不要开始升级。
三、停服
3.1 顺序:先应用,后数据库
systemctl stop appsvc
systemctl stop postgresql.service
systemctl status postgresql.service
appsvc 是应用服务名,按实际环境的服务名替换。顺序反了会怎样:应用还在运行并不断尝试重连数据库,日志里刷满连接失败,数据库也可能因为活跃连接而停不干净。
系统上有 service 和 systemctl 两套命令,service appsvc stop 与 systemctl stop appsvc 效果相同,后者是新写法,优先用 systemctl。
3.2 确认真的停了
ps -ef | grep postgres | grep -v grep
必须无输出。有输出说明进程还在,等十秒重试停止命令,多次无效时记下 PID 单独终止:
kill <PID>
数据库进程没退干净就替换二进制,可能导致数据文件状态不一致。这一步不能省。
3.3 关于 active (exited)
停完之后 systemctl status 会显示:
Active: inactive (dead)
启动后则是:
Active: active (exited)
active (exited) 这个状态容易被误读成「服务已经退出了」。实际情况是:这类服务由 SysV init 脚本经 systemd 包装生成,查看 Loaded 一行能看到 (/etc/rc.d/init.d/postgresql; generated) 这样的路径与 generated 标记。启动脚本执行完 fork 出后台进程后就退出了,systemd 记录脚本的退出状态为 active,但不再跟踪数据库进程的 PID。
所以:
active (exited)不代表数据库能接受连接- 数据库是否可用,只能靠
psql连一次确认
这个状态还有一个副作用:不能用 systemctl restart 的返回结果判断数据库是否已经就绪,重启后要等几秒再连。
四、编译新版本
4.1 解压
cd /opt
tar -xzf /root/postgresql-14.24.tar.gz
ls -d /opt/postgresql-14.24
解压前先校验安装包:
md5sum postgresql-14.24.tar.gz
与官方 md5 文件比对,一致才继续。
4.2 configure 参数必须与原环境完全一致
这是整个升级过程中最关键的一步。
cd /opt/postgresql-14.24
./configure --prefix=/usr/local/pgsql --with-pgport=5433 --datadir=/data/pgdata
三个参数一个都不能改:
| 参数 | 写错会怎样 |
|---|---|
--prefix |
新程序装到别处,系统仍调用旧程序,select version() 还是 14.23,看着像升级失败 |
--with-pgport |
默认端口变化。已有 postgresql.conf 里显式指定了端口时不受影响,但任何依赖默认值的脚本会连错端口 |
--datadir |
时区文件、系统视图定义装到别处,启动时找不到时区文件,直接报错退出 |
参数不是靠记忆,要靠反查。 两种方法:
# 方法一:从已有安装里读
/usr/local/pgsql/bin/pg_config --configure
# 方法二:如果旧版本源码树还在
cd /opt/postgresql-14.23
grep -E "^(prefix|datadir|DEF_PGPORT) " src/Makefile.global
方法一会打印出当初编译时的完整参数列表,最可靠。
4.3 --datadir 不是数据目录
这一点需要单独说明,因为踩过的人不少。
configure --help 里对 --datadir 的描述是:
--datadir=DIR read-only architecture-independent data [DATAROOTDIR]
只读数据 ,指 share 目录下的内容:时区文件、postgres.bki、系统视图 SQL、全文检索词典。数据库数据目录(PGDATA,存放 base、global、pg_wal 的那个)由 initdb -D 决定,与编译参数无关。
但实际部署中经常出现 --datadir 与 PGDATA 指向同一路径的情况。这时 make install 会以 root 身份往数据目录里写时区文件,文件属主变成 root,而数据库进程以 postgres 运行读不了,启动报错。这就是下一节要处理的问题。
反查当前环境实际用的什么路径:
find / -name postgresql.conf -type f 2>/dev/null | grep -v sample
返回路径的父目录就是 PGDATA。如果它和 pg_config --configure 里的 --datadir 相同,说明两者重合,安装后必须处理属主。
4.4 核对参数是否真的写进去了
configure 接受参数不等于参数生效。唯一的证据是生成的 Makefile.global:
grep -E "^(prefix|datadir|DEF_PGPORT) " src/Makefile.global
预期:
prefix = /usr/local/pgsql
datadir = /data/pgdata
DEF_PGPORT = 5433
三行都要在,数值与预期一致。缺行或数值不对,说明参数没传进去,执行 make distclean 清除后重新配置。直接重跑 configure 可能沿用之前的缓存结果。
4.5 编译
make -j$(nproc); echo "退出码=$?"
只看退出码,不看输出。 PostgreSQL 14 的 Makefile 不会打印总结性的成功提示,输出几万行里找不到「successfully」这种字样是正常的,别在日志里翻。
退出码=0:成功- 其他:失败
失败时定位:
make -j$(nproc) 2>&1 | grep -n "error:" | head -20
4.6 确认编译产物
src/backend/postgres --version
postgres (PostgreSQL) 14.24
注意这是源码目录里的程序,还没装到系统。这里对只说明编译成功。
五、安装后的两件必做
5.1 安装
make install; echo "退出码=$?"
退出码 0 即成功。
5.2 修正时区文件属主
这是所有步骤里最容易漏、后果最直接的一步。
find / -name "timezone" -type d 2>/dev/null
返回结果可能有两类路径:
$PGDATA/timezone(当--datadir与 PGDATA 重合时)$prefix/share/timezone(默认情况)
以 find 返回的实际路径为准,然后整目录修正:
chown -R postgres:postgres /data/pgdata/timezone
chown -R postgres:postgres /data/pgdata/timezonesets
-R 不能省。时区目录下按洲和城市分层存放,例如 timezone/Asia/Shanghai,只改 timezone 目录本身的属主,里面的文件还是 root,照样读不了。
核对:
ls -ld /data/pgdata/timezone
ls -ld /data/pgdata/timezonesets
预期两行都以 postgres postgres 开头。
为什么这一步必须做 :make install 由 root 执行,写进去的文件属主是 root。数据库进程以 postgres 身份运行,对这些文件没有读权限。启动时会报:
FATAL: could not open file ".../timezone/Asia/Shanghai": Permission denied
有些文档建议等报错之后再处理,那时服务已经起不来,排障时间更长。主动在启动前做完,省事。
5.3 确认系统目录的程序版本
/usr/local/pgsql/bin/postgres --version
postgres (PostgreSQL) 14.24
这一步和 4.6 的区别:4.6 查源码目录,这里查系统安装目录。两处都是 14.24,才算安装真正覆盖成功。如果这里还是 14.23,检查 --prefix 是否写错,然后重跑 make install。
5.4 更新共享库
echo /usr/local/pgsql/lib > /etc/ld.so.conf.d/pgsql.conf
ldconfig
两条命令都无输出,安静返回即成功。作用是让系统动态链接器找到新版本的 libpq.so。省略的话,链接 PostgreSQL 库的程序在运行时可能加载到旧版本库或报找不到。
如果这个配置文件已存在且内容相同,第一条可省略,直接 ldconfig。
六、启动与验证
6.1 启动
systemctl start postgresql.service
systemctl status postgresql.service
看到 Starting PostgreSQL: ok 与 Active: active (exited) 即为脚本执行成功。
无 systemd 的环境用 pg_ctl:
su postgres
pg_ctl -D /data/pgdata -l /data/pgdata/logfile start
预期 server started。
两种方式不要混用 。用 systemctl 启动的实例,systemd 记录了启动脚本的 PID;用 pg_ctl 启停时 systemd 不知情,systemctl status 显示的状态会与实际不符,排查时容易误判。
6.2 启动失败看日志
tail -50 /data/pgdata/logfile
日志位置取决于启动方式:用 pg_ctl 启动时是 -l 参数指定的文件;用 systemctl 启动时一般在数据目录下,文件名可能是 logfile 也可能是 serverlog。不确定时按时间排序看:
ls -lt /data/pgdata/ | head -10
日志里以 FATAL 开头的行就是失败原因。
6.3 验证版本:必须查服务端
su postgres
psql -U postgres -c "select version();"
预期:
PostgreSQL 14.24 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 8.5.0 20210514, 64-bit
这是判定升级成功的唯一依据。
只跑 psql --version 是不够的:
psql --version
# psql (PostgreSQL) 14.24
这条查的是客户端程序 版本。客户端与服务端是两个进程,可能版本不一致。升级时如果只换了部分二进制,或者 PATH 指向了不同目录,完全可能出现客户端 14.24、服务端还是 14.23 的情况,而 psql --version 显示正常,误导判断。
两条都跑,都显示 14.24 才稳:
psql --version
psql -U postgres -c "select version();"
6.4 验证数据
版本对了,还要确认数据在。三条命令,都在 postgres 身份下执行。
# 库清单
psql -U postgres -c "\l"
# 业务库的表清单
psql -U postgres -d appdb -c "\dt"
# 关键表行数
psql -U postgres -d appdb -c "select count(*) from devices;"
appdb 与 devices 按实际替换。预期库数量、表数量、行数与升级前一致。
升级前建议先采集一份基线,升级后逐项对比:
psql -U postgres -c "select version();"
psql -U postgres -c "\l"
psql -U postgres -d appdb -c "\dt"
psql -U postgres -d appdb -c "select count(*) from devices;"
grep -E "^listen_addresses|^port|^max_connections" /data/pgdata/postgresql.conf
6.5 启动应用
数据核对通过后再起应用,顺序不能反。数据库没起好就起应用,应用会连续报连接失败,日志里一堆噪音,掩盖真正的问题。
systemctl start appsvc
systemctl status appsvc
预期 active (running)。
然后登录应用界面,确认设备列表、策略配置等页面能正常打开、数据能正常显示。应用报数据库连接错误时,按顺序查四项:
- 应用服务是否已重启并加载了新配置
- 应用配置里的数据库地址与端口是否与实际一致
pg_hba.conf是否允许应用主机连接postgresql.conf的listen_addresses是否与升级前一致
七、七个实际踩过的坑
7.1 备份文件 0 字节
现象 :ls -lh 显示大小为 0。
原因:命令执行失败,但重定向已经先创建了文件。
处理:删掉文件,查明失败原因后重做,重新核对大小。
这个坑的危险在于文件名字与正常备份完全一样,ls 不看大小的话根本发现不了。真到需要回滚那天才发现备份是空的,就没有退路了。
7.2 role "root" does not exist
现象:
pg_dumpall: error: connection to server on socket "/tmp/.s.PGSQL.5432" failed:
FATAL: role "root" does not exist
原因 :命令漏了 -U postgres,以 root 身份连接时 PostgreSQL 拿系统用户名当角色名。
处理 :补上 -U postgres。所有数据库命令统一带这个参数,不管当前是什么身份。
7.3 权限不够(备份写不进目录)
现象:
-bash: /data/pgdata/full_backup.sql: 权限不够
原因:目录或目标文件属主是 root,postgres 没有写权限。
处理:
chown postgres:postgres /data/pgdata
rm -f /data/pgdata/full_backup.sql
rm 那步不能省。已有的 root 属主文件,即使目录交给了 postgres,postgres 也无法覆盖它,因为文件本身的权限没变。必须删掉让 postgres 重新创建。
7.4 时区文件 Permission denied
现象 :启动失败,日志报 could not open file ".../timezone/Asia/Shanghai"。
原因 :make install 以 root 写入,时区文件属主是 root。
处理:
find / -name "timezone" -type d 2>/dev/null
chown -R postgres:postgres <找到的路径>
chown -R postgres:postgres <同级的 timezonesets 目录>
7.5 systemctl 报 Unit not found
现象:
Failed to start postgresql.service: Unit postgresql.service not found.
原因 :源码编译安装不会创建 systemd 单元。只有通过 rpm 或安装脚本部署的环境才有 postgresql.service。
处理 :改用 pg_ctl,或者自己写单元文件。判断环境属于哪一类:
ls /etc/rc.d/init.d/postgresql 2>/dev/null && echo "有 SysV 脚本"
ls /usr/lib/systemd/system/postgresql* 2>/dev/null && echo "有 systemd 单元"
两者都没有,就是纯源码安装,只能用 pg_ctl。
7.6 端口被占用
现象 :日志报 could not bind 或 address already in use。
原因:旧进程没退干净,或别的程序占了同一端口。
处理:
ps -ef | grep postgres | grep -v grep
ss -lntp | grep 5433
确认是旧数据库进程后再 kill,别误杀其他服务。
7.7 数据目录版本不匹配
现象:
FATAL: database files are incompatible with server
原因 :启动的程序与数据目录的版本不一致。常见两种情况:安装没成功(make install 覆盖失败),或者启动时调用的是错误路径下的程序(PATH 里有旧版本目录)。
处理:
/usr/local/pgsql/bin/postgres --version # 安装目录的程序版本
which pg_ctl # 实际调用的是哪个 pg_ctl
ls -l /etc/rc.d/init.d/postgresql # 服务脚本里写的是哪个路径
三处都要指向新版本。which pg_ctl 指向旧目录时,说明 PATH 里有旧版本残留,用绝对路径启动。
八、回滚
8.1 方式一:快照回滚
虚拟化平台恢复到升级前的快照。整机回到 14.23 与升级前数据,十分钟内完成,结果确定。
注意:恢复会丢失升级后服务器上的所有变更,包括升级操作之外的其他改动。恢复前确认没有需要保留的新内容。
8.2 方式二:重编旧版本 + 备份恢复
没有快照条件时使用。
第一步,停服:
systemctl stop appsvc
systemctl stop postgresql.service
第二步,重新编译安装旧版本。参数与升级时完全一致:
cd /opt/postgresql-14.23
./configure --prefix=/usr/local/pgsql --with-pgport=5433 --datadir=/data/pgdata
make -j$(nproc)
make install
ldconfig
chown -R postgres:postgres /data/pgdata/timezone
第三步,恢复数据:
su postgres
psql -U postgres -d postgres -f /data/pgdata/full_backup_20261021.sql
恢复过程会输出大量 SQL 执行信息,中间出现若干 already exists 提示属正常,因为库和表已经存在。最后没有 FATAL 或 ERROR 即为完成。
第四步,启动并验证:
systemctl start postgresql.service
su postgres
psql -U postgres -c "select version();"
预期回到 14.23。
8.3 回滚的两个注意点
备份由哪个版本生成,就用哪个版本的 psql 恢复。 14.23 生成的备份文件头里有对应的版本标记和指令(例如较新版本会输出 \restrict),用不匹配的客户端执行可能出现语法错误。
回滚后同样要跑完整验证。 版本、数据、表结构、应用连接四项都正常,才算回滚完成。看到版本号变回 14.23 只是第一步。
九、判断清单
升级过程中按这张表逐项确认,任一项不符就停下来。
| 阶段 | 判定项 | 通过标准 |
|---|---|---|
| 备份 | 文件大小 | 不为 0 |
| 备份 | 文件属主 | postgres postgres |
| 备份 | 建库语句条数 | 大于 0 |
| 停服 | 服务状态 | inactive (dead) |
| 停服 | 残留进程 | ps 查询无输出 |
| 解压 | 安装包 md5 | 与官方值一致 |
| 配置 | Makefile.global 三项 | prefix / datadir / DEF_PGPORT 与预期一致 |
| 编译 | 退出码 | 0 |
| 编译 | 源码目录程序版本 | 14.24 |
| 安装 | 退出码 | 0 |
| 安装 | 系统目录程序版本 | 14.24 |
| 安装 | 时区目录属主 | postgres postgres |
| 安装 | ldconfig | 无输出,正常返回 |
| 启动 | 服务状态 | active (exited),日志含 Starting PostgreSQL: ok |
| 验证 | 服务端版本 | select version() 返回 14.24 |
| 验证 | 库与表数量 | 与升级前一致 |
| 验证 | 关键表行数 | 与升级前一致 |
| 验证 | 应用功能 | 页面正常,数据可显示 |
十、四句总结
升级这件事,命令不多,风险都集中在几个"不看就发现不了"的地方:
- 备份只看文件在不在,不看大小
- 参数凭记忆写,不反查原环境
- 编译在几万行输出里找成功提示,不看退出码
- 版本只查
psql --version,不查服务端
反过来记四句:
备份看大小,参数靠反查,编译看退出码,升级看服务端。
配套的安装过程可以看另一篇《PostgreSQL 14 源码编译安装实战》,两篇连起来刚好是一条完整的链路:从零装一个实例,再把它升到新版本。