概述

lab2中实现了raft协议,本lab将在raft之上实现一个可容错的k/v存储服务,第一部分是实现一个不带日志压缩的版本,第二部分是实现日志压缩。时间原因我只完成了第一部分。

设计思路

如上图,lab2实现了raft协议,本lab将实现kvserver。每个raft都关联一个kvserver,Clerks发送Put(), Append(), Get() RPC给leader服务器中的kvserver,kvserver收到请求后将操作打包成Log Entry提交给raft,然后阻塞等待raft将这个Entry拷贝到其它server,当Log Entry被拷贝到大部分的server后,leader 的raft会通知kvserver(raft往管道中塞comitted Entry,kvserver通过读这个管道获取通知),kvserver执行命令,然后响应Clerk。

Clerk

客户端通过Clerk发送请求,来看下Clerk代码:

type Clerk struct {
servers []*labrpc.ClientEnd
// You will have to modify this struct. lastLeader int
cid int64
seq int
} func (ck *Clerk) Get(key string) string { // You will have to modify this function.
// 参数: 要读的key, 当前clerk的id, 请求序列号
getArgs := GetArgs{Key: key, Cid:ck.cid, Seq:ck.seq}
reply := GetReply{} for {
doneCh := make(chan bool, 1)
go func() {
//发送Get() RPC
ok := ck.servers[ck.lastLeader].Call("KVServer.Get", &getArgs, &reply)
doneCh <- ok
}() select {
case <-time.After(600 * time.Millisecond):
DPrintf("clerk(%d) retry PutAppend after timeout\n", ck.cid)
continue
case ok := <- doneCh:
//收到响应后,并且是leader返回的,那么说明这个命令已经执行了
if ok && reply.WrongLeader != WrongLeader {
//请求序列号加1
ck.seq++
return reply.Value
}
} //换一个server重试
ck.lastLeader++
ck.lastLeader %= len(ck.servers)
} return ""
}

这里只给出了Get()的代码,Put()和Append()类似,发送KVServer.Get给一个server,如果这个server不是leader,换一个server重试。直到发给真正的leader,并且leader将这个命令拷贝到大部分其它server后,然后成功执行该命令,Clerk.Get()才会返回。

KVServer

再来看下服务端的代码,KVServer处理Clerk的RPC请求:

type KVServer struct {
mu sync.Mutex
me int
rf *raft.Raft
applyCh chan raft.ApplyMsg maxraftstate int // snapshot if log grows this big // Your definitions here.
// 保存键值对
db map[string]string
latestReplies map[int64]*LatestReply
notify map[int]chan struct{}
} func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {
// Your code here.
if _, isLeader := kv.rf.GetState(); !isLeader {
reply.WrongLeader = WrongLeader
reply.Err = ""
return
} // 防止重复请求
kv.mu.Lock()
if latestReply, ok := kv.latestReplies[args.Cid]; ok && args.Seq <= latestReply.Seq {
reply.WrongLeader = IsLeader
reply.Value = latestReply.Reply.Value
reply.Err = latestReply.Reply.Err
kv.mu.Unlock()
return
}
kv.mu.Unlock() command := Op{Operation:"Get", Key:args.Key, Cid:args.Cid, Seq:args.Seq}
index, term, _ := kv.rf.Start(command) // 阻塞等待结果
kv.mu.Lock()
ch := make(chan struct{})
kv.notify[index] = ch
kv.mu.Unlock() select {
case <-ch:
curTerm, isLeader := kv.rf.GetState()
DPrintf("%v got notify at index %v, isLeader = %v\n", kv.me, index, isLeader)
if !isLeader || curTerm != term {
reply.WrongLeader = WrongLeader
reply.Err = ""
} else {
reply.WrongLeader = IsLeader
kv.mu.Lock()
if value, ok := kv.db[args.Key]; ok {
reply.Value = value
reply.Err = OK
} else {
reply.Err = ErrNoKey
}
kv.mu.Unlock()
} } }

KVServer.db用于保存键值对。

KVServer.Get()首先判断自己是不是leader,如果不是leader,直接返回,这样Clerk好重试其它server。如果是leader,先在缓存中找,看这个请求是否已经执行过了。

