无主分布式存储新增节点时,如何迁移数据并安全切换读写流量?
新增节点时,应将“发现节点”和“接管数据”分成两个阶段。新节点可以先加入 gossip、复制数据,迁移范围内的读写仍由旧 owner 处理;等历史数据和迁移期间的增量变更全部同步完成后,再切换路由。
当前读取返回 null/none,是因为路由已经指向新节点,而对应数据还在旧节点。仅推迟开放读取也不够:如果迁移期间直接将写入交给新节点,旧节点上的快照可能覆盖新值,或者不同节点读到不同结果。
为迁移范围设置独立状态
建议按 key range 或分片维护迁移状态,不要只给整个节点设置一个 ready 标志。一个节点可能已经完成部分范围的迁移,其他范围仍在复制。
可以使用以下状态,名称仅为设计示例:
JOINING:节点已被发现,还不承担新分配范围的业务请求。STREAMING:正在复制快照和增量,旧 owner 继续处理该范围的读写。CUTTING_OVER:正在完成最后一段增量同步,并阻止旧 owner 继续接受该范围的新写入。ACTIVE:新 owner 已接管该范围的读写。
gossip 可以传播节点信息和迁移状态,但收到新节点信息后不应直接切换所有权。节点存活状态和数据就绪状态需要分别记录。
用快照和增量日志覆盖迁移期间的写入
假设某个范围从节点 A 迁移到节点 B,迁移期间由 A 接受该范围的写入,可以按以下流程执行:
- 在 A 上建立一致的快照边界,并记录对应的变更序号
s0。 - 将快照传输到 B。复制期间,该范围的读写仍发往 A。
- A 持续记录
s0之后发生的变更,包括插入、更新和删除。 - B 导入快照后,按顺序重放这些变更。
- 在切换阶段确定最终边界
s1,确认 B 已应用到s1,再允许 B 接管。
快照必须与日志边界配套,确保每次写入都进入快照或待重放日志,不能留有空档。对于内存存储,普通并发遍历不一定能提供这样的快照,需要通过锁、版本化数据结构或其他快照机制建立明确的边界。
如果先导入快照、再重放日志,应确保重放完成前快照不会继续覆盖数据。如果选择交错导入,则需要版本检查,避免旧快照覆盖较新的变更。
这里的序号只需在该迁移范围的变更流中有明确顺序,不要求整个集群使用同一个全局序号。如果该范围实际允许多个节点独立接受写入,还需要覆盖所有写入来源,并沿用系统已有的版本比较和冲突处理规则。
切换前设置明确的写入边界
“增量已经追得很近”不能作为切换条件。只要 A 还在接受写入,就可能继续产生尚未复制的数据。
对于当前按 owner 转发请求的设计,可以在切换时短暂阻塞该范围的写入。这种方案容易实现和验证:
A 继续服务该范围的读写
B 导入 A 的一致快照,并重放后续变更
开始切换:
A 阻塞该范围的新写入
A 等待已接受的写入完成
A 记录最终变更序号 s1
B 应用所有截至 s1 的变更并确认完成
提交新的路由版本:该范围由 B 接管
B 开始服务
被阻塞的请求按新路由重试或转发
这段暂停只作用于正在迁移的范围。持续时间取决于剩余增量、在途请求以及路由切换机制,无法仅凭现有材料给出具体数值。
还要处理路由传播延迟:部分入口节点可能仍把请求发给 A。接管完成后,A 应转发请求,或返回可重试的路由版本错误,不能继续在本地接受并确认这些写入。
可以给路由记录附加单调递增的 epoch,帮助识别旧路由请求。但 epoch 本身不能保证各节点对切换结果达成一致。如果要求网络分区期间也不会出现两个 owner 同时接受写入,就需要具备相应一致性保证的元数据更新和写入隔离机制;仅靠最终传播的 gossip 无法提供这一保证。
请求路由应区分目标 owner 和当前服务 owner
下面是 Go 风格的路由示意。类型和函数均用于说明设计,不是可直接运行的完整实现:
func servingOwner(m Migration) NodeID {
switch m.State {
case Joining, Streaming:
return m.OldOwner
case CuttingOver:
// 仍发往旧节点,由旧节点执行切换屏障。
// 写请求可能等待,或收到可重试错误。
return m.OldOwner
case Active:
return m.NewOwner
default:
panic("unknown migration state")
}
}
ACTIVE 必须表示接管决定已经提交,且新节点已经具备服务条件。仅仅计算出新节点应该拥有哪些 key,不应改变当前服务 owner。
处理删除、失败和旧数据清理
迁移日志必须包含删除操作。如果支持 TTL,还应保留正确的过期语义,避免迁移后数据重新出现或存活时间被延长。日志可能重传时,重放也需要支持去重或幂等处理。
切换提交前,如果 B 迁移失败,可以放弃本次迁移,由 A 继续服务。切换提交后,不能直接将路由退回 A,因为 A 可能已经缺少 B 接管后的写入;此时应按故障恢复流程处理。
旧数据不宜在切换后立即删除。应先确认接管结果和旧路由请求的处理方式,再按保留策略清理。
不建议把“新节点查不到就去旧节点查”作为完整迁移方案。缺失结果可能表示数据尚未复制,也可能表示该 key 已被删除,直接回查旧数据可能返回过期值。对于当前设计,先实现“旧 owner 服务、后台复制、短暂停写切换”的流程,更容易明确每个阶段的数据归属和写入边界。
备注:内容仅供参考。
