PostgreSQL 迁移切换:逐库推进还是整体切换?先想清楚再动手

2026-05-21 25 预计阅读时间:1 分钟
来源:postgr.es AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:14 分钟

新集群已经部署好了,数据同步也跑起来了,接下来就是切换(cutover)——把流量从旧集群切到新集群。很多人把切换当成迁移的最后一步,到了那个节点才去想怎么做。但实际上,切换策略决定了你怎么迁移,而不是反过来。它影响你怎么配复制、预留多少 replication slot、怎么处理 schema 变更、以及回滚路径长什么样。所以在搭复制之前,先把切换策略定下来。

本文对比两种最常见的切换策略:逐库切换和整体切换,看看各自付出什么代价、换来什么收益。

逐库切换:一个库稳了再动下一个

想象搬办公室——先搬财务部,下周再搬工程部。每个部门安顿好了再搬下一个,出了问题也只影响一个部门。逐库切换就是这个思路:挑一个库,迁移、测试、切换、确认没问题,再动下一个。

为什么这样更稳

管理负担轻。 盯一个库的切换,出了问题你知道往哪看。团队不用同时操心十个库。

第一个库就是消防演习。 你会发现 runbook 漏了什么、监控没覆盖什么、回滚步骤实际跑起来什么样——但影响范围有限。到第五个库的时候,团队已经驾轻就熟。

Replication slot 不堆积。 逻辑复制期间,发布端要为每个活跃的订阅端保持一个 replication slot,slot 持有 WAL 数据防止订阅端遗漏。同时同步很多库,slot 就堆起来了——磁盘占用上升、内存压力增大、风险升高。逐库切换的好处是:一个库切完,slot 立刻释放,发布端喘口气。

回滚简单。 一个库出了问题,只需要回退这一个库。反向复制路径短而干净,不用同时撤销十几个切换。

CPU 保持平稳。 同时管理很多库的复制会给发布端带来可观负载:WAL sender、复制 worker、跨多个 slot 的变更追踪,可能在最需要系统稳定的时刻把 CPU 打满。逐库推进让负载可预测。

停机时间可以只有几秒。 单库切换流程很短:暂停写入 → 确认订阅端 lag 为零 → 更新连接字符串 → 恢复写入。

人为错误空间小。 切换是高压操作,同时做的事越少出错越少。一个库意味着一份检查清单、一次确认、一个回滚方案,而不是十份并行。

迁移周期可控。 每个库是独立的,不用等所有库都准备好才能开始切换。即使某个库很大,迁移窗口也只限于这一个库。

整体切换:所有库一起上卡车

回到搬办公室的比喻——所有部门同时装车、一起出发、希望新大楼对每个部门都准备好了。纸面上看起来更快,实际操作难度大得多。

你会面对的问题

参数值随库数量膨胀。 同时复制几百个库,max_logical_replication_workersmax_worker_processes 要设到几百再加预留。每个 worker 进程消耗内存和 CPU,值越高,PostgreSQL 启动的后台进程越多,在最需要稳定的时刻给系统施加更大压力。

停机时间随集群规模增长。 逻辑复制不同步 sequence(序列),切换前要把每个库的每个 sequence 在新集群上快进到安全值。一个库 30 秒的事,整个集群可能要 20 分钟。

一个问题可以卡住全部。 一个库遇到 schema 不匹配、卡住的事务、或复制失同步,整个切换都受影响。没法轻松跳过有问题的库继续其他的。

中间的 schema 变更很痛苦。 迁移要时间,逻辑复制跑着的时候开发还在改 schema。发布端建好复制之后的任何 schema 变更必须手动应用到订阅端,否则那个库的复制会断。跨很多库、长窗口追踪这些变更,遗漏几乎不可避免,补漏通常是在压力下手工修复。

新库可能悄无声息地出现。 其他团队在迁移期间可能建了新库,如果没人通知数据库团队,这些库在订阅端就不存在,被跳过。直到该在新集群上的东西还在旧集群上悄悄跑着,才有人发现。

回滚慢且痛苦。 切完很多库之后要回滚,得给所有库建反向逻辑复制。迁移窗口越长,新集群上变化的数据越多,回滚越难。

迁移窗口可能拖成几周。 所有库都得准备好才能切换,schema 变更还在不断进来,准备阶段越来越长。本来几天的事变成几周的同步工作,业务还在持续变化。

一次重启可能让你从零开始。 如果发布端跑在 Patroni 上,一次 PostgreSQL 重启(比如 OOM kill 触发的),Patroni 会丢弃它不认识的 replication slot——你为迁移创建的逻辑复制 slot 正属于这类。整体迁移意味着所有正在复制的库都丢 slot,从头开始初始数据同步,不是从断点续传。几百个库的情况下,这不是 setback,是重新开始整个迁移。

实操:逐库切换的关键步骤与命令

下面给一个逐库切换的实操流程,包含可以直接运行的命令。

1. 建立逻辑复制(发布端)

-- 在旧集群(发布端)上,为单个库创建 publication
-- 先连到目标库
\c myapp_production

-- 发布所有表(也可以指定表名列表)
CREATE PUBLICATION myapp_prod_pub FOR ALL TABLES;

2. 建立逻辑复制(订阅端)

-- 在新集群(订阅端)上,连到对应库
\c myapp_production

-- 创建订阅,连接信息指向旧集群
CREATE SUBSCRIPTION myapp_prod_sub
  CONNECTION 'host=old-cluster.internal port=5432 dbname=myapp_production user=replicator password=SECRET'
  PUBLICATION myapp_prod_pub;

订阅创建后,PostgreSQL 自动开始初始数据同步,然后进入持续复制模式。