因为可能出现这么一种情况:如果leader commit一个Entry后立即奔溃了,那么Clerk就收不到响应,那么Clerk会将这个请求发给新的leader,新的leader收到请求后如果不做任何措施,将会二次commit该Log Entry,对于Put()和Append()请求执行两次是不正确的,所以需要一个办法防止一个请求执行两次。

可以这么做:每个Clerk都分配一个唯一的cid,每个请求分配一个唯一的序列号seq,每成功一个请求,该序列号加一。服务端记录每个客户端cid最近一次apply的请求的序列号seq和对应的响应结果,根据这个信息可知,当再次收到这个客户端的序列号小于seq的请求时,说明已经执行过了,直接返回结果。

如果之前没有执行过,那么调用

kv.rf.Start(command)

将Log Entry提交给raft,并且阻塞等待raft将这个Entry拷贝到其它大部分server,从阻塞返回后,说明这个Entry已经被拷贝到大部分server了,并且已经执行了命令,这时可以将结果返回给Clerk了。

那么在哪里接收raft的消息呢?KVServer在创建的时候会在一个线程中执行如下函数:

func (kv *KVServer) applyDaemon()  {
for appliedEntry := range kv.applyCh {
command := appliedEntry.Command.(Op) // 执行命令, 过滤已经执行过得命令
kv.mu.Lock()
if latestReply, ok := kv.latestReplies[command.Cid]; !ok || command.Seq > latestReply.Seq {
switch command.Operation {
case "Get":
latestReply := LatestReply{Seq:command.Seq,}
reply := GetReply{}
if value, ok := kv.db[command.Key]; ok {
reply.Value = value
} else {
reply.Err = ErrNoKey
}
latestReply.Reply = reply
kv.latestReplies[command.Cid] = &latestReply
case "Put":
kv.db[command.Key] = command.Value
latestReply := LatestReply{Seq:command.Seq}
kv.latestReplies[command.Cid] = &latestReply
case "Append":
kv.db[command.Key] += command.Value
latestReply := LatestReply{Seq:command.Seq}
kv.latestReplies[command.Cid] = &latestReply
default:
panic("invalid command operation")
}
} DPrintf("%d applied index:%d, cmd:%v\n", kv.me, appliedEntry.CommandIndex, command) // 通知
if ch, ok := kv.notify[appliedEntry.CommandIndex]; ok && ch != nil {
DPrintf("%d notify index %d\n",kv.me, appliedEntry.CommandIndex)
close(ch)
delete(kv.notify, appliedEntry.CommandIndex)
}
kv.mu.Unlock()
}
}

kv.applyCh这个chanel会在创建raft的时候传给raft,当某个Log Entry可以被commit的时候,raft会往这个chanel中塞,只要for循环这个kv.applyCh,就能知道已经被commit的Entry,拿到Entry后,根据其中的命令执行相应的操作,然后通知KVServer.Get()继续执行。

具体代码在:https://github.com/gatsbyd/mit_6.824_2018

如有错误,欢迎指正:

15313676365

