🔍 登录 注册

PostgreSQL 逻辑复制与零停机跨版本迁移

打 打工人 2026-10-23 👁 0 ⏱ 4min

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 会做初始数据同步,实现方式是订阅端在一个可延期事务快照里复制,然后从该快照位点开始应用增量。三个常见误解:

  1. "初始同步会锁表":不会。它用快照隔离读,不阻塞写;但长时间持有复制槽会让源库 WAL 无法回收——这比锁表危险得多;
  2. "复制完一定一致":初始同步期间的写入通过增量补齐,但如果订阅中途被 disable 再启用,或复制槽失效,就可能永久漏数据;
  3. "序列会自动同步":不会。序列值不属于表数据,必须单独迁移,且切换前要重新对齐,否则新库插入会撞主键。
-- 切换前对齐序列(目标库执行)
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 为例,逻辑复制的价值是目标全新版本 + 源库继续服役 + 随时可回退。

  1. 新版本集群建好,参数、扩展、locale 对齐;pg_dump --schema-only 迁结构并逐项 review(不要直接跑老 SQL);
  2. 建立发布订阅,先迁小表验证链路;
  3. 大表分批 ADD 到 publication,避免一次性拉起巨大初始同步;
  4. 观察延迟收敛到秒级,做数据校验;
  5. 停写窗口(几十秒即可,因为数据已经同步过)→ 切流量 → 观察;
  6. 回滚路径保留:立刻建立反向逻辑复制(新库 → 老库),让老库能接收切换后产生的新写入,这样即使回退也不丢数据。保留 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。这是绝大多数事故的来源:迁移期间在源库加了一列,目标库没有,订阅报错并停止。

三条纪律:

  1. 迁移窗口冻结 DDL,写进变更流程;
  2. 确需变更时,先在目标库执行同样的 DDL,再在源库执行(顺序不能反);
  3. 报错后不要盲目 ALTER SUBSCRIPTION ... RESUME,先查 pg_stat_subscription 与日志确定冲突原因。
ALTER SUBSCRIPTION okgoes_sub DISABLE;
-- 修复目标表结构
ALTER SUBSCRIPTION okgoes_sub ENABLE;

六、切换与流量层配合

切换的本质是改连接串。三条注意事项:

  1. 连接池要提前预热目标库连接,避免首波请求集中建连与认证;
  2. 写入口只能有一个。任何双写窗口都是数据不一致的来源,靠"停写 + 切流 + 恢复写"三步而不是双写;
  3. 切换后的第一分钟要有明确观察项:错误率、主键冲突、慢查询计划变化(新版本优化器行为可能不同)。

新版本优化器变化是真实风险。PG 大版本升级后的统计信息与 cost 模型变化会导致执行计划翻转,切换前应在目标库跑一遍生产慢查询集合并 EXPLAIN 对比。

小结

逻辑复制做零停机升级的正确姿势是:先确认主键与 wal_level,控制复制槽的 WAL 保留量,序列单独对齐,DDL 冻结且目标先行,校验用分段指纹加 LSN 对照,切换后立即建反向复制保住回滚能力。它的优势是"源库不停、随时可退",风险集中在两处——复制槽撑爆磁盘,和误以为 DDL 会被复制。守住这两条,跨大版本升级就从高风险操作降级为一次普通的变更窗口。

读完了?给作者一点支持

本文采用 CC BY 4.0 协议转载 · © 打工人

评论(0)

登录 后参与评论。

  • 还没有评论,来抢沙发。

🔗 相关文章