3. 切换前检查复制延迟

# 在订阅端查看复制延迟,确认 lag 为 0 再继续
psql -h new-cluster.internal -U postgres -d myapp_production \
  -c "SELECT subname, pid, received_lsn, latest_end_lsn,
       latest_end_time, now() - latest_end_time AS lag
     FROM pg_stat_subscription
     WHERE subname = 'myapp_prod_sub';"

lag 应为 0 或接近 0。如果还有延迟,等它追上。

4. 快进 sequence 到安全值

逻辑复制不同步 sequence,切换前必须手动处理。下面是一个脚本,把旧集群上所有 sequence 的当前值查出来,在新集群上设到更大的安全值:

#!/usr/bin/env bash
# fast_forward_sequences.sh
# 用法: ./fast_forward_sequences.sh <旧集群连接串> <新集群连接串> <库名> <增量偏移>
# 示例: ./fast_forward_sequences.sh \
#   "host=old-cluster.internal port=5432 user=postgres dbname=myapp_production" \
#   "host=new-cluster.internal port=5432 user=postgres dbname=myapp_production" \
#   myapp_production 1000

OLD_CONN="$1"
NEW_CONN="$2"
DBNAME="$3"
OFFSET="${4:-1000}"  # 默认偏移 1000,防止切换期间旧集群还在用旧值

# 从旧集群取所有 sequence 的当前值
SEQ_VALUES=$(psql "$OLD_CONN" -t -A -c \
  "SELECT sequencename, last_value FROM pg_sequences WHERE schemaname = 'public';")

# 在新集群上快进每个 sequence
while IFS='|' read -r seq_name last_val; do
  new_val=$((last_val + OFFSET))
  echo "Setting $seq_name to $new_val on new cluster..."
  psql "$NEW_CONN" -c \
    "SELECT setval(pg_get_serial_sequence('public.${seq_name}', 'id'), $new_val, false);"
done <<< "$SEQ_VALUES"

echo "Sequence fast-forward complete for $DBNAME."

注意:上面的 pg_get_serial_sequence 假设 sequence 与表的 id 列关联。如果你的 sequence 是独立的或命名不同,需要调整查询逻辑,直接用 SELECT setval('public.${seq_name}', $new_val, false);

5. 暂停写入、切换连接字符串

# 暂停应用写入(具体方式取决于你的应用架构)
# 例如,通过 PgBouncer 切换后端:

# 1. 在应用层暂停写入(或设 maintenance 模式)
# 2. 确认 lag 为 0(步骤 3)
# 3. 更新 PgBouncer 配置指向新集群

# PgBouncer 配置片段示例 (databases.ini):
# 切换前:
myapp_production = host=old-cluster.internal port=5432 dbname=myapp_production
# 切换后:
myapp_production = host=new-cluster.internal port=5432 dbname=myapp_production

# 重载 PgBouncer
pgbouncer -R databases.ini

6. 清理:释放 replication slot

-- 在新集群上删除订阅(这会通知发布端释放 slot)
\c myapp_production
DROP SUBSCRIPTION myapp_prod_sub;

-- 在旧集群上确认 publication 和 slot 已清理
\c myapp_production
DROP PUBLICATION myapp_prod_pub;

slot 释放后,旧集群的 WAL 积压和磁盘压力立刻下降。

7. 保护你的 replication slot(Patroni 环境)

如果你的旧集群跑在 Patroni 上,Patroni 重启时会丢弃它不认识的 slot。整体迁移中这是灾难,逐库迁移中也值得防护——毕竟一个库的 slot 丢了也要重新同步。在 patroni.yml 中明确告诉 Patroni 不要清理你的迁移 slot:

# patroni.yml — 防止 Patroni 重启时丢弃迁移用的逻辑复制 slot
postgresql:
  parameters:
    # 确保参数足够支撑你的复制 worker 数量
    max_logical_replication_workers: 4
    max_worker_processes: 8
  # 关键配置:告诉 Patroni 忽略这些 slot
  use_slots: true

bootstrap:
  # Patroni 0.9+ 支持在 DCS 配置中指定 ignore slots
  # 也可以在 patroni.yml 中设置:
  # ignore_slots:
  #   - name: myapp_prod_sub  # 与订阅名对应的 slot 名

# 更推荐的方式:在 Patroni DCS (etcd/consul) 配置中添加 ignore_slots
# 这样所有节点都能读到,避免单节点配置遗漏

不同 Patroni 版本的 ignore_slots 配置位置可能不同,请查阅你所用版本的文档确认具体写法。核心原则是:在迁移开始前就把 slot 保护配置到位,不要等重启发生了再补救。

决策清单

逐库切换几乎在所有维度占优:更易管理监控、风险更低、回滚更快、停机更短、发布端负载更小、团队对状况更清楚。整体切换只在特定约束下才合理。

选择前问自己这几个问题:

问题 逐库切换 整体切换
业务是否允许不同库暂时在不同集群? ✅ 允许 → 逐库 ❌ 不允许 → 整体
应用架构能否拆分流量,部分指向新集群? ✅ 能 → 逐库 ❌ 不能 → 整体
库数量是否超过几十个? 逐库周期更长但仍然可控 参数和 slot 管理压力极大
团队是否有整体切换的演练经验? 没经验 → 更应逐库 有经验且约束硬 → 可整体
回滚时间要求? 单库秒级回滚 可能需要小时级反向复制
发布端是否跑 Patroni? 逐库 + slot 保护 整体 + slot 保护,但一次重启代价巨大

如果你有选择,逐库切换。 一个库一个库地搬,让每个库在新集群上安顿好了再动下一个。你的 on-call 排班会感谢你。


相关推荐