文章目录
说明
- 这里提供详细的实现思路和实验原理详解,提供骨架级别的绝大部分代码。
整体架构
- lab4 整体的实验说明十分冗长,我们需要详细阅读它来完全理解我们需要构建什么。话不多说,直接上图:
- 在这个实验中,我们要创建一个 “分布式的,拥有分片功能的,能够加入退出成员的,能够根据配置同步迁移数据的,Key-Value数据库服务”。
- 在Lab4A中,我们需要构造 ShardCtrler , 运行时,多个ShardCtrler Servers 组成一个Raft管理的集群,这个集群共同维护了一个数组 []Config。一个Config即代表测试者对整个服务集群的一次“最新配置指令”。Config的具体理解见图中。同时,通过shardctrler/client 提供了一个可以Query这个集群Config信息的接口,因此,用户以及后续的ShardKVCluster都可以通过这个接口来获取集群最新的Config信息。
- Lab4B则要求我们实现 分片的KeyValue服务。系统规定分片数量为NShards = 10.我们将要构建多个ShardKV Cluster(也就是一个用gid来区别的Group), 每个Group 负责几个分片(至少一个)。
- 每个Group负责的分片会根据Config信息的不同而变化。因此,在有新的Config之后,这些Group需要根据他在彼此间进行互相迁移。迁移过程中相应分片无法对外提供服务。
- 而客户端根据hash函数得到要操作的 KEY 属于的 Shard, 在去查询Config信息,又能得到这个Shard属于哪个Group在负责。然后去相应的Group Raft Leader处请求操作数据即可。
几个整体性问题
-
一个比较重要的是,Lab4,sepecailly B Test, 将会十分消耗CPU资源,而且Go -race模式下Goroutine是有一个8128的限制,因此,我们要尽量减少程序中的RPC通信量,不进行不必要的,冗余的通信。这一点最重要的就是许多事,例如SendDataToOtherGroup,尽量通过Leader去单独做,成功后再把结果同步给大家的方式来实现。
-
整个实验实际上就是Lab3的一个扩展版本。操作由GET,PUT,APPEND,扩展出了JOIN, LEAVE, MOVE, 还有后面你自己定义的Command类型。这些Command的共同处理流程就是:
-
- 全员监听,但只在Leader处能够被触发。触发方式可能是收到Client请求,或者由本地过程中的其他函数调用触发。
-
- Leader检测触发条件和执行条件,如果满足,将Op交由Raft同步,根据情况可以设置WaitChan等待同步结果。
-
- Raft Apply Command , EveryServer in Group 都能收到,每个Server单独根据Command进行【Duplicate Detection】 ->【ExeCommandWithArgs】-> 【SnapShotRaftStateIfNeed】->【SendMessageToWaitChanIfNeed】
-
- Leader收到自身RaftCommandApplier向WaitChan发送的执行结果信息,返回给客户端或者进行重试处理。
-
-
后续所需要的无论是NewConfig的拉取,还是数据迁移,本质上都是这样的过程。只不过Lab3给你定义了2个,Lab4让你自己设计两个而已。
Lab4A
整体
- 这里要我们实现集群的”配置管理服务“的部分。首先我们先理解几个操作的含义:
- Join : 新加入的Group信息。
- Leave : 哪些Group要离开。
- Move : 将Shard分配给GID的Group,无论它原来在哪。
- Query : 查询最新的Config信息。
- 其实,这里就能看出来,它非常像我们实现过的Lab3. Join, Leave, Move其实就是PutAppend, Query就是Get. 因此,我们只需要在Lab3的代码逻辑上修改就可以了。主要修改的就是:
- 替换几大操作。
- 在Join, Leave之后,需要根据分片分布情况进行负载均衡(reBalance).其实就是让每个集群负责的分片数量大致相同,并且进行尽量少的数据迁移。
- 一些细节。
- 余下的,一些像 ”重复命令检测“之类的完全不少。因此,我们先把lab3B的代码copy过来即可。仅通过lab4A的测试来看,不需要实现snapshot。
Client
- 几乎无需修改,和lab3相同。添加ClientId, requestId来检测duplicate,也可以记录上一次成功访问呢的LeaderId,进行访问优化。我们以Join为例子,代码为:
func (ck *Clerk) Join(servers map[int][]string) {
ck.requestId++
server := ck.recentLeaderId
args := &JoinArgs{
Servers: servers,ClientId: ck.clientId,Requestid: ck.requestId}
for {
reply := JoinReply{
}
ok := ck.servers[server].Call("ShardCtrler.Join", args, &reply)
if !ok || reply.Err == ErrWrongLeader {
server = (server+1)%len(ck.servers)
continue
}
// try each known server.
if reply.Err == OK {
ck.recentLeaderId = server
return
}
time.Sleep(RequsetIntervalTime * time.Millisecond)
}
}
Server
- 逻辑和Lab3相同,Server收到Client请求后交付Raft并指定Channel等待结果,Raft 持续Apply Command,并执行响应操作及去重等行为,再将结果返回给Wati Channel, 从而返回给用户。
- 指定Wait Channel的同时需要设置超时机制。
- Join & Leave之后需要 ReBalance Shards[].
- 我们仍然以Join为例:
- Join RPC Handler :
func (sc *ShardCtrler) Join(args *JoinArgs, reply *JoinReply) {
if _,ifLeader := sc.rf.GetState(); !ifLeader {
reply.Err = ErrWrongLeader
return
}
op := Op{
Operation: JoinOp, ClientId: args.ClientId, RequestId: args.Requestid, Servers_Join: args.Servers}
raftIndex,_,_ := sc.rf.Start(op)
// create WaitForCh
sc.mu.Lock()
chForRaftIndex, exist := sc.waitApplyCh[raftIndex]
if !exist {
sc.waitApplyCh[raftIndex] = make(chan Op, 1)
chForRaftIndex = sc.waitApplyCh[raftIndex]
}
sc.mu.Unlock()
select {
case <- time.After(time.Millisecond*CONSENSUS_TIMEOUT):
if sc.ifRequestDuplicate(op.ClientId, op.RequestId){
reply.Err = OK
} else {
reply.Err = ErrWrongLeader
}
case raftCommitOp := <-chForRaftIndex:
if raftCommitOp.ClientId == op.ClientId && raftCommitOp.RequestId == op.RequestId {
reply.Err = OK
} else {
reply.Err = ErrWrongLeader
}
}
sc.mu.Lock()
delete(sc.waitApplyCh, raftIndex)
sc.mu.Unlock()
return
}
- RaftCommand Join Handler
func (sc *ShardCtrler) ExecJoinOnController(op Op) {
sc.mu.Lock()
sc.lastRequestId[op.ClientId] = op.RequestId
sc.configs = append(sc.configs,*sc.MakeJoinConfig(op.Servers_Join))
sc.mu.Unlock()
}
func (sc *ShardCtrler) MakeJoinConfig(servers map[int][]string) *Config {
lastConfig := sc.configs[len