Skip to content

分片、配置与迁移 ​

复制让一份状态有多个副本,分片让不同数据由不同组保存。三个副本都保存全部数据不会把可用存储容量扩大三倍;要扩展容量,需要把键空间拆成 shard,再让每个 shard 由一个复制组负责。

路由规则属于服务状态 ​

一种简单映射是 shard = hash(key) % S,再用配置表把 shard 分配给 group。固定较多逻辑 shard,使节点变化主要改变 shard→group 映射,而不是改变每个键的 hash 模数。配置必须带单调版本,例如 config 7 指定 shard 3 属于 A,config 8 把它迁移到 B。

客户端缓存配置会过期。服务收到请求时检查自己是否仍负责该分片;不负责就返回可识别的配置错误,让客户端刷新并重试。不能因为“我磁盘上还有这个键”就继续处理写入。

迁移的核心是唯一写入权 ​

天真的迁移是复制 A 的 map 到 B 然后改路由。在复制期间 A 仍接收写入,新写可能没出现在 B 的拷贝里;若 A、B 同时处理写,则产生两个互相不知的事实来源。

教学上容易推理的流程是:先在 A 的日志中提交冻结 shard 的动作,得到稳定迁移边界;B 拉取该边界的数据和去重信息;B 在自己的日志中提交安装动作;配置与状态机规则允许 B 开始服务;确认交接后 A 再删除旧数据。数据传输可以重复,安装动作必须幂等。

sequenceDiagram
  participant M as 配置服务
  participant A as 旧组 A
  participant B as 新组 B
  M->>A: 配置版本 8
  M->>B: 配置版本 8
  A->>A: 日志提交冻结 shard 3
  B->>A: 请求版本 8 的迁移数据
  A-->>B: 数据与去重表
  B->>B: 日志提交安装
  B-->>A: 安装确认
  A->>A: 安全回收旧副本
查看流程图文本
sequenceDiagram
  participant M as 配置服务
  participant A as 旧组 A
  participant B as 新组 B
  M->>A: 配置版本 8
  M->>B: 配置版本 8
  A->>A: 日志提交冻结 shard 3
  B->>A: 请求版本 8 的迁移数据
  A-->>B: 数据与去重表
  B->>B: 日志提交安装
  B-->>A: 安装确认
  A->>A: 安全回收旧副本

这是一种便于讲解的阻塞迁移方案;在线迁移可先拷贝快照再追增量,减少停写时间,但仍必须定义写入权切换的原子边界。迁移前的未确认请求可能在迁移后重试,因此去重表不能遗留在 A。

为每个 shard 设计状态机 ​

可以用 Serving / Frozen / Pulling / Installed / Retired 表达状态。事件带 (config_version, shard_id),迟到事件不能让版本倒退。B 收到重复安装不应清空后来已写入的数据;A 收到旧版本删除消息不能删除已经重新迁回来的新数据。版本号因此是安全条件,不是仅用于展示的元数据。

配置迁移与 Raft 组成员变更是不同问题:前者移动业务数据归属,后者改变同一个共识组的投票成员。不能直接把新节点列表替换进“多数派分母”而跳过成员变更协议,否则新旧集合可能各自形成互不相交的多数。

热点与负载均衡 ​

均匀分配 shard 数量不保证负载均匀。一个热键可能占据绝大多数请求,拆更多普通 shard 也无法把单键原子更新任意分散。可用读副本、缓存、热点隔离或在业务允许时分解计数,但每种优化都会改变一致性、失效和聚合问题。

跨分片范围查询需要扇出并合并结果;跨分片事务还需要协调提交。分片减少每组状态规模,却增加协调与运维复杂度,不能只看单机吞吐。

自测:B 已安装但确认丢了,A 应立即删除数据吗?

不能仅凭超时推断成功。应可安全重复查询或安装,获得足够的交接证据后再回收;协议还要允许 A 保留旧数据但停止对外服务,避免把“仍保存字节”和“仍有写入权”混为一谈。

来源:MIT 6.5840、Raft 成员变更与快照。