平滑扩容与数据迁移

分库分表上线一年后数据量翻倍,需要从 64 张表扩到 128 张。如果直接按新规则上线,几乎全部数据都要换位置,而业务不能停机。平滑扩容解决的就是:在不停机、不丢数据、可回滚的前提下,把数据搬到新的分片布局上。

为什么取模扩容如此痛苦

假设原本 64 个分片,改为 65 个:

旧规则:shard = id % 64      新规则:shard = id % 65
id = 100 → 旧 36,新 35      变了
id = 200 → 旧 8, 新 5       变了
id = 64  → 旧 0, 新 64      变了

因为 64 与 65 互质,只有极少数 id 的落点在两个规则下相同,几乎 100% 的数据都要迁移。迁移期间既要搬历史数据又要处理增量写入,风险极高。结论:分片数量尽量取 2 的幂,避免随意加 1

双倍扩容法

把 64 扩到 128,而不是 65。关键在于新旧规则的关系:

旧规则:shard = id % 64   → 0 ~ 63
新规则:shard = id % 128  → 0 ~ 127

同一个 id:
  id % 128 < 64  → 新分片号 == 旧分片号,数据不用动
  id % 128 >= 64 → 新分片号 == 旧分片号 + 64,只需搬到新增分片

更重要的性质是不跨库合并:旧分片 3 的数据只会流向新分片 3 或新分片 67,永远不会流向新分片 5。因此每个旧分片可以独立迁移,互不影响,也便于逐个灰度。

迁移前:64 个分片承载全部数据
迁移中:新分片 64~127 由对应旧分片 0~63 复制而来,双写生效
迁移后:128 个分片各承载约一半数据

用一致性哈希减少迁移

方案扩容时迁移比例实现复杂度分布均匀性
取模几乎 100%均匀
双倍扩容约 50%均匀
一致性哈希约 1/N(N 为节点数)需虚拟节点才均匀

一致性哈希的代价是实现复杂、要靠虚拟节点解决倾斜,而且"数据归谁"不再直观。实际工程中,取模 + 双倍扩容因为简单可预测,使用得更普遍。

迁移的五个阶段

阶段动作回滚方式
1 双写新写入同时写旧、新分片,读仍走旧分片关掉双写开关
2 存量同步按旧分片逐批把历史数据复制到新分片停止同步任务,丢弃新分片数据
3 数据校验比对两侧行数与关键字段无需回滚,修复差异后重跑
4 灰度切换按比例把读流量切到新分片并逐步放大把读流量比例调回 0
5 清理确认稳定后停止双写、下线旧分片旧分片保留期内可随时恢复
阶段1 双写:write(id, data) 先写旧分片,再写新分片(失败进补偿队列重试)
阶段2 同步:按旧分片独立进行,主键游标分页 + 断点续传
阶段3 校验:COUNT(*) 比对 + 主键差集 + 抽样字段比对
阶段4 切换:读比例 1% → 5% → 20% → 50% → 100%,逐步观察
阶段5 清理:停双写 → 观察 7 天 → 归档并下线旧分片

迁移进度与断点记录表

长跑任务必须有断点,否则中断一次就得重来:

CREATE TABLE migrate_progress (
  task_name     VARCHAR(64) NOT NULL COMMENT '任务名,如 order_64to128',
  old_shard     INT NOT NULL COMMENT '旧分片号',
  last_id       BIGINT NOT NULL DEFAULT 0 COMMENT '已同步到的最大主键',
  migrated_rows BIGINT NOT NULL DEFAULT 0 COMMENT '已迁移行数',
  status        TINYINT NOT NULL DEFAULT 0 COMMENT '0未开始 1进行中 2完成 3失败',
  updated_at    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (task_name, old_shard)
) ENGINE=InnoDB COMMENT='迁移进度表';
  • 用主键游标分页,不要用 LIMIT offset——深分页会越跑越慢;
  • 写入用幂等 upsert,任务重跑不会产生重复数据;
  • 单批几百到几千行并限速,避免同步任务打满主库 IO。
-- 按主键游标取一批(id 必须有序)
SELECT id, user_id, amount, status FROM t_order_0
WHERE id > 1000000 ORDER BY id LIMIT 1000;

-- 幂等写入新分片,重复执行结果一致
INSERT INTO t_order_9 (id, user_id, amount, status)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE amount = VALUES(amount), status = VALUES(status);

一致性校验

校验最容易被省略,也最不能省:

校验方式做法能发现的问题
行数比对两侧 COUNT(*) 对比漏迁、重复迁移
主键集合比对分批取两侧主键做差集精确定位缺失的行
抽样字段比对按比例抽样比对全部字段字段截断、时区偏差
全量比对分批全字段比对(离线跑)最彻底,但耗时长
-- 行数比对:在两侧分别执行后对比结果
SELECT COUNT(*) FROM t_order_0;
-- 输出:12480000

回滚预案

回滚触发:灰度切换后错误率上升、校验差异超阈值、新分片延迟或可用性异常
阶段1~2 出错 → 停双写与同步任务,清理新分片残留数据
阶段3   出错 → 暂停同步、修复差异后重跑,不影响线上读
阶段4   出错 → 读流量比例调回 0,恢复读旧分片
阶段5 之后   → 旧分片数据必须保留一段时间,不能立即删除
  • 双写与读切换都做成配置开关,秒级生效,不要靠发版;
  • 旧分片在确认稳定前不删除,保留期建议不少于一个业务周期;
  • 切换放在低峰期,并按 1%、5%、20%、50%、100% 逐级放量,每级至少观察一个完整业务周期。

常见坑

  • 双写不一致:写新分片失败却没有补偿机制,数据会悄悄丢失;
  • 增量同步不幂等:binlog 重复消费会产生重复行或覆盖新值;
  • 全量与增量之间有缝隙:全量同步开始后的新写入必须靠增量补齐;
  • 主键冲突:两侧各自自增会撞号,应以分布式 ID 或业务主键为准;
  • 大事务长锁:别用一条大事务提交百万行,会长时间占用 undo log 与锁;
  • 忽略监控:迁移期间必须盯主库延迟、IO、连接数与新分片错误率。

小结:平滑扩容的核心是"双写 + 存量搬迁 + 校验 + 灰度切换 + 可回滚"。分片数取 2 的幂并用双倍扩容,可把迁移量降到约一半且不跨库合并;迁移按阶段推进,每阶段留开关与断点,校验环节不可省略,旧分片确认稳定后再下线。

笔记加载中…