Appearance
数据迁移与切库方案
定位:工程实践方法论(工程方法论系列收官篇之一)。换库、分库分表落地、机房搬迁、大表加字段,本质都是同一件事。这件事是:在不停止写入的前提下,把数据与流量从一个"真相源"搬到另一个。迁移方案的全部设计目标只有一个——每时每刻都知道当前以谁为准、出事能退回哪里。与 数据库怎么分库分表(为什么迁)互链。与 系统切流通用方案(流量侧灰度通用套路)互链。与 如何出一个技术方案(方案评审视角)互链。
三条铁律
1)任一时刻只有一个真相源:切写之前旧库为准,切写之后新库为准。双写不是两个真相,是"一主一从影",从影侧的失败只记日志不阻塞业务。 2)每一步都有闸门:上一步验收不通过不进下一步。每一步都保留回退到上一步的能力,直到观察期满主动收口。 3)迁移链路是常驻基础设施:全量是一次性任务,增量同步是常态化链路。要按线上系统标准配监控(延迟/断流/脏数据告警)。不是跑完就扔的脚本。
迁移六步法
text
①双写 ②迁移 ③校验 ④灰度读 ⑤切写 ⑥回收
───── ───── ───── ───── ───── ─────
写路径同时 存量分批拷贝 条数/抽样/ 读流量按 写流量切到 摘双写、
写旧+新 + binlog 增量 业务三维 1%→10%→ 新库(旧库 观察完整
(旧为准, 追平,同步 核对, →100% 切 改反向写 业务周期
新写失败 链路常驻 全绿才放行 到新库,双 兜底,可一 后下线
只记日志) ↓ 读比对差异) 键回切)
不一致→停在原地修数据
═══ 闸门:每步验收不通过不进下一步;每步保留回退到上一步的能力,直到收口 ═══逐步要点:
- ①双写:新表/新库写失败降级为日志(本地消息表或 MQ 补偿),绝不能让新库故障拖垮旧库主链路。开关化(配置中心),可秒摘。
- ②迁移:存量 + 增量两条腿并行(见下节),迁移期间业务照常写旧库。
- ③校验:三维核对(见下节),是进入灰度前的硬闸门。
- ④灰度读:按用户尾号/白名单比例把读切到新库,双读比对(读新读旧各一份,以旧为准返回)。比对差异打点,用真实流量验证新库语义,出问题用户无感。
- ⑤切写:写切到新库,旧库转为反向双写(新为准写旧)。这一步是回滚窗口最窄的时刻,要选低峰、要有自动回切预案。
- ⑥回收:观察期覆盖一个完整业务周期(至少包含一次月末结算/大促类峰值)。才允许摘反向链路、下线旧库。观察期内的全链路容量验证参考 3+1保障 中的全链路压测。
全量 + 增量与断点续迁
text
全量迁移(分片断点)
┌────────────────────────────────────────────────────┐
│ 按 PK 分 chunk:(0,100w], (100w,200w] ... │
│ 每 chunk 完成后写 checkpoint:chunk_id/max_pk/ │
│ binlog 位点/状态 │
│ 崩溃重启:跳过 done 分片,从断点 chunk 续跑 │
│ 幂等写入:INSERT ... ON DUPLICATE KEY UPDATE │
│ (重跑安全是断点续迁的前提,写入必须幂等) │
└────────────────────────────────────────────────────┘
增量追平(binlog 位点)
全量期间业务仍在写旧库 ──► row binlog ──► 解析回放到新库
消费位点(GTID)随 checkpoint 持久化 ──► 重启从位点续消费
追平判据:复制延迟 < 1s 持续 10 分钟 且 校验通过- 先记位点再拷数据:全量开始前先记录 binlog 位点,增量从该位点消费。否则存量快照与增量流之间有缝隙。
- 增量回放要保序:同一行的变更按 binlog 顺序应用,多分片消费时按主键哈希分区保行内有序。
- 同步链路监控三件:延迟(位点差换算成秒)、断流(消费心跳)、错误行数(回放失败进死信人工修)。
数据校验三维
| 维度 | 做法 | 抓什么问题 | 成本 |
|---|---|---|---|
| 条数 | 按分片 count(*) 新旧对比(或每行 checksum 聚合后按分片比对) | 整片丢失/重复/漏回放 | 低 |
| 抽样 | 随机主键或哈希抽样 N 万行,全字段逐列比对 | 字段映射错、精度丢失、默认值差异、时区 | 中 |
| 业务核对 | 关键对账接口/报表在新旧各跑一遍比对(金额总和、状态分布、当日单数) | 语义级错误——前两维抓不到的"数据都对但业务不对" | 高 |
- 三维是递进的:条数不一致先修数再谈其他。抽样是统计置信,不是证明。业务核对才是迁移的验收标准(金融场景对账不平,前两维全绿也不能放行)。
- 校验要在增量暂停窗口内做(短暂停回放、比对、再续),否则边比边写永远对不齐。比对结果差异要能下钻到行级清单。
踩坑高发区(每一条都是真实事故的形状)
- 自增主键冲突:双写两侧各自自增,一旦发生回切,主键区间打架。迁移初始化时就把新库自增起点调到远高于旧库的水位,或全面改用分布式 ID(雪花类)。
- 双写顺序写死:正向阶段"先旧后新",反向阶段顺序对调。代码里不能写死顺序假设,顺序由配置驱动。旧库写成功、新库写失败要落补偿队列,而不是静默丢弃。
- 字段精度与默认值漂移:时间精度(秒 vs 毫秒)、decimal 位数、新库列默认值不同、字符集 collation 差异。抽样校验做全字段逐列比对就是为抓它们。
- 缓存与 DB 的一致性窗口:切写瞬间,旧值还躺在缓存里。切写前主动失效热点 key,或接受短暂脏读。金融场景不可接受,则灰度读期间双读比对加清缓存兜底。
- 漏迁移"看不见的写入路径":定时任务写状态表、归档任务写历史表、运营后台直改配置表。迁移范围要按变更入口清单(谁在写这张表)核对,而不是只看表清单。
- 对账口径没对齐:新旧库的"当日订单数"统计口径(时区/状态过滤/软删)不一致。业务核对永远差几百单。对账 SQL 先在双库跑同一份历史数据验证口径,再上线。
回滚窗口设计
| 阶段 | 真相源 | 回滚动作 | 数据代价 | 窗口长度 |
|---|---|---|---|---|
| ①双写 | 旧库 | 摘新库写 | 零(新库可重建重来) | 天级,可从容 |
| ④灰度读 | 旧库 | 读比例回 0 | 零 | 天级 |
| ⑤切写初期 | 新库 | 写切回旧 + 反向链路补差 | 秒级窗口内新写需对账补 | 小时级,最窄 |
| ⑥回收前 | 新库 | 反向链路=最后回滚位 | 随时间上升 | 周级,必须主动收口 |
- 切写窗口三看:错误率、P99 延迟、双读比对差异率,任一越限自动(或一键)回切。回切动作要在演练里真实执行过,不是纸面预案(演练纪律见 故障复盘与OnCall体系)。
- 窗口必须收口:长期"双写+反向双写"挂着,等于系统永远背着迁移的复杂度。任何一次双写故障都会演变成数据分叉事故。收口日期在方案评审时定死。
大表 DDL:gh-ost 原理
亿级大表直接 ALTER 会长时间锁写。gh-ost(github online schema change)用"影子表 + binlog 回放 + 原子 rename"把锁窗口压到秒级:
text
原表 t(亿级行) 影子表 _t_gho(空表,目标结构)
│ ①全量拷贝:按主键 chunk SELECT→INSERT,速率受控 ▲
│ ②增量回放:gh-ost 伪装成从库拉 row binlog, │
│ 把 t 的增删改同步应用到 _t_gho(与①并行) │
▼ │
业务写 t ──row binlog──────────────────────────────┘
③收尾 cutover:延迟≈0 且低峰时段 → 秒级锁表 →
原子 RENAME t ⇄ _t_gho(rename 前可演练、可随时取消放弃)| 对比项 | gh-ost | pt-online-schema-change |
|---|---|---|
| 变更捕获 | 伪装从库读 binlog,异步旁路 | 原表建 AFTER 触发器,写路径内联 |
| 侵入性 | 无触发器,与外键/级联/触发器存量冲突小 | 触发器放大每次写,高峰期易拖垮主库 |
| 限流 | 按从库延迟/主库负载动态 throttle,可暂停可恢复 | 需自行控制 chunk 间隔 |
| cutover | 原子 rename,可演练可放弃 | 依赖触发器追尾 + rename |
- 选型一句话:主库写压力大、有从库可读 binlog、要随时可停——gh-ost;存量触发器不可避、无 binlog 权限——pt-osc。
- gh-ost 迁移单表结构,跨库/分库分表搬迁(数据搬家)用本篇六步法——两者是"改结构"与"搬数据"两类问题。
实践清单
- [ ] 方案里写明每个阶段的真相源、回滚动作、窗口时长,收口日期评审时定死
- [ ] 新库写失败降级为日志,绝不阻塞旧库主链路;双写开关配置中心秒级可摘
- [ ] 存量分 chunk + checkpoint + 幂等写入,任何一步崩溃可断点续跑
- [ ] 新库自增起点已调高或已切换分布式 ID,防回切主键冲突
- [ ] 迁移范围按"变更入口清单"核对,定时任务/运营后台这类非主流程写入路径无一遗漏
- [ ] 增量链路有延迟/断流/死信三类告警,按线上系统标准值守
- [ ] 校验三维全绿(业务核对含对账)才进灰度;差异可下钻到行级
- [ ] 灰度读双读比对打点差异率,切写窗口三看指标越限自动回切
- [ ] 回滚/回切动作在预演环境真实执行过,不是纸面预案
- [ ] 观察期覆盖完整业务周期后才回收旧库;反向链路下线当天全量对账一次
两问两答
双写阶段为什么要"新写失败只记日志",不一致不是从这里产生的吗?
双写阶段的真相源是旧库,新库只是"影子"。让新库写失败阻塞主链路,等于给还没切流的库赋予了拖垮线上的权力——方向反了。影子侧的失败进补偿队列异步重放,靠增量链路的追平能力兜底。真正的对账闸门在第三步校验,而不是在业务写路径上。原则只有一条:新库的一切问题,都不能让旧库的用户买单。
校验三维全绿了,为什么还必须灰度读?
校验证明的是"迁移时刻的数据一致",灰度读验证的是"新库作为真相源之后的语义一致"。执行计划差异(优化器版本不同)、隔离级别与锁行为差异、字符集排序差异、慢查询超时。这些不产生"数据不同",产生"行为不同"。只有让真实读流量带着真实的参数组合去撞新库。并用双读比对兜底,才能在用户无感的前提下把行为差异暴露完。
一句话总结
双写铺路、增量追平、校验放行、灰度过渡、切写最险、回收收口。迁移工程的安全感不来自"不会出问题",而来自"每一步出问题都知道退到哪里、代价是什么"。这条"同步链路 + 双向复制 + 流量切换"的底座,正是异地多活单元化的技术前提(见 多活容灾与单元化)。
2026-10 新增(工程方法论系列)。