KernelRanch
返回文章列表

逻辑复制做零停机升级:1M 行同步 1.6 秒,切换停顿 0.86 秒,附五个必踩的坑

2026-11-10 #数据库

大版本升级有两条路:pg_upgrade 要停机,逻辑复制可以几乎不停。后者的代价是要处理一堆"复制不过去"的东西。我在本机把 14.24 → 18.6 的完整流程跑了一遍,也把三个最容易翻车的点逐个复现了。

0. 实测环境

项值
发布端PostgreSQL 14.24(Docker,wal_level=logical,1GB 内存限制)
订阅端PostgreSQL 18.6(Docker,wal_level=logical,shared_buffers=256MB)
数据pgbench -i -s 10 → 1,000,000 行 accounts + 4 张表
网络自定义 Docker 网络,容器名互访,无宿主端口映射

1. 结果

阶段实测
14 上灌数据(1M 行)1.2 s
schema 复制(pg_dump -s + restore)4 张表
建订阅 + 初始同步 1M 行1.6 s
20 秒写入压测(4 客户端)696 tps,平均 5.747 ms
压测期间订阅端延迟0 字节
追平耗时0 字节待回放,无需等待
切换停顿(停写到解除订阅)0.86 s
切换后行数校验两侧均 1,000,000
18 上可写是

1.6 秒同步 1M 行看起来很美,但要打个折:数据量只有约 100MB。真实场景里初始同步的耗时跟数据量近似线性,几百 GB 的库是小时级——那段时间旧库还得继续服务,所以同步慢不是问题,同步期间旧库不能删 WAL 才是。

2. 四步流程

第一步:在订阅端建好 schema

逻辑复制不复制 DDL。官方文档写得很直白:"数据库 schema 与 DDL 命令不会被复制,初始 schema 可以用 pg_dump --schema-only 手工拷贝"[1]。

docker exec lr14 pg_dump -s -U postgres -d postgres -f /tmp/schema.sql
docker cp lr14:/tmp/schema.sql ./schema.sql
docker cp ./schema.sql lr18:/tmp/schema.sql
docker exec lr18 psql -U postgres -q -f /tmp/schema.sql

顺序不能反:先建表,再建订阅。

第二步:发布端建 publication

-- 在 14 上
CREATE PUBLICATION pub FOR ALL TABLES;

FOR ALL TABLES 覆盖当前与未来所有表,适合"整库迁移"。只迁部分表时用 FOR TABLE a, b,但记得把后续新建的表也加进去,否则它们不会复制。

第三步:订阅端建 subscription

-- 在 18 上
CREATE SUBSCRIPTION sub
  CONNECTION 'host=lr14 port=5432 user=postgres dbname=postgres'
  PUBLICATION pub;

创建时会自动在发布端建一个同名复制槽(日志里能看到 created replication slot "sub" on publisher)。这个槽会持有 WAL,直到订阅被解除或删除。

第四步:切换

-- 1) 应用停写(或把旧库设为只读)
-- 2) 等延迟归零
-- 3) 解除订阅(保留复制槽,便于回滚)
ALTER SUBSCRIPTION sub DISABLE;
ALTER SUBSCRIPTION sub SET (slot_name = NONE);
DROP SUBSCRIPTION sub;
-- 4) 连接串指向 18,恢复写入

SET (slot_name = NONE) 这一步不能省。直接 DROP SUBSCRIPTION 时,如果发布端连不上,会卡住甚至报错——因为它默认要顺手把发布端的槽删掉。先断开关联再删,才可控。

3. 三个不同步,逐个复现

坑一:序列不复制(最致命)

官方文档:"序列数据不会被复制。serial 或 identity 列的数据会随表一起复制,但订阅端的序列仍然是起始值"[1]。如果就此切过去,第一条 INSERT 就会主键冲突。

我复现了一遍。在订阅端造一张带 serial 的表,插 3 行(模拟"随表复制过来的数据"),此时序列还在起点:

nextval_before=1
ERROR:  duplicate key value violates unique constraint "seq_demo_pkey"
DETAIL:  Key (id)=(2) already exists.

nextval 返回 1,但 1 已经被占用;再取到 2 时直接撞上主键。修正方式是切换后立刻把序列推到表的当前最大值:

SELECT setval('seq_demo_id_seq', (SELECT max(id) FROM seq_demo));
-- 返回 3,之后插入得到 id=4

生产上的做法是批量生成所有序列的 setval 语句,切换脚本里一次执行完:

