数据库 Resharding 的在线切换与回滚
Resharding 是分库分表架构绕不过去的难题。系统上线时用 uid % 2 把数据分到两个库,跑了两年发现容量不够,要扩成四个库——uid % 4。这个 % 号一变,大约 75% 的数据需要重新搬家。搬数据本身不难,难的是三件事同时做到:在线搬、搬完瞬间切、切坏了能回滚。
下面从最朴素的双写到 Vitess 的 VReplication,逐一拆解业界主流的 resharding 在线切换方案。核心问题只有三个:数据怎么不丢,流量怎么瞬间切,故障怎么秒级回。
问题的本质:hash mod 变化引发的数据海啸
hash 取模是最常见的分片路由策略。uid % N 的 N 一旦改变,绝大部分数据的归属分片都会变。
以 2 库扩 4 库为例:原先 uid % 2 = 0 的数据全在 A 库,扩容后 uid % 4 把这批数据拆成了 uid % 4 = 0(留 A 库)和 uid % 4 = 2(搬到新 C 库)。B 库的情况类似,一半数据要搬去新 D 库。
1 | |
如果 N 不是倍数关系(比如 3 扩 5),情况更糟——几乎所有数据都要重新分配。所以业界有一条经验法则:分片数永远按 2 的幂次规划(2→4→8→16),每次扩容只需搬约 50% 的数据。
不过即使只搬 50%,对一个日均千万级写入的在线系统来说,"边跑边搬"的挑战仍然巨大。
停机迁移——简单但代价最大
最直觉的做法:停服务 → 导数据 → 改路由 → 启服务。
整个过程的数据一致性由"没有新写入"来保证。停机窗口取决于数据量,千万级别的表通常需要数小时。对于 7×24 的互联网系统,这个代价往往不可接受。银行核心和社保基金偶尔还会用这种方案,但更多是出于合规和风控考量而非技术限制。
停机迁移的回滚很简单:切换前保留旧库快照,发现问题就回退路由、恢复快照。
双写迁移——五阶段渐进切换
双写迁移是业界使用最广泛的在线扩容方案。核心思路是让新旧两套分片拓扑在一段时间内共存,通过五个阶段渐进切换。
阶段一:同步
先把存量数据从旧库搬到新库。常见做法是用 Canal 或 Debezium 订阅旧库的 binlog,按新的分片规则写入新库。这一步只需要"追",不需要改应用代码。
搬运期间旧库仍在接收写入。新写入的数据通过 binlog 实时同步到新库,因此新库的数据会逐步逼近旧库。
阶段二:双写
当新库的数据与旧库基本追平后,在应用层开启双写:每次写操作同时写旧库和新库。此时可以关闭 binlog 同步程序,由应用层的双写逻辑接管增量同步。
双写的一致性策略通常是"以旧库为准":旧库写成功即返回业务成功,新库写失败则记日志后异步补偿。整个迁移期间旧库始终是 source of truth,任何时刻都可以安全回滚。
sequenceDiagram
participant App as 应用层
participant Old as 旧库(2 分片)
participant New as 新库(4 分片)
App->>Old: 写入 uid=123
Old-->>App: 成功
App->>New: 异步写入 uid=123(按新分片规则路由)
alt 新库写入失败
New-->>App: 失败
Note over App: 记日志,异步补偿
end
阶段三:校验
双写稳定运行后,启动数据校验程序,对旧库和新库的数据做全量对比,发现不一致就以旧库为准修复新库。
校验通常按批次进行:每次取一批 ID 范围的数据,逐行比对字段值和更新时间。如果不一致率在万分之一以下且持续数天无新增差异,进入下一阶段。
阶段四:切读
把读流量从旧库切到新库。这一步风险相对可控——读操作不影响数据完整性,发现异常可以秒级切回旧库。
灰度切换是常见做法:先把 1% 的读流量导向新库,观察响应时间和错误率,逐步放大到 100%。
阶段五:切写
这是整个迁移过程中风险最高的一步:停掉旧库的写入,让新库成为唯一的写入目标。
切写的关键动作是一个很短的"停写窗口":
- 暂停所有写请求(通常在 Proxy 层拦截,返回重试或排队等待)
- 等待新库追平旧库的最后一批双写数据(通常只需几秒)
- 切换路由规则,让写请求指向新库
- 恢复写请求
这个停写窗口就是用户能感知到的"瞬间切换",实际持续时间从几百毫秒到几秒不等。
graph LR
A["阶段一:同步"] --> B["阶段二:双写"]
B --> C["阶段三:校验"]
C --> D["阶段四:切读"]
D --> E["阶段五:切写"]
E -.->|回滚| D
D -.->|回滚| C
C -.->|回滚| B
B -.->|回滚| A
双写迁移的回滚能力来自一个关键设计:在切写完成之前,旧库始终是 source of truth。任何阶段发现异常,只需关闭双写、切回旧库路由即可。切写完成之后,回滚需要额外手段——下文的"反向复制"就是为此而设。
CDC/Binlog 增量追平——对应用零侵入
双写方案的最大缺点是需要修改应用代码。CDC(Change Data Capture)方案通过数据库底层的 binlog 机制实现同步,对应用层完全透明。
工作原理
CDC 工具(Canal、Debezium、Maxwell)伪装成 MySQL 从库,实时解析 binlog 中的行变更事件,按新的分片规则写入目标库。
整个流程分两步。
全量复制:对旧库做一致性快照(如 mysqldump --single-transaction),按新分片规则导入新库。记录快照时刻的 binlog position。
增量追平:从快照的 binlog position 开始,持续解析增量变更并应用到新库。当延迟降到秒级以内时,执行切换。
1 | |
切换与回滚
切换时刻的处理与双写方案类似:短暂停写 → 等 CDC 追平最后一批变更 → 切路由 → 恢复写入。
回滚策略:在切换后一段时间内保持旧库在线,同时反向配置 CDC(从新库同步回旧库),确保旧库数据持续更新。一旦需要回滚,切回旧库路由即可。
CDC 方案的优势是零代码侵入,劣势是依赖 binlog 格式(必须是 ROW 模式)和 CDC 工具的稳定性。对于写入量特别大的系统,CDC 工具可能成为瓶颈。
升级从库——不搬数据的 2^n 倍扩
这个方案利用 MySQL 主从复制的特性,堪称最优雅的扩容手段。
前提条件
系统已经配置了主从复制,每个主库有一个或多个从库。分片数按 2 的幂次规划。
操作步骤
以 2 库扩 4 库为例:
1 | |
操作序列:
- 将从库 A0、B0 升级为独立主库
- 修改路由规则:uid % 2 改为 uid % 4
- 在 A 库中删除 uid % 4 = 2 的数据(这些数据已经在 A0 中)
- 在 A0 库中删除 uid % 4 ≠ 2 的数据
- B 和 B0 做同样处理
由于从库通过主从复制天然持有主库的完整数据副本,升级为主库后只需要删除不属于自己分片的数据即可。整个过程不需要跨网络搬运数据。
切换与回滚
切换动作就是:断开主从复制 + 修改路由规则,两步可以在秒级完成。
回滚相对麻烦:需要把已升级的从库重新接回主库恢复主从关系。如果升级后已经删除了冗余数据,回滚就需要重新全量同步。因此实践中通常不急着删冗余数据,等确认稳定后再清理。
局限性
只支持 2 倍扩容(2→4→8→16),不能 3→5 或 2→3。如果初始分片数规划不当(比如分了 3 个库),后续扩容会很痛苦。
一致性哈希与虚拟分片——从源头减少数据迁移
前面几种方案有一个共同前提:hash mod 变化导致大量数据需要重新分配。一致性哈希从根本上缓解这个问题。
一致性哈希
把哈希空间组织成一个环(0 ~ 2^32-1),数据和节点都映射到环上的某个位置。每条数据归属于顺时针方向遇到的第一个节点。
新增或移除节点时,只有相邻区间的数据需要迁移,其余数据不动。N 个节点新增 1 个节点时只需迁移 1/N 的数据,远优于 hash mod 的"几乎全部重分配"。
但一致性哈希有一个缺陷:节点在环上的分布不均匀,导致数据倾斜。虚拟节点技术为每个物理节点在环上创建多个映射点(通常 100~200 个),大幅改善数据分布的均匀性。
1 | |
虚拟分片
虚拟分片是一致性哈希思想在分库分表场景下的工程化应用。在逻辑层预先划分大量虚拟分片(如 1024 个),物理部署时将多个虚拟分片映射到同一台物理机。扩容时只需调整映射关系,把部分虚拟分片迁移到新机器。
1 | |
虚拟分片方案的切换和回滚非常灵活:路由表是一个"虚拟分片 → 物理机"的映射配置,原子更新这张映射表就完成了切换。回滚只需还原映射配置。
ShardingSphere 的弹性扩容就采用了这种思路:预设大量逻辑分片,物理部署时按需聚合,扩容时只需调整逻辑到物理的映射。
切换的"瞬间"到底怎么实现
不管采用哪种迁移方案,最终都要回答一个问题:新旧拓扑的流量切换如何做到"原子性"?
数据库层面没有跨实例的原子切换机制。所谓的"瞬间切换"发生在数据库之上的路由层。
Proxy 层路由切换
ShardingSphere、MyCat、ProxySQL 等数据库中间件维护着一张路由表,定义了分片键到物理库表的映射关系。切换时更新这张路由表,所有后续请求立即按新规则路由。
更新路由表的操作本身是原子的(单次配置推送),但需要处理"正在执行的请求"。常见策略是:
- 等待所有进行中的事务完成(drain)
- 短暂拒绝新请求(< 1 秒)
- 推送新路由配置
- 恢复接受请求
这个短暂的拒绝窗口就是业务层感知到的"抖动"。
gh-ost 的原子 RENAME
gh-ost(GitHub Online Schema Tool)在单表级别实现了一种精妙的原子切换。虽然 gh-ost 主要用于在线 DDL 而非 resharding,但其切换思路对 resharding 有直接借鉴价值。
gh-ost 的 cut-over 流程:
- 创建一张与原表结构相同的影子表(ghost table)
- 通过 binlog 持续同步原表的变更到影子表
- 切换时刻:对原表加锁 → RENAME 原表和影子表互换 → 释放锁
MySQL 的 RENAME TABLE 是元数据操作,不涉及数据拷贝,执行时间在毫秒级。通过 RENAME 实现表名互换,应用层完全无感知。
1 | |
Vitess 的 SwitchTraffic / ReverseTraffic
Vitess 是 YouTube 开源的 MySQL 分片中间件,在 resharding 的切换和回滚设计上最为完整。
Vitess 的 resharding 基于 VReplication——一套内建的 binlog 复制引擎。切换流程:
- 启动 VReplication,将源分片的数据持续复制到目标分片
- SwitchTraffic:先切读流量,再切写流量;切写时 Vitess 暂停源分片写入,等 VReplication 追平,然后原子切换路由
- 切换完成后,Vitess 自动建立反向 VReplication:从新分片向旧分片同步数据
反向复制是 Vitess 回滚能力的核心。调用 ReverseTraffic 即可把流量切回旧分片,因为旧分片通过反向复制一直保持着最新数据。
sequenceDiagram
participant C as 客户端
participant V as VTGate(路由层)
participant S as 源分片(2 片)
participant D as 目标分片(4 片)
Note over S,D: VReplication 持续同步
S->>D: binlog 复制
Note over V: SwitchTraffic — 切读
C->>V: SELECT
V->>D: 路由到新分片
Note over V: SwitchTraffic — 切写
V->>S: 暂停写入
S->>D: 追平最后一批变更
V->>V: 原子切换路由
V->>D: 恢复写入
Note over S,D: 自动建立反向复制
D->>S: 反向 binlog 同步
Note over V: 如需回滚:ReverseTraffic
V->>V: 切回源分片路由
回滚:切不回去才是最大的恐惧
resharding 的回滚能力取决于一个核心问题:旧拓扑的数据还是不是最新的?
三种回滚策略
保持旧库在线:切换后不急着下线旧库。新库出问题时直接把路由切回旧库。这种方式最简单,但要求切换期间旧库没有落后。适用于双写方案——双写阶段旧库始终在接收写入。
反向复制:切换后从新库向旧库建立反向数据同步(CDC 或 binlog 复制)。旧库持续更新,随时可以接管。Vitess 的 ReverseTraffic 就基于这个原理。
快照加重放:切换前对旧库做快照,回滚时恢复快照再重放切换期间的 binlog。恢复时间最长但最可靠,适用于对数据一致性要求极高的金融场景。
回滚的时间窗口
回滚能力不是永久的。随着新拓扑运行时间增长,回滚成本越来越高:
- 切换后数小时内:直接切路由,秒级回滚
- 切换后数天内:反向复制保持旧库更新,分钟级回滚
- 切换后数周:旧库可能已下线,回滚需要重建环境,小时级甚至更长
业界通常规定一个"观察期"(如一至两周),观察期内保持旧库和反向复制在线,确认无异常后才正式下线旧库。
graph TD
A["切换完成"] --> B{"观察期内?"}
B -->|是| C["旧库在线 + 反向复制运行中"]
C --> D{"发现异常?"}
D -->|是| E["切回旧库路由(秒级)"]
D -->|否| F["继续观察"]
B -->|否| G["下线旧库 + 停止反向复制"]
G --> H["回滚需重建环境(小时级)"]
业界工程实践对比
大规模电商的虚拟分片实践
一种在大厂广泛使用的做法以 32×32 = 1024 张表为例,初始部署在 8 台物理机上(每台 4 个库实例,每库 32 张表)。扩容时不改路由规则,只把部分库实例从旧物理机搬到新物理机——本质上就是虚拟分片:1024 张表是 1024 个虚拟分片,物理搬运不涉及数据重分配。
路由规则始终是 uid 后四位 % 32 定库、uid 后四位 / 32 % 32 定表,从头到尾不变。DBA 只需把数据库实例从一台机器迁移到另一台。
Vitess
Vitess 的 Reshard 工作流是目前开源方案中最完整的:内建 VReplication 引擎不依赖外部 CDC 工具;SwitchTraffic 支持先切读后切写的灰度策略;自动建立反向复制,ReverseTraffic 一键回滚;支持任意分片规则变更,不限于 2^n 倍扩。
ShardingSphere 弹性扩容
Apache ShardingSphere 的弹性扩容采用四阶段模型:资源准备(创建新库实例,初始化元数据)→ 全量迁移(一致性快照复制)→ 增量同步(基于 binlog 的 CDC)→ 流量切换(原子更新路由配置)。ShardingSphere 作为 Proxy 层中间件,切换路由配置本身就是原子操作,对应用层透明。
Stripe 四阶段迁移
Stripe 的在线数据迁移模式虽然不完全是 resharding 场景,但思路通用:
- Dual-write:同时写新旧存储
- Backfill:迁移历史数据
- Dark-read:同时从新旧存储读取并对比结果,但只返回旧存储的结果
- Cutover:确认一致后切到新存储,下线旧存储
Dark-read 阶段是这套方案的亮点:在不影响生产流量的前提下验证新存储的正确性。每个阶段结束后系统都处于一个可安全回退到上一阶段的状态。
方案选型
| 方案 | 停机时间 | 代码侵入 | 回滚能力 | 适用场景 |
|---|---|---|---|---|
| 停机迁移 | 小时级 | 无 | 快照恢复 | 非 7×24 系统、合规驱动 |
| 双写迁移 | 秒级停写窗口 | 高(改 DAO 层) | 任意阶段可回滚 | 通用在线系统 |
| CDC/Binlog | 秒级 | 无 | 反向 CDC | binlog ROW 模式、不想改代码 |
| 升级从库 | 秒级 | 低(改路由配置) | 需重建主从 | 已有主从、2^n 倍扩 |
| 虚拟分片 | 秒级 | 低 | 还原映射表 | 需要灵活扩缩容 |
| Vitess | 秒级 | 无 | ReverseTraffic 一键 | MySQL 生态、需完整工具链 |
实际工程中这些方案经常混合使用:用虚拟分片降低数据迁移量,用 CDC 做增量同步,用 Proxy 层做原子切换,用反向复制保障回滚能力。最终选择取决于数据规模、团队技术栈、以及对停机时间的容忍度。
参考资料
- Vitess 官方文档:Resharding 工作流 https://vitess.io/docs/user-guides/migration/
- Apache ShardingSphere 弹性扩容文档 https://shardingsphere.apache.org/
- gh-ost:GitHub Online Schema Tool https://github.com/github/gh-ost
- Canal:阿里巴巴 MySQL binlog 增量订阅组件 https://github.com/alibaba/canal
- Debezium:开源 CDC 平台 https://debezium.io/


