Multi-Raft 中的 Region 合并与两阶段提交协调
在分布式数据库(如 TiKV、CockroachDB)的 Multi-Raft 体系中,分片机制是动态自适应的:
- 当数据量膨胀时,Region 会自动**分裂(Split)**为两个小分片;
- 而当业务大量删除历史数据、或者在大促结束后数据量急剧缩减时,集群中会产生海量数据量极小(如不足 10MB)的“空洞分片”。
成千上万个稀疏小 Region 的存在,会造成严重的资源浪费:
- 每个 Region 都需要维护独立的 Raft 心跳与 Raft 日志状态机,白白浪费海量的 CPU 周期与网络心跳带宽;
- 降低跨分片范围扫描(Range Scan)的查询性能。
为了收缩集群拓扑、回收空洞资源,Multi-Raft 系统必须支持Region 合并(Region Merge)。
然而与简单的本地 Region 分裂不同,Region 合并跨越了两个完全独立的 Raft 状态机(源 Region 与 目标 Region)!
如果合并协调不当,极易在两个独立 Raft 组之间引发致命的状态不一致与数据丢失。
深入剖析基于两阶段协调(Two-Phase Coordination / Commit-Merge)的 Multi-Raft Region 合并机制,是掌握分布式共识高级架构的必由之路。
+--------------------------------------------------------------------------+ | Multi-Raft Region 合并 (Merge) 两阶段执行全景 | +--------------------------------------------------------------------------+ | 初始状态: | | Region A (区间: ["a", "m"), 仅剩 5MB 数据, 源 Region) | | Region B (区间: ["m", "z"), 目标 Region) | +--------------------------------------------------------------------------+ | 阶段一: 准备阶段 (PrepareMerge on Region A) v | 1. Region A Leader 发起 Raft 提案: PrepareMerge(Target: Region B) | | 2. Region A 多数派提交该日志并锁定本地状态机: | | -> 🛑 停止接收任何新的客户端写请求! | | -> 递增 Region A 的 Epoch Version | +--------------------------------------------------------------------------+ | 阶段二: 提交合并阶段 (CommitMerge on Region B) v | 3. Region A 向 Region B 发送内部 RPC 传递合并上下文与当前状态机日志位点 | | 4. Region B Leader 发起 Raft 提案: CommitMerge(Source: Region A) | | 5. Region B 多数派提交该日志: | | -> 将本地 Key 范围原子扩展为: ["a", "z") 🚀 | | -> 物理接管 Region A 的全部历史数据,并销毁 Region A 的 Raft 实例! | +--------------------------------------------------------------------------+1. 为什么跨 Raft 组的 Region 合并极其复杂?
在 Multi-Raft 体系中,每一个 Region 是一个在物理上完全自治、拥有独立任期号(Term)与日志序列号(Log Index)的孤立宇宙:
- Region A 和 Region B 可能由完全不同的物理节点副本构成;
- Region A 的 Leader 根本无法直接向 Region B 的状态机写入日志;
- 如果 Region A 在合并中途突然发生 Leader 切换、或者网络分区导致 Region A 又接收了新的客户端写入,盲目将数据合并进 Region B 会直接导致数据覆盖与幻读!
2. 两阶段合并(Commit-Merge)的严密状态机
工业级 Multi-Raft(如 TiKV)通过严格的两阶段协议来保证合并的绝对安全性:
第一阶段:源 Region 冻结(PrepareMerge)
- 全局调度中心(PD)向源分片
Region A发送合并调度指令; Region A的 Leader 发起一条内部 Raft 日志:AdminCmd::PrepareMerge { target_region_id: Region B, min_index };- 状态机冻结与 Epoch 递增:
- 当该日志在
Region A内部达成多数派 Commit 并 Apply 时; Region A将本地状态标记为Merging,永久拒绝后续所有客户端的读写请求;- 将
Region A的epoch.version递增,确保所有持老路由的客户端在访问时立刻被拦截。
- 当该日志在
第二阶段:目标 Region 接管与原子扩容(CommitMerge)
Region A向Region B发起跨分片内部 RPC,告知其:Region A已完全冻结,当前日志最大 Index 为 $I_A$;Region B的 Leader 检查自身 Key 区间与Region A是否严格物理连续(例如Region A.end_key == Region B.start_key);Region B发起 Raft 提案:AdminCmd::CommitMerge { source_region: Region A };- 原子合并生效:
- 当该日志在
Region B内部被多数派 Commit 并 Apply 时; Region B原子地将自身的start_key修改为Region A.start_key(范围合并为["a", "z"));- 将
Region B的epoch.version递增; - 本地存储引擎正式将原属于
Region A的数据纳入Region B的管理生命周期,并在后台异步销毁Region A的 Raft 运行时。
- 当该日志在
3. 合并中途遭遇异常的自愈与回滚(Rollback)
如果在第一阶段PrepareMerge成功后,目标Region B突然发生物理宕机或网络永久隔离,导致第二阶段迟迟无法推进:
Region A维护一个超时定时器;- 超时后,
Region A可以通过向自身发起一条AdminCmd::RollbackMergeRaft 日志,解冻本地状态机并恢复正常读写,避免分片陷入永久死锁。
用严密的两阶段状态机跨越独立共识组的鸿沟,让成千上万个动态分片在收缩与合并中始终保持毫厘不差的一致性,这是现代分布式存储架构的至高工程艺术。