29.7 实战:迁移 `pg36_shop`
前六节建立了 logical replication、CDC、全量校验、迁移状态机、异构语义与多集群边界。 本节把它们压缩到一个可重复的 Pigsty 双集群实验:
pg-test / pg36_shop_src
-> publication + logical slot
-> pg-meta / pg36_shop_dst
-> subscription实验不是“看见几行复制过去”就结束,而要主动制造 consumer 停滞、显式 apply conflict 和不会报错的 silent drift,再完成 sequence 校准、写围栏、模拟切流、条件回退与精确 清理。
29.7.1 完成全量加增量同步
先读边界,再运行脚本
本章只允许在已确认的 Pigsty 四节点开发沙箱执行:
source pg-test / PostgreSQL 18
target pg-meta / PostgreSQL 18
data deterministic synthetic fixture
capture L0 read-only preflight
exercise L2 bounded two-cluster disposable fixture
production forbidden
real route never changed完整合同在
lab-contract.md,机器可读的环境、对象、数量和验收条件在
requirements.json,迁移状态与允许动作在
migration-contract.json,拓扑图在
topology.mmd。
实验只创建以下固定名称对象,并用随机 run_id marker 证明所有权:
source database pg36_shop_src
target database pg36_shop_dst
publication pg36_shop_pub
slot pg36_shop_slot
subscription pg36_shop_sub
five exact fixture roles它明确禁止读取现有业务表、修改 Pigsty inventory、修改 Patroni/持久参数、改变真实 HAProxy/PgBouncer/DNS/VIP、终止无关连接和 force-drop。marker、对象名或连接范围有一项 不匹配,runner 都失败关闭。
先做纯静态合同检查:
static/labs/ch29/task.sh lint完整实验会创建和删除两个一次性数据库,必须在已确认沙箱中指定一个新的私有证据目录:
export PG36_EVIDENCE_DIR="$(
mktemp -d "${TMPDIR:-/tmp}/pg36-ch29.XXXXXX"
)"
static/labs/ch29/task.sh all若希望分步审阅:
static/labs/ch29/task.sh capture
static/labs/ch29/task.sh exercise
static/labs/ch29/task.sh verify
static/labs/ch29/task.sh reviewcapture 在任何写入前检查:
- 两端 service、cluster、PostgreSQL major、primary 身份;
- 两个不同的 system identifier;
- source
wal_level = logical; - 目标 database、role、slot 和 subscription 起点不存在;
- 第 19、23、25、28 章上游证据存在且环境边界一致;
- 实验源文件散列与随后执行的版本一致;
- 远端临时目录和证据目录满足私有权限。
创建 schema 与逻辑复制对象
夹具包含:
shop.customers 5,000 rows
shop.orders 20,000 rows两表都有稳定主键,orders.customer_id 引用 customer;状态、金额和更新时间具有固定
业务约束。runner 在源端创建 publication 和 logical slot,在目标端创建相同 schema 与
subscription,然后等待 pg_subscription_rel 中两张表都从同步状态进入 r。
正式参考 run 证明:
source system id 7668025967696967004
target system id 7668025945980641675
customers 5,000
orders 20,000
tables ready 2
logical manifest equalsystem identifier 是本次沙箱证据,不是读者环境中的预期常量。验收的是“两端不同且分别 绑定已声明 cluster”,不是数字本身。
让初始快照与持续变更汇合
初始复制完成后,源端执行:
INSERT 500 orders
UPDATE 200 orders
DELETE 100 orders
COMMIT one unique migration marker流程等待目标读取 marker,再比较两端精确行数、有序摘要、金额合计、状态分布和业务 不变量。参考 run 的结果为:
inserted / updated / deleted 500 / 200 / 100
marker acknowledged true
logical manifest equal true这同时证明了 initial copy 与增量可以汇合,以及验证是在声明的 marker 边界之后执行。 它不证明生产大表所需时间,也不覆盖迁移期间的 DDL;后者仍需独立编排。
连接失败也是 preflight 结果
开发中的第一次候选 run 在 fixture 写入前被 HBA 拒绝,因为临时角色未匹配 Pigsty 的 group-role 分类。流程没有临时放宽认证,而是:
- 停止实验;
- 按 marker 清理两个 database、五个角色、slot/subscription;
- 证明所有临时对象不存在;
- 用
INHERIT FALSE, SET FALSE的成员关系满足 HBA 分类; - 重新从新的 run 和空证据目录开始。
生产演练也应如此:preflight 失败说明合同不成立,不能在原 run 上一边改权限一边继续, 否则最终证据无法说明实际执行了哪套安全边界。
29.7.2 注入消费者停滞与数据差异
反例一:consumer 停了,风险留在 source
runner 精确禁用 pg36_shop_sub,确认源端 pg36_shop_slot inactive,然后在源夹具中
生成固定 3,000 行变化。参考结果:
confirmed_flush_lsn unchanged true
retained WAL before 227,008 bytes
retained WAL after 2,867,128 bytes
retained WAL grew true
caught up after re-enable true禁用动作只匹配本 run 的 subscription;负载行数固定,不改变
max_slot_wal_keep_size,也不制造无限 WAL。数字取决于 tuple、full-page image、
checkpoint 和版本,教学结论是方向:
consumer 不确认 -> slot 不能推进 -> source retained WAL 增长重新启用并追平后必须再次校验 manifest,不能因 worker 恢复 running 就进入下一阶段。
反例二:显式冲突会停 apply
实验先在目标端插入:
order_id = 900000, target payload再在源端提交同一 key、不同值。目标 apply 命中唯一键,PostgreSQL 18 的
subscription 统计出现 insert_exists:
confl_insert_exists 0 -> 1
apply_error_count 0 -> 1此时 slot 仍可能存在,subscription 也仍是一个 catalog 对象,但 apply 已无法越过 冲突事务。修复流程必须先证明:
冲突表与主键
源端权威值
目标端冲突值
错误计数和日志时间
允许采取的修复方向实验删除精确的目标冲突 fixture 行,让源事务重放,再等待 marker 与 manifest 收敛。 生产环境不能把“删除目标所有冲突行后重试”写成通用脚本;不同冲突可能代表合法的 target-only write。
反例三:静默漂移不会停 apply
runner 在目标端直接修改 order_id = 1。该行之后没有新的源变化,因此:
subscription remains healthy
no apply conflict is raised
target query succeeds但 16 个稳定 hash bucket 中,只有 bucket 1 的行数/摘要不一致。实验处于切流前, 合同指定 source 为权威,因而按主键读取源行、记录修复前后摘要、修复目标并重跑所有 分桶,结果:
mismatched buckets before [1]
mismatched buckets after []这组反例区分了两种故障:
| 故障 | apply 是否报错 | 主要发现方式 |
|---|---|---|
| 唯一键/缺行等显式 conflict | 通常会 | worker/log/pg_stat_subscription_stats |
| 目标手工写、错误 backfill 等 silent drift | 不一定 | 持续 manifest、分桶和业务不变量 |
运行状态与数据等价必须分别验收。
29.7.3 验证、切流、回退并输出迁移证据包
切流前修正 sequence 并建立写围栏
逻辑复制已经把 order_id = 900000 复制到目标,但 sequence 本身没有随 DML 推进:
target sequence before 1
source max order_id 900000runner 按源端最大 ID 校准目标 sequence。随后目标 runtime canary 得到 900001,证明
不会立刻与已迁移主键碰撞。
源端则撤销 runtime 的 DML 能力,用同一凭据实际发起 INSERT:
SQLSTATE 42501
INSERT denied
UPDATE denied
DELETE denied
SELECT retained“执行过 revoke”不是证据,旧凭据的负向操作才是。生产还要盘点 owner、
SECURITY DEFINER、scheduler 和其他 writer。
只模拟路由,不碰真实平台
私有 route-history.json 记录:
source -> target -> source它只是一台状态机的模拟输入。runner 会检查:
Pigsty inventory unchanged
Patroni configuration unchanged
real HAProxy/PgBouncer/DNS/VIP unchanged
actual_platform_route_changed = false在模拟 target 阶段写入一条可识别 canary。回退前把这 1 条目标独占数据显式对账回源, 再切回 source 并写 rollback canary。最终参考结果:
customers 5,000
orders 23,402
logical manifest equal true
orphan orders 0
negative amounts 0
invalid statuses 0
source retained through rollback true这里证明的是“在目标独占写可枚举时,条件回退协议可执行”。真实业务流量已经写入目标后, 是否回退仍取决于 reverse sync、对账能力和不可补偿副作用。
证据包必须能反驳伪成功
私有证据目录包含:
preflight-evidence.json
remote/migration-evidence.json
remote/route-history.json
remote-cleanup.json
negative-report.json
validation-report.json
public-summary.json
review.txt
source file hashesreview 会检查证据权限、schema、交叉字段、源文件 hash、私密信息与 public/private 边界。validator 不只验证成功样本,还要求:
29 declared counterexamples rejected
19 live evidence mutants rejected
11 source files hash-bound也就是说,篡改 system identifier、初始行数、marker、retained WAL、conflict counter、
bucket repair、sequence、写围栏、路由边界或清理结论,都不能继续得到 pass。公开参考
摘要在
migration-run.json;它不含密码、conninfo、主机密钥
或原始行。
完成实验后可以对同一私有证据包重复审计:
static/labs/ch29/task.sh verify
static/labs/ch29/task.sh review但不能把另一个 run 的证据目录与当前源文件拼接使用。
清理也是验收阶段
正常删除目标 subscription 时,PostgreSQL 同时删除远端 main slot。runner 随后分别在 两端确认:
source database absent
target database absent
all fixture roles absent
source slot absent
target subscription absent
ordinary DROP used
force drop used = false
unrelated sessions terminated = 0
remote temp absent任何一项不成立,实验都不算完成。尤其不能为了让 CI 变绿而终止所有连接或删除所有 inactive slot。
从沙箱证据到生产迁移票据
本实验的最终决策仍是:
production_ch29_gate = pending正式票据至少还要补:
- 真实 schema/type/DDL/sequence/large-object inventory;
- 数据量、写入峰值、全量时长与 slot WAL 容量压测;
- source primary failover 和 consumer restart 演练;
- TLS、HBA、secret rotation 与权限评审;
- 应用影子读、连接池 drain、真实路由 owner 和变更窗口;
- 业务不变量、允许语义损失与 reconciliation owner;
- target-only write 边界、回退/前滚矩阵、不可逆动作;
- 备份恢复、RPO/RTO、观察窗口和退出条件。
读者完成本节后,应能提交的不是一句“逻辑复制已同步”,而是一份可以回答同步了什么、 在哪个边界相等、故障怎样暴露、切流由谁执行、何时还能回退、如何证明已清理的迁移 证据包。
上一节:多集群迁移环境 · 返回本章目录 · 下一章:推陈出新:版本升级与回滚策略 · 查看全书目录 · 查看索引中心