第一步阅读文档
https://pdos.csail.mit.edu/6.824/labs/lab-kvraft.html
阅读第8章
https://www.infoq.cn/article/raft-paper
根据文档弄清楚交互流程
第二步 理解代码框架
CLIENT 端
type Clerk struct {
这里定义CLERK 属性
func MakeClerk(servers []*labrpc.ClientEnd) *Clerk {
新建一个CLERK,同时会告知一组SERVER
func (ck *Clerk) Get(key string) string {
发送GET请求的代码
func (ck *Clerk) PutAppend(key string, value string, op string) {
发送PUT 或 APPEND请求的代码
SERVER 端
type Op struct {
Op是要发向RAFT的COMMAND
type KVServer struct {
mu sync.Mutex
me int
rf *raft.Raft
applyCh chan raft.ApplyMsg
KV SERVER 自己会有把锁,有一个RAFT PEER, 自己的序号,还有一个和RAFT通信的APPLY CHANNEL
func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {
func (kv *KVServer) PutAppend(args *PutAppendArgs, reply *PutAppendReply) {
上面2个是和CLERK 通信的 RPC HANDLER
func (kv *KVServer) Kill() {
KILL 掉一个KV SERVER的命令
func StartKVServer(servers []*labrpc.ClientEnd, me int, persister *raft.Persister, maxraftstate int) *KVServer {
初始化一个KV SERVER,测试函数会在START SERVER的时候调用,注意不要阻塞,应该快速返回。
第三步 实现Client Get && PutAppend
因为要发GET RPC 给SERVER,所以看下 ARGS 和 REPLY
因为如果找错了LEADER,需要换一个重试,而是不是对的LEADER 应该就是WRONG LEADER这个域
在发请求的时候,需要用FOR,找到正确的LEADER为止。
根据文档,还要实现幂等性,这里的逻辑之后实现幂等性的时候还要加
第四步 from Hint
You will probably have to modify your Clerk to remember which server turned out to be the leader for the last RPC, and send the next RPC to that server first. This will avoid wasting time searching for the leader on every RPC, which may help you pass some of the tests quickly enough.
在PUT APPEND里同样的代码
client 告一段落,下面开始写SERVER端
第五步 实现Op
op就是发送给RAFT的COMMAND
第六步 实现 Get RPC HANDLER
一开始很NAVIE的实现,就是提交给RAFT后,去直接听applyCh。但是想到可能会有其他的CLERK也往LEADER发信息,那么这个APPLY CH 可能返回的不一定是你要的那个MSG。而这个MSG从CHANNEL里读掉就没了。那个需要的CLERK可能就拿不到了。
带着问题我去读了STUDENT GUIDE
里面说的和我想的一模一样
You might start off by having your service, whenever it receives a client request, send that request to the leader, wait for Raft to apply something, do the operation the client asked for, and then return to the client. While this would be fine in a single-client system, it does not work for concurrent clients.
解决方法文章下面也有暗示。
这个坎还是要思考一下的
可以通过Raft的Start函数返回请求在log中的index,对于每个index创建一个channel来接收执行完成的消息,接收到了之后,知道是完成操作了,然后回复给client
需要单独开一个goroutine来监视apply channel,一旦底层的Raft commit一个,就立马执行一个。
让我们来整理下思路。
当发送一个GET请求,它会去调用START方法,然后拿到INDEX。去监听这个INDEX上的CHANNEL。
后台有一个GO ROUTINE,会循环处理APPLY CHANEL上的消息。来了一个消息,就打到对应的INDEX的CHANNEL上。
然后一直到那个GET请求的CHANNEL上的消息来了,返回给客户端。
根据
you can tell whether or not the client’s operation succeeded based on whether the operation that came up for that index is in fact the one you put there. If it isn’t, a failure has happened and an error can be returned to the client.
我们可以知道如果这2个INDEX不一致的话,就说明有错。基于上述思路来写代码。
1.构建map[int] chan Op, 初始化
- 后台开一个GOROUTINE, 去监听applyCh, 发现来了一个,做处理,找到applyMsg 的CHANNEL ,NOTIFY BY OP
- 在GET HANDLER里 发送这个命令,拿到INDEX,随后去监听INDEX直到消息回来。然后去KV.DB里读,还需要CHECK OP是不是一致,不一致的话,就代表有错。
实现
PASS 3A1
第七步 对共享变量加锁
PASS 3A2
第八步 实现幂等
目标
Add code to cope with duplicate Clerk requests, including situations where the Clerk sends a request to a kvserver leader in one term, times out waiting for a reply, and re-sends the request to a new leader in another term. The request should always execute just once. Your code should pass the go test -run 3A tests.
参见STUDENT GUIDE Duplicate detection
One simple and fairly efficient one is to give each client a unique identifier, and then have them tag each request with a monotonically increasing sequence number. If a client re-sends a request, it re-uses the same sequence number. Your server keeps track of the latest sequence number it has seen for each client, and simply ignores any operation that it has already seen.
RPC参数这边我的思考是 读请求本身就具备幂等性。所以我加在PUTAPPEND ARG上
SERVER端 增加幂等判断
这里有一个思考的点,幂等判断是加在发送PUTAPPEND的地方还是加在专门监听APPLY MSG的地方。
如果不仔细想,可能会觉得加在PUTAPPEND的地方,这样可以减少往RAFT 发LOG的开销。不过这样就会有一个问题。因为你加在这边了,你是不能再PUTAPPEND的时候就去更新clientid to seqnum 的这个MAP,因为可能你这个消息发出去,不一定保证会被COMMIT.贸然更新就会出错,我们要知道何时被COMMIT还是要回到监听APPLY MSG的地方。所以把幂等逻辑放在了第二处。
pass until Partition
第九步 实现TIMEOUT 逻辑
到这步之前,可以测过所有非PARTITION的。
PARTITION这个,因为RAFT LEADER会拿不到足够的票数无法COMMIT,所以CLERK 会被BLOCK住。根据要求,这边需要加个超时逻辑
对应更新,GET 和 PUTAPPEND的代码。
之后可以跑过全部3A TEST了
zyx@zyx-virtual-machine:~/Desktop/mit6824/mit6.824/src/kvraft$ go test -run 3A
Test: one client (3A) ...
... Passed -- 15.5 5 6919 148
Test: many clients (3A) ...
... Passed -- 16.7 5 18013 741
Test: unreliable net, many clients (3A) ...
... Passed -- 16.9 5 2246 604
Test: concurrent append to same key, unreliable (3A) ...
... Passed -- 2.3 3 183 52
Test: progress in majority (3A) ...
... Passed -- 0.9 5 85 2
Test: no progress in minority (3A) ...
... Passed -- 1.2 5 112 3
Test: completion after heal (3A) ...
... Passed -- 1.0 5 47 3
Test: partitions, one client (3A) ...
... Passed -- 22.6 5 12458 116
Test: partitions, many clients (3A) ...
... Passed -- 23.6 5 24778 519
Test: restarts, one client (3A) ...
... Passed -- 20.5 5 23348 155
Test: restarts, many clients (3A) ...
... Passed -- 21.6 5 75660 742
Test: unreliable net, restarts, many clients (3A) ...
... Passed -- 24.3 5 3749 617
Test: restarts, partitions, many clients (3A) ...
... Passed -- 29.1 5 69641 606
Test: unreliable net, restarts, partitions, many clients (3A) ...
... Passed -- 29.1 5 2817 421
Test: unreliable net, restarts, partitions, many clients, linearizability checks (3A) ...
... Passed -- 25.5 7 8061 751
PASS
ok kvraft 251.633s
第九步 代码优化
因为_,isLeader := kv.rf.GetState()
的判断被包含在index,_,isLeader := kv.rf.Start(originOp)
所以上面的代码可以去掉。
实现KILL,方法和之前在RAFT 实现的时候一样,加个KILL CH
用go test -race
测下来没有问题。