PostgreSQL 逻辑复制与零停机跨版本迁移
数据库大版本升级最难的部分从来不是新特性,而是"怎么在服务不停的情况下把数据搬走并回滚得回来"。逻辑复制是 PG 领域最成熟的答案:它基于 wal2json/pgoutput 订阅变更流,不复制表结构、不锁表、可以只迁一部分表,还天然支持双向与降级回滚路径。
一、发布与订阅的最小骨架
-- 源库
CREATE PUBLICATION okgoes_pub FOR ALL TABLES;
-- 或精细控制
CREATE PUBLICATION okgoes_pub FOR TABLE articles, comments, users
WITH (publish = 'insert,update,delete');
-- 目标库(预先建好表结构)
CREATE SUBSCRIPTION okgoes_sub
CONNECTION 'host=src port=5432 dbname=okgoes user=repl password=xxx'
PUBLICATION okgoes_pub
WITH (create_slot = true, copy_data = true, enabled = true);
必须满足的前提:源库 wal_level = logical,预留足够的 max_replication_slots 与 max_logical_replication_workers;表要有主键或 REPLICA IDENTITY,否则 UPDATE/DELETE 无法正确复制。
ALTER TABLE login_logs REPLICA IDENTITY FULL; -- 无主键表的兜底,代价是 WAL 膨胀
REPLICA IDENTITY FULL 会把整行旧值写进 WAL,大表上会显著放大写入。没有主键的表要优先补主键,把 FULL 当作临时手段。
二、初始快照的三个误区
copy_data = true 会做初始数据同步,实现方式是订阅端在一个可延期事务快照里复制,然后从该快照位点开始应用增量。三个常见误解:
- "初始同步会锁表":不会。它用快照隔离读,不阻塞写;但长时间持有复制槽会让源库 WAL 无法回收——这比锁表危险得多;
- "复制完一定一致":初始同步期间的写入通过增量补齐,但如果订阅中途被
disable再启用,或复制槽失效,就可能永久漏数据; - "序列会自动同步":不会。序列值不属于表数据,必须单独迁移,且切换前要重新对齐,否则新库插入会撞主键。
-- 切换前对齐序列(目标库执行)
SELECT setval(pg_get_serial_sequence('articles','id'),
(SELECT MAX(id)+1 FROM articles));
WAL 堆积是这个方案最危险的运维风险。复制槽订阅延迟或网络中断时,源库磁盘会持续增长直至打满。必须监控:
SELECT slot_name, active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots;
保留量超过阈值(如 10GB)且未 active,就要立刻处理,必要时接受丢弃数据、重建订阅。
三、跨版本迁移的实际流程
以 12 升 16 为例,逻辑复制的价值是目标全新版本 + 源库继续服役 + 随时可回退。
- 新版本集群建好,参数、扩展、locale 对齐;
pg_dump --schema-only迁结构并逐项 review(不要直接跑老 SQL); - 建立发布订阅,先迁小表验证链路;
- 大表分批
ADD到 publication,避免一次性拉起巨大初始同步; - 观察延迟收敛到秒级,做数据校验;
- 停写窗口(几十秒即可,因为数据已经同步过)→ 切流量 → 观察;
- 回滚路径保留:立刻建立反向逻辑复制(新库 → 老库),让老库能接收切换后产生的新写入,这样即使回退也不丢数据。保留 1 天到 1 周。
反向复制是这个方案最被低估的价值点。没有它,"回滚"就等于"接受切换期间产生的数据丢失"。
四、数据校验怎么做
行数一致不代表内容一致。两层校验:
-- 结构化的聚合指纹(源/目标分别执行比对)
SELECT count(*),
coalesce(sum(hashtext(id::text || coalesce(updated_at::text,''))),0) AS fp
FROM articles WHERE updated_at < '2026-10-01';
按时间或 id 分段跑,避免长事务。差异行再单独查。校验要选在业务低峰并停止校验表之外的写入干扰,否则更新延迟会造成"看起来不一致"的误报。
更严格的做法是用同一时间点的 LSN 做对照:源库取 pg_current_wal_lsn(),等订阅端 replay 到该位点后再比对:
SELECT latest_end_lsn, latest_end_time FROM pg_stat_subscription;
五、DDL 不在覆盖范围内
原生逻辑复制只复制 DML,不复制 DDL。这是绝大多数事故的来源:迁移期间在源库加了一列,目标库没有,订阅报错并停止。
三条纪律:
- 迁移窗口冻结 DDL,写进变更流程;
- 确需变更时,先在目标库执行同样的 DDL,再在源库执行(顺序不能反);
- 报错后不要盲目
ALTER SUBSCRIPTION ... RESUME,先查pg_stat_subscription与日志确定冲突原因。
ALTER SUBSCRIPTION okgoes_sub DISABLE;
-- 修复目标表结构
ALTER SUBSCRIPTION okgoes_sub ENABLE;
六、切换与流量层配合
切换的本质是改连接串。三条注意事项:
- 连接池要提前预热目标库连接,避免首波请求集中建连与认证;
- 写入口只能有一个。任何双写窗口都是数据不一致的来源,靠"停写 + 切流 + 恢复写"三步而不是双写;
- 切换后的第一分钟要有明确观察项:错误率、主键冲突、慢查询计划变化(新版本优化器行为可能不同)。
新版本优化器变化是真实风险。PG 大版本升级后的统计信息与 cost 模型变化会导致执行计划翻转,切换前应在目标库跑一遍生产慢查询集合并 EXPLAIN 对比。
小结
逻辑复制做零停机升级的正确姿势是:先确认主键与 wal_level,控制复制槽的 WAL 保留量,序列单独对齐,DDL 冻结且目标先行,校验用分段指纹加 LSN 对照,切换后立即建反向复制保住回滚能力。它的优势是"源库不停、随时可退",风险集中在两处——复制槽撑爆磁盘,和误以为 DDL 会被复制。守住这两条,跨大版本升级就从高风险操作降级为一次普通的变更窗口。
评论(0)
登录 后参与评论。