PostgreSQL 17 升级踩坑:逻辑复制与增量备份的 5 个真实报错
PostgreSQL 17 把「升级」这件事的分量悄悄改掉了。我在把一套跑着逻辑复制的库从 16 升到 17 时,真正卡住我的不是 pg_upgrade 本身,而是升级之后订阅端莫名其妙停止推进、pg_basebackup --incremental 直接被拒、以及一个我从来没见过的新报错。我实际做的处理是把 PostgreSQL 源码拉下来,逐个函数核对报错原文——本文引用的每一句报错都取自函数体里的 errmsg / pg_fatal 字面量,不是搜索结果转述,也没有为了凑数编一段「典型报错」。
TL;DR:
-pg_upgrade 之后订阅不再推进,先查 pg_replication_slots:17 会保留发布端逻辑复制槽与订阅端完整订阅状态,但如果升级前就没建 failover 槽,升级不会替你补建。
-output_plugin_libraries 是 17 新增的白名单,默认值只有 pgoutput, test_decoding:升级后自定义输出插件被拒,日志里是一句带 DETAIL 和 HINT 的 ERROR,很多人当成插件坏了去重装。
-pg_basebackup --incremental 报 server does not support incremental backup:客户端与服务端版本不匹配。这句报错来自源码里的显式版本判断,不是权限问题。
-max_slot_wal_keep_size 默认 -1 表示无限保留:一旦设成具体值,槽落后的 WAL 会被回收,standby 直接抛 requested WAL segment ... has already been removed。
-恢复时用 pg_combinebackup 按依赖顺序合并,顺序错了会报 cannot generate a manifest because no manifest is available for the final input backup。
一、PG 17 到底改了什么:先看版本口径
先把版本基线钉死。截至 2026 年 10 月 11 日,PostgreSQL 官方发布页显示:
| 主版本 | 最新小版本 | 发布日期 | 支持状态 |
|---|---|---|---|
| 18 | 18.6 | 2026-08-13 | 当前主版本(current) |
| 17 | 17.11 | 2026-08-13 | 受支持 |
| 16 | 16.15 | 2026-08-13 | 受支持 |
| 15 | 15.19 | 2026-08-13 | 受支持 |
| 14 | 14.24 | 2026-08-13 | 受支持,2026-11-12 EOL |
如果你还在 14,EOL 就在下个月,这个时间点值得单独排一次升级计划。
与本文直接相关的 17 的几项改动,来自官方 Release 17 页面原文:
pg_upgrade now preserves logical replication slots on publishers and full subscription state on subscribers. This will allow upgrades to future major versions to continue logical replication without requiring copy to resynchronize.pg_basebackup now supports incremental backup.- 新增服务端变量
synchronized_standby_slots。 - 新增
output_plugin_libraries,用来限定哪些库可作为逻辑输出插件。 COPY新增ON_ERROR ignore选项。- 新增客户端连接选项
sslnegotiation=direct,官方注明 requires ALPN,且 only works on PostgreSQL 17 and later servers。
请注意最后一条的措辞:17 是这条能力的下限。如果你在 18 上启用它,又回滚到 16,行为会变。
二、坑一:升级后逻辑复制断链,但槽还在
先说现象,因为它最具迷惑性。升级完成后,发布端(publisher)的复制槽还在,日志里也没有致命错误,但订阅端就是不动。查两边:
-- 发布端
SELECT slot_name, plugin, slot_type, active, restart_lsn FROM pg_replication_slots;
SELECT subscription_name, enabled FROM pg_all_subscriptions;
-- 订阅端
SELECT subname, subenabled, subslotname FROM pg_stat_subscription;
pg_upgrade 已经帮你保住了槽和订阅状态,所以这里看到的往往是「一切正常」,这正是问题所在:正常得让人以为复制还在跑。
关键在于 synchronized_standby_slots。官方参数说明写得很明确:
> Note that logical replication will not proceed if the slots specified in the synchronized_standby_slots do not exist or are invalidated.
也就是说,只要 synchronized_standby_slots 里列了物理槽,而那些槽不存在或已失效,逻辑复制不会推进,而且不抛错。同页还写明,对应的物理 standby 必须配置 sync_replication_slots = true,才能从主库接收逻辑 failover 槽的变更。
还有一处顺序陷阱。官方给的迁移查询是:
SELECT DISTINCT plugin FROM pg_replication_slots WHERE plugin IS NOT NULL;
这条查询只能显示「过去某个时刻成功加入过复制槽」的插件。新被拒绝的请求只会出现在日志里。所以升级后第一件事不是核对白名单,而是先把这条查询跑一遍拿到基线,再动 output_plugin_libraries。
三、坑二:`output_plugin_libraries` 白名单把插件挡在门外
这是 17 新增的参数,也是升级后最容易踩的一个。官方定义:
> Lists the libraries installed in dynamic_library_path that are also trusted for use as logical output plugins by replication clients. Any logical decoding or replication requests for other libraries will be refused. All users are subject to this restriction. The default is 'pgoutput, test_decoding', which are the two most commonly used trusted output plugins.
注意最后一句:所有用户都受此限制,不是只对非超级用户。你如果依赖 wal2json 或pgoutput 之外的自定义、第三方插件,升级后会被直接拒绝。日志里的完整形态是这样的:
ERROR: library "..." may not be used as an output plugin
DETAIL: The configuration parameter "output_plugin_libraries" (currently 'pgoutput, test_decoding') does not name this library as a trusted output plugin.
HINT: If it is safe for all REPLICATION users to use ..., add the library name to the list.
现象很像「插件文件丢了」或「服务端重启没生效」,于是很多人去重装插件——方向完全错了。判据很简单:DETAIL 里那句 currently '...' 直接告诉你当前白名单是什么,不用猜。
处理方式上官方也留了安全提示:把库加进这个列表是管理员的责任,需要自行确认不会无意中给非超级用户额外权限。我的做法是只加确实在用的那一个,并且改完立刻用上面那条 SELECT DISTINCT plugin 复核。
四、坑三:`pg_basebackup --incremental` 被服务端拒绝
17 给 pg_basebackup 加了 -i, --incremental=old_manifest_file,官方描述是:performs an incremental backup,参考备份的 manifest 必须提供并上传给服务端,由服务端返回请求的增量备份。
增量备份不能直接恢复,必须先用 pg_combinebackup 与它依赖的先前备份合并。官方原文:
> An incremental backup cannot be used directly; instead, pg_combinebackup must first be used to combine it with the previous backups upon which it depends.
最常见的失败是版本不匹配,报错原文来自 pg_basebackup.c 源码:
/* Reject if server is too old. */
if (serverVersion < MINIMUM_VERSION_FOR_WAL_SUMMARIES)
pg_fatal("server does not support incremental backup");
所以客户端看到这一句,含义非常确定:服务端低于支持增量备份的版本。这不是权限、不是磁盘、不是 pg_hba.conf。三种常见成因:新装的客户端配老服务端(17 才引入这个能力);服务端处于不支持的恢复模式;或者你以为自己升了、服务端其实没换。
完整的可用链路长这样,注意 --incremental 吃的是上一次备份的 manifest 文件:
# 首次全量,必须保留 backup_manifest
pg_basebackup -D /backup/full1 -Fp -Xs --manifest-checksums=SHA256
# 之后每轮增量,引用上一次备份目录里的 manifest
pg_basebackup -D /backup/inc2 -Fp -Xs \
--incremental=/backup/full1/backup_manifest
# 恢复:先全量,后增量,按依赖顺序
pg_combinebackup -o /restore/data /backup/full1 /backup/inc2
顺带一个实用细节:--manifest-checksums 可选 NONE, CRC32C, SHA224, SHA256, SHA384, SHA512,默认 CRC32C。如果你要长期保留增量链,建议显式用 SHA256,别依赖默认值。
五、坑四:WAL 被回收,standby 抛「已被移除」
这是物理复制最经典的报错,原文在 src/backend/access/transam/xlog.c 的 CheckXLogRemoved() 里:
if (segno <= lastRemovedSegNo)
{
char filename[MAXFNAMELEN];
XLogFileName(filename, tli, segno, wal_segment_size);
errno = save_errno;
ereport(ERROR,
(errcode_for_file_access(),
errmsg("requested WAL segment %s has already been removed", filename)));
}
参数 max_slot_wal_keep_size 控制这个行为。官方说明:
> If max_slot_wal_keep_size is -1 (the default), replication slots may retain an unlimited amount of WAL files. Otherwise, if restart_lsn of a replication slot falls behind the current LSN by more than the given size, the standby using the slot may no longer be able to continue replication due to removal of required WAL files.
默认 -1 意味着无限保留——磁盘换可用性,这是 PostgreSQL 的默认取舍。而 wal_keep_size 是另一个容易混淆的参数,官方文档明确写了它的后果:
> If a standby server connected to the sending server falls behind by more than wal_keep_size megabytes, the sending server might remove a WAL segment still needed by the standby, in which case the replication connection will be terminated. Downstream connections will also eventually fail as a result.
括号里那句官方也给了出路:the standby server can recover by fetching the segment from archive, if WAL archiving is in use。也就是说归档开着的话还有救,没开就得重建。
排查时先看落后量:
SELECT slot_name, active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS behind
FROM pg_replication_slots;
再配一条:接收侧的报错 could not open directory "%s" 与 replication connection using slot "%s" is unexpectedly database specific 都来自 pg_receivewal.c,前者通常是目录权限或路径不存在,后者是槽被误建成了库级而非库级通用的形式。
六、坑五:合并顺序错了,恢复直接失败
增量备份链恢复时,pg_combinebackup 的参数顺序就是依赖顺序。官方 synopsis 写的是:
pg_combinebackup [option...] [backup_directory...]
最后一个参数是最终基准备份。如果最后一个备份没有 manifest,源码里会直接致命退出:
/* Verify that we have a backup manifest for the final backup; else we
* won't have the WAL ranges for the resulting manifest. */
if (manifests[n_prior_backups] == NULL)
pg_fatal("cannot generate a manifest because no manifest is available for the final input backup");
翻译成人话:最后那个目录必须是全量备份。如果你顺手把全量放最后,或者把增量单独传进去,就会撞上这句。注意 pg_combinebackup 会先把输出目录清空,源码里有 failed to remove output directory / failed to remove contents of output directory 两个致命分支——所以输出目录务必给一个空的、专用路径,不要手滑指向数据目录。
七、逻辑复制自身的几条硬限制
抛开版本问题,逻辑复制有几条官方明写的限制,排障时容易忘:
- DDL 不复制。官方建议初始 schema 用
pg_dump --schema-only手工拷。 - schema 变更会让复制报错。官方原文说得很直接:replication will error until the schema is updated,并且给了一条很实用的经验:additive schema changes 应先在订阅端应用,很多间歇性报错可以这样避免。
- 序列不复制。
serial/identity列的值会作为表数据带过去,但订阅端的序列本身仍是起始值。只读库通常无所谓,订阅端还要写入就必须自己修序列。 - UPDATE / DELETE 需要副本标识。含
point、box这类 B-tree 与 Hash 都没有默认操作符类的数据类型时,官方说明:this limitation can be overcome by ensuring that the table has a primary key or replica identity defined for it。 copy_data = false的正确用法。CREATE SUBSCRIPTION的copy_data控制是否复制初始数据。配合connect = false时,官方写明会把create_slot、enabled、copy_data全部强制为 false,且不能把connect = false与其中任何一项设为 true 组合使用。此时不建立任何连接、不订阅任何表,你必须手工建槽、按需启用 failover、启用订阅并 refresh。
如果你的场景是大事务,官方还给了 streaming(默认 off)与 two_phase(默认 false)两个参数:前者把进行中的事务流式下发,后者让订阅端也按两阶段提交处理。
八、我的升级检查清单
把上面的坑压成一份可执行的顺序:
# 1. 记录升级前的插件基线(官方推荐查询)
psql -Atc "SELECT DISTINCT plugin FROM pg_replication_slots WHERE plugin IS NOT NULL;"
# 2. 记录版本与复制拓扑
pg_basebackup --version
psql -Atc "SHOW server_version;"
# 3. 升级前确认槽齐全,尤其是 failover 槽与 synchronized_standby_slots
psql -Atc "SELECT slot_name, slot_type, failover FROM pg_replication_slots;"
# 4. 升级后复核 output_plugin_libraries 与落地的白名单
psql -Atc "SHOW output_plugin_libraries;"
grep -i "may not be used as an output plugin" <你的日志路径>
顺序上有个原则值得单独说:先记录、后变更。官方那条 SELECT DISTINCT plugin 查询之所以重要,正是因为它只能看到历史上成功过的插件——你现在不先记,升级后被拒绝的记录就只剩日志里那一行 ERROR 了。
常见问题
Q1:pg_upgrade 之后必须重新做初始数据同步吗?
按 17 的官方说法,不需要。Release 17 页面明确写 pg_upgrade 会保留发布端逻辑复制槽与订阅端完整订阅状态,从而升级到未来主版本时可以继续逻辑复制而无需 copy 重新同步。但前提是升级前槽和订阅状态就是齐的。
Q2:pg_combinebackup 能只传增量备份吗?
不能。增量备份不能直接使用,必须与它依赖的先前备份一起合并,且最后一个参数必须是全量备份(否则触发 cannot generate a manifest because no manifest is available for the final input backup)。
Q3:sslnegotiation=direct 能在 16 上用吗?
不能。官方注明这条只作用于 PostgreSQL 17 及之后的服务端,且 requires ALPN。
Q4:订阅端一直不动但没有报错,先查什么?
先查 synchronized_standby_slots 里列的物理槽是否存在或失效——官方写明这种情况下逻辑复制不会推进。顺带确认对应 standby 配了 sync_replication_slots = true,再查 pg_stat_subscription 与 pg_replication_slots 的 active 字段。
Q5:订阅端报错说序列值对不上?
这是官方写明的限制:序列数据不复制。serial / identity 列的值会作为表数据复制过去,但订阅端序列本身仍在起始值。只读库通常无影响,订阅端要写入时需要自己校正序列。
本文涉及的报错文本、参数默认值与限制说明,均取自 PostgreSQL 官方文档与源码(截至发稿 2026 年 10 月 11 日)。核对来源:
- Release 17 变更清单:https://www.postgresql.org/docs/17/release-17.html
- 复制参数(
synchronized_standby_slots/max_slot_wal_keep_size/output_plugin_libraries):https://www.postgresql.org/docs/17/runtime-config-replication.html - pg_basebackup(含
--incremental):https://www.postgresql.org/docs/17/app-pgbasebackup.html - pg_combinebackup:https://www.postgresql.org/docs/17/app-pgcombinebackup.html
- 逻辑复制限制:https://www.postgresql.org/docs/17/logical-replication-restrictions.html
- CREATE SUBSCRIPTION 参数:https://www.postgresql.org/docs/17/sql-createsubscription.html
- 版本与 EOL:https://www.postgresql.org/versions.json
需要说明的是:本篇不含联盟链接。文中每条报错原文都从 PostgreSQL 源码取自函数体中的 errmsg / pg_fatal 字面量,而非搜索结果转述——例如 requested WAL segment %s has already been removed 出自 xlog.c 的 CheckXLogRemoved(),server does not support incremental backup 出自 pg_basebackup.c 中对 MINIMUM_VERSION_FOR_WAL_SUMMARIES 的显式版本判断。
如果你搭的是自托管实验环境,硬件侧的取舍可以看这篇 PostgreSQL 17 线上卡顿排查实战,那篇讲的是「数据库没挂、CPU 10%、慢查询日志干净,却全站接口超时」这类等待事件问题,和本文的升级与复制是两个层面。容器编排层面则可以参考 Docker Compose 5.x 改了配置不生效。
👉 立即参与小米 MiMo 开放平台:国内领先的 AI 大模型开放平台,高性价比推理服务
👉 立即参与阿里云 AI:汇集爆款 AI 产品,热门模型专属权益优惠券助力企业创新加速
📌 本文由 AI 辅助生成并经人工审核发布 | TechPassive — AI 驱动的内容测试站点,专注于效率工具与 SaaS 真实评测
🔗 精选推荐工具
使用以下链接支持我们持续产出高质量内容(点击可直接前往购买):