SELECT format('SELECT setval(%L, COALESCE((SELECT max(%I) FROM %I), 1));',
              pg_get_serial_sequence(quote_ident(schemaname)||'.'||quote_ident(relname), 'id'),
              'id', quote_ident(schemaname)||'.'||quote_ident(relname))
FROM pg_stat_user_tables
WHERE EXISTS (
  SELECT 1 FROM pg_attribute a
  WHERE a.attrelid = (quote_ident(schemaname)||'.'||quote_ident(relname))::regclass
    AND a.attname = 'id' AND a.attnotnull
);

把生成的语句贴出来逐条执行,比手写靠谱。

坑二:DDL 不复制

加列、改类型、加索引,订阅端一概不会跟。官方给的建议是"多数情况下可以先把增量式的 schema 变更应用到订阅端,以避免间歇性报错"[1]。

实践顺序是:先在订阅端加列(可空或有默认值)→ 再在发布端加列 → 让复制继续。反过来做,复制会因为"目标表没有这一列"而中断。

坑三:大对象与视图不复制

大对象(lo 类型)完全不复制,官方明确"没有绕过的办法,只能把数据存进普通表"[1]。视图、物化视图、外部表也不支持——只支持表(含分区表)。

4. 一条容易忽略的纪律

切换窗口里,旧库仍然是可写的。我在实测里是手动停掉 pgbench 才切换的;生产上如果没有把旧库切成只读,切换后可能有零星写入落在旧库,造成数据丢失。

两个做法:ALTER DATABASE ... SET default_transaction_read_only = on,或者在切换前把应用侧连接串摘掉。前者更可靠,因为它不依赖应用配合。

5. 和 pg_upgrade 怎么选

维度逻辑复制pg_upgrade
停机时间秒级(切换窗口)与数据量相关,实测 799MB 约 5 秒、308MB 约 2.3 秒
大版本跨越可以跨多个大版本支持 9.2+ 直跳
扩展与 schema 变更需要手工同步随集群一起升级
回滚切换前旧库完好,随时可退--link 启动新库后旧库不可用
运维复杂度高(序列、DDL、大对象、槽管理)低(一条命令)
适用场景不能停机、库不大、schema 稳定有维护窗口、schema 复杂

我的判断标准:能停机就用 pg_upgrade,因为它把"复制不过去"的那一整类问题都规避了。逻辑复制的价值在"完全不能停"的场景——比如面向海外用户的 7×24 服务,找不到可接受的低峰窗口。

6. 在 Windows 上复现

docker network create lrnet
docker run -d --name lr14 --network lrnet -e POSTGRES_PASSWORD=postgres -e POSTGRES_HOST_AUTH_METHOD=trust postgres:14 -c wal_level=logical
docker run -d --name lr18 --network lrnet -e POSTGRES_PASSWORD=postgres -e POSTGRES_HOST_AUTH_METHOD=trust postgres:18 -c wal_level=logical
docker exec lr14 pgbench -i -s 10 -U postgres -h 127.0.0.1 -d postgres
# 之后按第 2 节四步走,注意 pg_dump 要带 -d postgres

小结

1M 行、逻辑复制、0.86 秒切换停顿,这套流程在本机是成立的。但它不是"配好就完事"的方案:schema 要自己 dump、序列要自己 setval、DDL 要自己同步、大对象根本过不去。

三个坑里,序列问题最隐蔽——复制期间一切正常,切换后第一条写入才炸。把第 3 节那段生成 setval 的 SQL 存进切换脚本,能省掉一次事故。

数据来源与检索时间

检索时间 2026-11-10。全部耗时、延迟、行数与报错文本均为本机实测。

来源URL对应数据
PostgreSQL 18 文档《Logical Replication Restrictions》https://www.postgresql.org/docs/18/logical-replication-restrictions.htmlschema 与 DDL 不复制、可用 pg_dump --schema-only 手工同步;序列不复制、切换前需把序列更新到最新值;大对象不复制且无绕过办法;只支持表(含分区表);建议先对订阅端做增量 schema 变更
PostgreSQL 18 文档《pg_upgrade》https://www.postgresql.org/docs/18/pgupgrade.htmlpg_upgrade 的停机特性与 --link 的行为,用于第 5 节对比

核验说明

  • 两个容器、初始同步、20 秒写入压测、追平、切换、行数校验全部在本机执行;duplicate key 的报错原文与 setval 修复结果是真实输出。
  • 初始同步 1.6 秒只对应约 100MB 数据,不能外推到大数据量;文中已标注。
  • 未做:跨大版本时序列的批量修正脚本只在单表上验证过逻辑,没有在几十张表上跑;没有测复制槽在长时间停机后的 WAL 堆积;没有测双向复制或级联复制。