前面一节我们处理了配置变更的需求,主要是定时从 shardctrler 中拉取配置,我们对传入 raft 模块的结构体进行了一些改造,使其能够兼容两种不同类型的请求,分别是用户操作和配置变更。

用户操作和之前的一样,并没有任何的变化,只是反解析结构体的时候需要注意。

配置变更的操作,上一节主要是简单处理了一下,主要的 shard 迁移流程需要我们继续完善。

在商讨 shard 迁移的具体流程之前,我们可以来简单看一下官方 lab4 给出的一些提示:

  • 在 KVServer 中添加定期从 shardctrler 拉取配置的代码,并且如果客户端请求的 shard 不属于当前 Group,那么应该返回 ErrWrongGroup 错误。这个加上之后,应该仍然能通过第一个测试。
  • 确保 KVServer 中的 Get、Put、Append 方法和配置变更同时发生时,需要有一致的行为。
  • 一次处理一个配置变更的请求,并且按照顺序。
  • 需要在 shard 迁移期间,确保对重复请求的过滤,保证线性一致性。
  • 在一个 shard 迁移到新的 Group 之后,原来的 Group 可以继续持有并不属于它的旧的 shard,目前这样可以简化我们的实现。但是下一节我们会处理这种情况。
  • 你可以直接将整个 map 结构放到 RPC 请求或者回复里,这能够让代码实现更加简洁。
  • 在不同的 Group 之间发送 shard 数据的时候,一般不会持有 map 的锁,例如数据发送到了另一个 Group,但是当前 Group 又要修改这个 shard 的数据,那么就会产生竞态条件。一种解决方案是传输 map 结构的时候将其拷贝一份。
  • 在配置变更期间,在一些 Group 之间可能需要双向传递 shard,如果发生了死锁,可能需要检查这个条件。

了解了这些之后,我们再来商讨一下 shard 迁移的解决方案。

简单方案

我们会启动一个后台线程(fetchConfigTask),定期从 shardctrler 拉取配置,拿到这个配置之后,就需要构造一个配置变更的命令,然后传入到 raft 模块中进行状态的同步。

Raft 状态同步完成后,我们会在一个后台 apply 协程中处理 raft 传递过来的结果。

这里我们需要判断请求的类型,如果是用户操作的请求,则和之前一样,应用到状态机即可。

如果是配置变更的请求,我们就需要处理是否有 shard 迁移的情况,比如我们在新配置中发现某个 shard 需要迁移过来,那么我们就可以拉取这个 shard 的数据,然后保存到当前 Group 中。

但是这里有一个问题,那就是 shard 的迁移可能需要较长的时间,因为这涉及到从其他的 Group 中去拉取 shard 数据(网络 IO),这时候我们的 apply 协程就会一直阻塞,直到这个请求处理完成。

由于客户端的用户操作也会经过 apply 协程处理,所以如果此时有客户端的请求到来,那么也只能阻塞等待,那些未参与此次配置变更的 shard 也不能继续提供服务。

还有一个问题是,假如我们分别需要从 G1 和 G2 拉取一个 shard,如果 G1 的拉取成功了,但是 G2 因为故障,我们并没有拉取成功。那么我们从 G1 拉取到的 shard 是否应该正常响应客户端的请求呢?

在 Lab 4 的 challenge 2 中,对这种情况进行了描述。

Challenge 2 要求我们在处理配置变更的时候,如果是未参与的 shard,需要继续提供对客户端的服务,而不是也和其他 shard 一样等待配置变更完成后才恢复服务。

Challenge 2 还要求我们注意,即便是整体的配置变更未完成,只要 shard 已经迁移过来,则可以立即开始提供服务,而不用等待配置变更全部完成。

针对上述的这两种情况,我们就需要调整上面这个简单的方案。

目前方案

前面我们已经了解到,既然每个 shard 在配置变更的时候,可以是独立的,比如一个 Group 中有的 shard 受到了配置变更的影响,而有的并没有受到。而且只要一个 shard 完成了迁移,就需要立即提供服务,不用等待所有的配置变更完成。

根据这样的特征,我们可以给每个 shard 加上一个状态的标识,分别有下面这几种状态

  • Normal
  • MoveIn
  • MoveOut
  • GC

结合这个状态,下面是一个 Shard 迁移的流程示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
G1 1 3 5
G2 2 6
G3 4 7

配置变更,shard 5 从 G1 -> G3

-------
后台线程拉取配置,更新 shard 状态
G1 5
MoveOut

G3 5
MoveIn
-------
后台 shard 迁移线程定期执行

G3:检测到 5 处于 MoveIn 状态,则从原所属 Group 中获取到该 shard 的数据,然后通过 raft 模块进行同步
-------
在 apply 协程中获取到 shard 迁移的请求,则将 shard 的数据插入到当前 Group 中
并且将当前 shard 的状态置为 GC

G3 5
GC
此时 shard 5 已经可以正常提供服务了

改动点

  1. 配置变更的任务只修改 shard 状态,不进行实际的 shard 迁移操作
  2. 新增 shard 迁移的后台线程,定期执行 shard 的迁移