29.4 在线迁移状态机
在线迁移不是一条命令,而是一个有进入条件、退出证据和失败转移的状态机。把 runbook 写成“先全量,再增量,最后切流”,现场仍会争论:什么叫追平、何时禁止旧库写入、目标 写过以后还能否回退。
本节把这些模糊动词改成可观测状态。每次转移都要求证据;证据不足就留在原状态,不用 截止时间替代正确性判断。
29.4.1 预检查、全量、增量、追平与冻结窗口
先定义状态,再填命令
| 状态 | 进入条件 | 退出证据 | 失败时动作 |
|---|---|---|---|
PREFLIGHT | 迁移合同、owner、窗口已批准 | 兼容性、容量、权限、网络和回退演练通过 | 修合同,不创建长寿命 slot |
BASELINE | snapshot/slot 边界已建立 | 全量 manifest、reject ledger、对象验证通过 | 停装载,保留输入与证据后重建目标 |
STREAMING | 基线与增量无缝衔接 | subscription/consumer 稳定,目标持续跟随 | 修 apply/connector,不切流 |
CATCHUP | 进入切换前观察 | marker 已在目标可见,lag 和 retained WAL 入预算 | 降写入、扩容或推迟窗口 |
FROZEN | 写围栏已生效 | 旧端写测试失败,最终 marker 和强校验通过 | 解除围栏或转前滚修复 |
CUTOVER | 路由变更已批准 | 新连接身份正确,目标写 canary 成功 | 按回退矩阵决策 |
OBSERVE | 目标承接生产流量 | SLO、数据、任务、slot、日志持续合格 | 回退或前滚 |
EXITED | 观察窗口和退出条件满足 | 迁移签收、资产和凭据收尾完成 | 不再把旧源当即时回退方案 |
状态不允许跳转,例如没有 FROZEN -> final validation,不能从仍在双写的
STREAMING 直接宣布 CUTOVER。
preflight 要检查迁移语义,不只检查端口
预检查至少覆盖:
source/target system identifier, version, encoding, locale, timezone
schema, types, extensions, functions, collations, generated/identity columns
table size, churn, large transactions, partition topology
primary key and replica identity
RLS, owners, grants, triggers, rules
DDL change policy
sequences and identity allocation
large objects and external object references
publication filters and subscription options
WAL generation, slot retention budget, disk headroom
network throughput, latency, TLS/HBA and credential lifetime
application compatibility, connection pool and prepared statements
backup, restore, rollback and reconciliation procedure“目标能连通”只证明网络路径存在。比如目标缺少 collation、sequence 没有同步、源表无 replica identity,都可能在增量或切流阶段才暴露。
Pigsty 的迁移任务可以生成环境检查、schema、publication/subscription、进度、差异和 sequence 操作的上下文与脚本;它不能替业务确认 trigger 语义,也不知道应用路由和 外部副作用。生成脚本应进入评审和版本控制,不能把“生成成功”当作“迁移完成”。
“追平”要有业务可见 marker
单看:
SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)
FROM pg_replication_slots
WHERE slot_name = 'pg36_shop_slot';只能看到 publisher 与 consumer acknowledgement 的位置关系。它不必然证明目标业务 查询已经看见某一笔事务,也不覆盖下游索引、缓存和异步任务。
更可靠的追平协议是:
- 在源端业务表或专用控制表提交唯一
migration_marker; - 记录该事务的业务 ID、提交时间和附近 LSN;
- 等目标端通过普通应用路径读到 marker;
- 同时确认 subscription worker、table state、slot、错误统计和 retained WAL 正常;
- 在一段稳定窗口中重复,而不是只采一个瞬时零延迟。
正式实验同步完 500 个 insert、200 个 update、100 个 delete 后,等待 marker 在目标端 可见,再比较两端 manifest。这个证据比“延迟图降到 0”更接近切换要求。
冻结窗口要证明旧写入真的失败
冻结不等于在群里发一句“请勿写库”。写围栏可以来自:
- 撤销专用 runtime role 的 DML 权限;
- 将旧端业务入口切换为只读;
- 应用 feature flag 阻止写请求;
- 停止 scheduler、ETL、CDC 回写和运维脚本;
- 对无法配合的 writer 建立数据库级拒绝规则。
然后用旧应用凭据执行负向 canary,确认 INSERT/UPDATE/DELETE 失败,同时需要的
SELECT 仍可用于核对。表 owner、superuser、SECURITY DEFINER 函数和绕过业务入口
的后台任务必须单独盘点;仅 revoke 普通角色无法约束这些路径。
29.4.2 影子读、双读、切流和观察
影子读与双读解决的是“应用是否认同”
数据库摘要相同,不代表应用行为相同。目标端可能因为 collation、timezone、扩展版本、 查询计划或 session 参数给出不同结果。切流前可以逐级放量:
| 方法 | 主结果来自 | 目标端副作用 | 适合发现 |
|---|---|---|---|
| 离线 replay | 源端录制流量 | 禁止 | SQL/类型/性能不兼容 |
| 影子读 | 源端 | 严格禁止 | 结果、错误码、延迟差异 |
| 采样双读 | 源端,后台比较目标 | 只读 | 长尾和真实参数差异 |
| 小比例真实读 | 目标端 | 应用正常读副作用需评估 | 连接池、缓存、SLO |
“读”也可能有副作用:SELECT nextval(...)、advisory lock、临时表、审计函数、缓存
填充、SELECT ... FOR UPDATE 都不适合直接镜像。影子层必须有 SQL allowlist、超时、
并发限制和结果脱敏;不能为验证目标把源端峰值流量翻倍。
结果比较应按业务语义规范化:
unordered result -> sort by declared key
timestamp -> normalize to UTC and agreed precision
numeric -> agreed scale and rounding
JSON -> canonical form
expected nondeterminism -> exclude or compare distribution同时比较错误类别、行数、关键字段、P50/P95/P99 与资源消耗。只比较 HTTP 200 会漏掉 返回空集、排序变化和悄悄截断。
切流要拆开连接地址与数据权威
应用可能经过:
DNS
-> VIP / load balancer
-> HAProxy service
-> PgBouncer
-> PostgreSQL primary迁移 runbook 必须指出实际控制点及缓存时间。改变 DNS 不会自动清掉旧 PgBouncer 连接;改变 HAProxy backend 也不会让应用已持有的 session 消失。切换步骤通常包括:
- 记录旧 route generation、目标 endpoint 和回退 endpoint;
- 降低或确认 TTL,准备 health check;
- 在源端建立写围栏并做最终 marker/校验;
- 刷新 sequence,保证目标下一个值高于已迁移最大值;
- 修改唯一的权威路由控制点;
- drain/重建旧池,拒绝新连接进入源端写服务;
- 新连接查询
system_identifier、database、server address 与只读状态; - 执行目标 canary 并从应用层回读;
- 进入限时观察,不立即拆源。
本章实验刻意不修改真实 Pigsty 路由,只在私有证据中模拟
source -> target -> source。这证明状态机与回退逻辑,不声称实验真的改过平台入口。
生产切流必须由应用或网络 owner 执行并给出实际路由证据。
观察窗口看四层信号
| 层 | 关键证据 |
|---|---|
| 应用 | 成功率、错误分类、业务转化、队列积压、关键任务 |
| 连接/路由 | 新旧连接数、endpoint 身份、池等待、事务/会话模式 |
| PostgreSQL | TPS、延迟、锁、WAL、checkpoint、autovacuum、复制/订阅错误 |
| 数据 | canary、分桶摘要、业务不变量、target-only writes、reconciliation |
应预先写出阈值和观察时间,例如“连续 60 分钟错误率不高于基线 + 0.1%,关键不变量 为零,异常桶为零”。现场再决定“看起来还行”无法形成一致决策。
29.4.3 回退点、前滚点与不可逆动作
回退不是把连接串改回去
切流前,源是唯一 writer,回退通常只是解除源围栏并放弃目标。目标开始承接写入后, 状态发生根本变化:
source last state S
target receives new writes T1..Tn
route points back to source若没有 reverse CDC 或显式 reconciliation,源端不知道 T1..Tn,直接回路由会造成
已成功请求消失。应在切换前确定矩阵:
| 所在阶段 | 目标端是否有独占写 | 默认策略 |
|---|---|---|
| baseline / streaming / frozen | 否 | 安全取消,恢复源写 |
| cutover,canary 尚未产生业务事实 | 否 | 快速回退 |
| cutover,只有可识别 canary | 是,可枚举 | 对账并回放 canary 后回退 |
| observe,已有普通业务写 | 是 | reverse sync/reconciliation,或前滚修复 |
| 已执行破坏性 schema/源退役 | 是且难逆 | 按灾难恢复或专门回迁方案处理 |
正式实验在目标写入唯一 canary,得到 sequence 值 900001。模拟回退前,证据包先识别
并把这条目标独占数据对账到源,再在源写入 rollback canary,最终两端 manifest 相同。
这个演示只覆盖“可枚举的一条目标写”,不能推导出任意生产写流都可这样回退。
sequence 是典型的切换缝隙
逻辑复制不复制 sequence 当前值。正式实验切流前看到目标 sequence 当前值仍为 1,
而源端最大 order_id 已到 900000。流程显式执行 setval,目标 canary 才得到
900001。
sequence 处理要考虑:
is_called语义;- identity column 背后的实际 sequence;
- 多 writer 是否预留不重叠区间;
- cached values 与仍存活的旧连接;
- 目标独占值回退后会否冲突;
- gap 是否被业务错误地当成连续性失败。
标出不可逆动作
以下动作不应与普通切流混在一个“一键脚本”:
删除 source cluster/database
删除 backup 或缩短 WAL 保留
drop publication/slot before exit criteria
执行 target-only destructive DDL
重新使用旧 sequence 区间
轮换后销毁唯一可用的旧凭据
让外部系统产生不可补偿副作用对 schema 使用 expand/contract:先部署双方都理解的扩展形态,完成迁移与观察,再在独立 变更中收缩旧字段。这样问题出现时可以前滚修复,而不是在数据迁移、应用发布、破坏性 DDL 三者同时发生时赌一个总回退按钮。
每次决策至少记录:
state: OBSERVE
route_generation: 42
source_fence: active
target_only_write_boundary: marker-20260729-17
rollback_possible: conditional
required_reconciliation: target-writes-after-marker
decision_owner: migration-commander
evidence_bundle: /secure/migrations/run-...
next_decision_deadline: ...回退能力是一项需要实验证明的属性。未演练、未定义数据合并边界的“随时可回退”,只是 一句安慰。
上一节:批量装载与数据校验 · 返回本章目录 · 下一节:异构同步的语义损失 · 查看全书目录 · 查看索引中心