MIT-6.824 Lab 3: Fault-tolerant Key/Value Service的更多相关文章

  1. MIT 6.824(Spring 2020) Lab1: MapReduce 文档翻译

    首发于公众号:努力学习的阿新 前言 大家好,这里是阿新. MIT 6.824 是麻省理工大学开设的一门关于分布式系统的明星课程,共包含四个配套实验,实验的含金量很高,十分适合作为校招生的项目经历,在文 ...

  2. 6.824 Lab 3: Fault-tolerant Key/Value Service 3A

    6.824 Lab 3: Fault-tolerant Key/Value Service Due Part A: Mar 13 23:59 Due Part B: Apr 10 23:59 Intr ...

  3. 6.824 Lab 3: Fault-tolerant Key/Value Service 3B

    Part B: Key/value service with log compaction Do a git pull to get the latest lab software. As thing ...

  4. 6.824 Lab 2: Raft 2A

    6.824 Lab 2: Raft Part 2A Due: Feb 23 at 11:59pm Part 2B Due: Mar 2 at 11:59pm Part 2C Due: Mar 9 at ...

  5. MIT 6.824 lab1:mapreduce

    这是 MIT 6.824 课程 lab1 的学习总结,记录我在学习过程中的收获和踩的坑. 我的实验环境是 windows 10,所以对lab的code 做了一些环境上的修改,如果你仅仅对code 感兴 ...

  6. MIT 6.824 : Spring 2015 lab3 训练笔记

    摘要: 源代码参见我的github:https://github.com/YaoZengzeng/MIT-6.824 Lab3: Paxos-based Key/Value Service Intro ...

  7. MIT 6.824 : Spring 2015 lab2 训练笔记

    源代码参见我的github:https://github.com/YaoZengzeng/MIT-6.824 Lab 2:Primary/Backup Key/Value Service Overvi ...

  8. MIT 6.824 Lab2D Raft之日志压缩

    书接上文Raft Part C | MIT 6.824 Lab2C Persistence. 实验准备 实验代码:git://g.csail.mit.edu/6.824-golabs-2021/src ...

  9. MIT 6.824 Lab2C Raft之持久化

    书接上文Raft Part B | MIT 6.824 Lab2B Log Replication. 实验准备 实验代码:git://g.csail.mit.edu/6.824-golabs-2021 ...

  10. MIT 6.824 Llab2B Raft之日志复制

    书接上文Raft Part A | MIT 6.824 Lab2A Leader Election. 实验准备 实验代码:git://g.csail.mit.edu/6.824-golabs-2021 ...

随机推荐

  1. SQL Server 事务与隔离级别实例讲解

    上班途中,你在一处ATM机前停了下来.正当你在敲入密码的时候,你的一位家人也正在镇上的另一处TAM机上输入密码.你打算从某个还有500元余额的账户上转出400元,而你的家人想从同一账户取走300元.倘 ...

  2. python deamon(守护)线程的作用

    stackoverflow 上的解释 某些线程执行后台任务,例如发送keepalive数据包,或执行定期垃圾收集,或任何.这些仅在主程序运行时有用,并且一旦其他非守护程序线程退出就可以将其关闭. 没有 ...

  3. 数据结构(三)串---KMP模式匹配算法之获取next数组

    (一)获取模式串T的next数组值 1.回顾 我们所知道的KMP算法next数组的作用 next[j]表示当前模式串T的j下标对目标串S的i值失配时,我们应该使用模式串的下标为next[j]接着去和目 ...

  4. JavaScript实现ajax发送表单数据

    知识点: 1.重置表单数据 2.获取表单数据(纯JavaScript) 3.设置表单数据(纯JavaScript) 4.ajax发送数据到客户端 (1)设置请求头,自己组合数据 (2)实例化表单对象, ...

  5. JeeSite 4.x 树形结构的表设计和用法

    有些同仁对于 JeeSite 4 中的树表设计不太了解,本应简单的方法就可实现,却写了很多复杂的语句和代码,所以有了这篇文章. 在 JeeSite 4 中的树表设计我还是相对满意的,这种设计比较容易理 ...

  6. Dubbo学习笔记10:Dubbo服务消费方启动流程源码分析

    同理我们看下服务消费端启动流程时序图: 在<Dubbo整体架构分析>一文中,我们提到服务消费方需要使用ReferenceConfig API来消费服务,具体是调用代码(1)get()方法来 ...

  7. SQL语句(十一)函数查询

    (十一)函数查询 1. 聚合函数 对一组值进行计算,得到一个返回值 SUM(), 求和 AVG(), 求平均 MIN(), 求最小 MAX(), 求最大 COUNT(), 计数,即个数 --例1 求所 ...

  8. CSS规范 - 最佳实践--(来自网易)

    最佳选择器写法(模块) /* 这是某个模块 */ .m-nav{}/* 模块容器 */ .m-nav li,.m-nav a{}/* 先共性 优化组合 */ .m-nav li{}/* 后个性 语义化 ...

  9. AngularJS入门基础——$provide.decorator 实例讲解

    <body ng-controller="OneController"> <script>  var Foobar = function() {      ...

  10. java学习第04天(语句、函数、数组)

    (3)循环结构 格式: for(初始化表达式,循环条件表达式,循环后的操作变大时){ 执行语句,循环体: } 注: a. for循环里面的连个表达式运行的顺序,初始化表达式只读一次,判断循环条件,为真 ...