PostgreSQL 备份报 server version mismatch:检查运行 pg_dump 的容器
数据库升级后 pg_dump 仍来自旧备份容器,导致 server version mismatch。核对两端主版本、调用匹配客户端,并让失败退出码、归档检查和隔离恢复成为验收条件。
数据库升级后,定时备份开始出现这样的错误:
pg_dump: error: aborting because of server version mismatch
pg_dump: detail: server version: 17.1; pg_dump version: 15.8
先比较日志里这两个版本。服务器是 17,实际执行导出的客户端还是 15;数据库容器升级,并不会同时更新另一个备份容器或宿主机上的 pg_dump。
备份脚本如果在导出失败后仍打印成功提示,还需核对退出码传递和产物检查。版本匹配与任务是否误报成功是两个检查点,不能仅凭最后一行日志认定备份可用。
要更新的是导出工具,不是数据库里的数据
pg_dump 可以读取其支持范围内的旧版本服务器,但不能导出比自己主版本更新的服务器。因此 pg_dump 15 连接 PostgreSQL 17 会拒绝执行;连接能成功,并不表示导出兼容。
这里比较的是主版本,两边的补丁号可以不同。对于以后仍要恢复到同一主版本的日常逻辑备份,选用该主版本的客户端比较容易控制兼容范围。不要只换成随手找到的“最新客户端”:较新 pg_dump 生成的输出,不保证能装入比这个客户端主版本更旧的服务器,即使数据最初来自那台旧服务器。
修改客户端镜像、客户端安装路径或备份任务,通常不需要迁移数据库数据目录。不要为了配合旧 pg_dump 降级数据库,也不要把已有数据库卷挂到另一个主版本的服务器镜像下启动。
从失败任务内部确认版本
在真正执行备份的环境里检查,不能只在宿主机终端运行 pg_dump --version。例如,备份由 example-backup 容器执行:
docker exec example-backup pg_dump --version
docker exec example-postgres pg_dump --version
docker exec example-postgres psql -U example_backup -d example_app \
-Atc 'SHOW server_version;'
容器名、账号和数据库名都是占位值,使用前替换。最后一条查询读取服务器版本;pg_dump --version 读取客户端版本,它们回答的是不同问题。连接账号需要有相应访问权限,认证方式沿用现有安全配置。
如果备份任务通过网络连接其他实例,应使用与任务相同的主机、端口、账号和库名查询服务器版本。数据库容器内查到的版本,只有确认它就是备份目标时才有意义。宿主机任务还应核对调用的是哪一个文件:
command -v pg_dump
pg_dump --version
定时任务可能有不同的 PATH。脚本使用了绝对路径时,直接运行那条路径加 --version;安装了新客户端后,旧路径仍可能继续工作在旧版本。
数据库容器自带匹配工具时,可以在那里导出
如果数据库容器已经提供了经过上述检查的匹配版本 pg_dump,可以通过 docker exec 调用它,把标准输出保存到宿主机。这样不必给其他应用容器临时装包,也不需要把数据库端口开放到公网。
下面是 Bash 脚本示例。它导出一个库,采用自定义归档格式;每次运行建立独立目录,成功前只写临时文件。执行账号必须有导出所需的对象读取权限,宿主机运行者必须有 Docker 和备份目录的访问权限。
#!/usr/bin/env bash
set -euo pipefail
umask 077
db_container='example-postgres'
db_user='example_backup'
db_name='example_app'
backup_dir='/srv/backup/example-app'
mkdir -p -- "$backup_dir"
run_dir=$(mktemp -d "$backup_dir/${db_name}-$(date -u +%Y%m%dT%H%M%SZ).XXXXXX")
partial_file="$run_dir/database.dump.partial"
dump_file="$run_dir/database.dump"
trap 'rm -f -- "$partial_file"' EXIT
docker exec "$db_container" pg_dump \
-U "$db_user" -d "$db_name" -Fc -w > "$partial_file"
test -s "$partial_file"
docker exec -i "$db_container" pg_restore --list \
< "$partial_file" > /dev/null
mv -- "$partial_file" "$dump_file"
printf '备份文件已生成:%s\n' "$dump_file"
-Fc 生成适合 pg_restore 读取的归档。这里不用 docker exec -t 分配伪终端;导出文件需要原样接收输出。归档目录检查从标准输入读取文件,因此那条 docker exec 使用 -i 保持输入开启。
-w 禁止弹出密码提示,认证缺失时让任务直接失败。它不免除认证要求。需要密码的部署,应给容器内的实际客户端配置受控密码文件或既有凭据机制;Unix 密码文件权限需为 0600 等仅所属用户可读写的设置。不要把密码写进命令行、公开脚本或备份日志,也不要为运行示例修改数据库为宽松认证。
脚本中的重定向由宿主机 shell 执行,所以文件保存在宿主机的 backup_dir。数据库容器只负责运行客户端。失败时只清理本轮新建的临时文件,保留之前的备份;成功后也不应马上删除旧备份。
如果数据库镜像没有客户端工具,就更新专用备份镜像,让它提供已确认主版本的客户端;同时保留原来的连接网络、认证和调度配置。不要手工修改一次正在运行的容器就认为定时任务永久修复,下一次重建可能丢失改动。镜像标签和升级步骤应对应当前项目版本与数据库主版本。
文件非空、目录能列出,仍需要恢复验证
示例在 pg_dump 成功后检查文件非空,再用 pg_restore --list 读取归档目录。失败会终止脚本,不会走到成功提示。若原来的 Bash 任务使用 pg_dump | gzip,还要检查管道是否启用了 pipefail;否则后面的压缩命令成功,可能掩盖前面的导出失败,压缩文件存在也就失去判断价值。
--list 只说明归档目录可以读取,不证明全部数据完整,更不证明它能恢复到你的目标环境。应准备隔离的测试实例、空的测试数据库和需要的扩展,再用匹配的 pg_restore 验证。示例使用独立测试容器和空测试库,执行前必须确认目标与业务数据库无关:
docker exec -i example-postgres-restore-test pg_restore \
-U example_restore -d example_app_restore_test \
--exit-on-error --no-owner --no-privileges \
< /srv/backup/example-app/REPLACE_WITH_RUN_DIRECTORY/database.dump
将 REPLACE_WITH_RUN_DIRECTORY 替换为本轮实际目录。只恢复来自可信源的归档;恢复过程会执行其中的 SQL。测试账号需要有建对象权限,扩展及其他依赖也要先准备好。
--exit-on-error 遇到数据库执行错误就停止。这里跳过原对象所有者和授权恢复,是为了先验证结构与数据;恢复后对象归测试账号所有,原来的权限关系并没有被完整演练。正式灾难恢复还需要核对角色、授权、扩展和应用配置。
恢复后抽查关键表、记录和应用的代表性查询,保留恢复日志。单库 pg_dump 不包括集群级角色和表空间,也不包含应用上传文件、完整的物理恢复链或任意时间点恢复能力;别把修好了这条导出命令,当作所有备份层面都已经齐全。
让下一次定时任务也使用同一套工具
从实际调度入口运行一轮,核对:新一轮任务运行的客户端版本是否正确,退出码是否能传到调度器,产物是否对应这次运行,隔离恢复是否通过。手工导出成功,但定时容器仍用旧镜像,问题就还在。
替换客户端或任务配置前保存原配置和镜像标识。需要回退时,只回退备份工具和任务配置的改动,保留已确认可用的历史备份和本轮恢复日志;若回退会重新引入版本不匹配,应让任务明确报错并告警。数据库主版本的回退属于另外的数据迁移工作,不能通过改回镜像标签顺便完成。


