PAYATHON 2026

无主分布式存储新增节点时,如何迁移数据并安全切换读写流量?

数据小陈

新增节点时,应将“发现节点”和“接管数据”分成两个阶段。新节点可以先加入 gossip、复制数据,迁移范围内的读写仍由旧 owner 处理;等历史数据和迁移期间的增量变更全部同步完成后,再切换路由。

当前读取返回 null/none,是因为路由已经指向新节点,而对应数据还在旧节点。仅推迟开放读取也不够:如果迁移期间直接将写入交给新节点,旧节点上的快照可能覆盖新值,或者不同节点读到不同结果。

为迁移范围设置独立状态

建议按 key range 或分片维护迁移状态,不要只给整个节点设置一个 ready 标志。一个节点可能已经完成部分范围的迁移,其他范围仍在复制。

可以使用以下状态,名称仅为设计示例:

  • JOINING:节点已被发现,还不承担新分配范围的业务请求。
  • STREAMING:正在复制快照和增量,旧 owner 继续处理该范围的读写。
  • CUTTING_OVER:正在完成最后一段增量同步,并阻止旧 owner 继续接受该范围的新写入。
  • ACTIVE:新 owner 已接管该范围的读写。

gossip 可以传播节点信息和迁移状态,但收到新节点信息后不应直接切换所有权。节点存活状态和数据就绪状态需要分别记录。

用快照和增量日志覆盖迁移期间的写入

假设某个范围从节点 A 迁移到节点 B,迁移期间由 A 接受该范围的写入,可以按以下流程执行:

  1. 在 A 上建立一致的快照边界,并记录对应的变更序号 s0。
  2. 将快照传输到 B。复制期间,该范围的读写仍发往 A。
  3. A 持续记录 s0 之后发生的变更,包括插入、更新和删除。
  4. B 导入快照后,按顺序重放这些变更。
  5. 在切换阶段确定最终边界 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 服务、后台复制、短暂停写切换”的流程,更容易明确每个阶段的数据归属和写入边界。

备注:内容仅供参考。