paper地址:nil.csail.mit.edu/6.824/2021/…
前言
建议在实现 Lab3 之前,结合 Lab2 的实现以及 Raft 论文进行实现 Lab3,即基于 Lab2 实现的 Raft 库构建容错键/值存储服务。
开始
整体架构
-
首先,结合 Lab3 的 paper 中给出的架构图进行理解本架构,下面会给出个人理解的通俗版本的图,来帮助理解。
-
其次,如果读过paper和Raft论文,应该会清楚一个要点:每个KVServer(raftServerId) 对应paper架构图里的 State Machine,也就是状态机,而每个 KVServer 对应 Lab2 实现的 Raft peer,并且 KVServer 之间是借助 Raft Service 来实现共识性,不直接交互的。
-
根据 paper 里对 Lab3 要求的描述,可以清楚 KVServer 通过ClientId可以知道 Client 的请求来自具体哪个客户端,同时保存每个客户端的请求信息和状态,所以每个客户端请求过来时,都赋予了一个刚生成的唯一ID,并且同一个请求对应唯一的序列号(ClientId),这两个 ID 就可以确定唯一性请求。这些在 client.go 和 server.go 就有具体代码和注释说明。
-
客户端的Id用 nrand() 随机生成唯一ID,经过测试最多有7个客户端ID且不会重复,每个 Client 维护一个 lastRequestId,通过mathrand(len(KVServer))生成,表示每一次请求的 Seq 序列号 clientId。
-
KVServer 通过维护了 lastRequestId,使得 Client 并发调用时,能通过最新的 RequestId, 得到最新的结果,保证应用程序的强一致性,这个强一致性通过定时器实现一段时间内(500ms)的分布式数据强一致性。
请求和响应流程:
请求响应流程,以Put/Get为例子:
- KVServer 收到 Client 的Request请求后,通过raft.Start() 提交Op给raft 库, 然后通过Chan机制,等待Raft 返回结果到 waitApplyCh, 也就是等待Raft应用日志到状态机后,才通过给chan缓冲区放入响应数据来响应给KVServer。
- 在Raft的所有peer 进行 apply当前请求的命令Op后, 每个Server会在单独的线程ApplyLoop 中等待不断到来的ApplyCh中的Op,直到 ApplyCh 缓冲区得到 Raft 的响应。
- Raft库 执行这个Op(GET全部执行,重复的PUT, APPEND不执行)
- Leader 等待Apply Loop 完成,之后根据Op中的信息将Raft库中的执行结果返回给Wait Channel , 中才有Wait Channel 在等结果
- 最后Leader将执行结果封装后返回给Client
具体请求流程
go kv.ReadRaftApplyCommandLoop()
这个Loop里,监听读取KVServer的applyCh- 通过在KVServer里的
applyCh chan raft.ApplyMsg
,借助管道chan的机制,实现写阻塞,也就是实现了在请求响应过程中,节点 peers监听一个chan 管道,管道接收到,才能触发接下来的操作。 - 只有Leader负责接受client的请求,才能触发以上条件,follower不主动触发,只能等待被leader同步
- 接受到请求后,Leader判定条件是否准确,正确则交由Raft#Start(Op)方法,接下来就阻塞等待方法的回调,等待结果返回。
- 根据需要设置 WaitChan 等待结果,同时设置Timeout,用来判定是否响应超时
- 之后Raft会将Leader的 Op 执行结果同步给所有Follower,ApplyEntry 同步到上层的每个KVServer
- 根据管道的返回时间是否超时,判定是否在强一致性的时间内能否得到响应
- 如果超时:
- 会先进行ifRequestDuplicate() 判断RequestId是否过时,依然是最新的RequestId 则从Leader的状态机中执行 Op 命令,返回本地日志执行命令后得到的结果
- 不超时,表示在一致性的有效时间内,只需要判断Raft响应的clientId和RequestId是否相同,即是否是最新的请求,是则表明KVServer的KVDB中有Op.key的最新数据,保证了数据的强一致性。
- 如果超时:
- 执行完Get或Append后,最后要删除管道的raftIndex对应的Op
请求阻塞问题
通过KVServer中的time.After
实现阻塞超时、重发。
因为无论是waitChan 还是labrpc中的Call方法,都没有“回调超时”的概念,会阻塞在哪里。
所以需要在Server端(或client端)实现计时器超时机制,避免无限阻塞。
重复请求问题
- Lab3A核心功能之一就是处理请求重复问题,即Duplicate Request ,实现保证一个重复的请求不会在同一个状态机上被执行两次,每个请求对应唯一的ClientID:RequestID
- 对于KVServer收到Client端的请求,无论是否重复,我们都提交给Raft作为它的log. 而KVServer 通过
kv.ifRequestDuplicate
方法 负责在接受apply log时判定这个log代指的Request Op是不是重复的,是重复的,我们就不在状态机上执行,直接返回OK即可,只需要在Put方法里考虑该问题就行,读操作不影响状态机的数据。
路由-负载均衡问题
Clinet . servers[] 的序号和KVServers.me 不是一 一对应的,而是随机shuffle过的。 但是KVServer.me 和 Raft.me 是对应的。 这就导致了Client 发送请求到一个不是LEADER的KVServer, 这个KVServer可以拿到raft.leader的序号并传回Client, 但Client 并不能通过client.servers[raft.leader]来找到真正的Leader, 还是要随机访问另一个。所以说我们收到ErrWrongLeader时候,只要再随机访问下一个KVServer即可
ck.RecentLeaderId = GetRandomServer(len(ck.Servers))
server := ck.RecentLeaderId
for{
// RPC请求KVServer的Get方法, 成功则返回leaderId
ok := ck.Servers[server].Call("KVServer.Get", &args, &reply)
// 换下一个Server,重试,直到 OK or Error
if !ok || reply.Err == ErrWrongLeader {
// LeaderId
server = (server + 1) % len(ck.Servers)
continue
...
}
快照
Snapshot 快照其实就是Server 维护的KeyValue数据库,可以看作是内存中一个map
对于Leader:
// 循环读取Raft已经应用的日志条目命令得到的回应
func (kv *KVServer) ReadRaftApplyCommandLoop() {
for message := range kv.applyCh {
if message.CommandValid {
kv.GetCommandFromRaft(message)
}
if message.SnapshotValid {
kv.GetSnapshotFromRaft(message)
}
}
}
-
在leader应用日志后,message.CommandValid 为true 说明RaftState[] 在递增,即有日志被应用并且命令是有效的。
-
那么Leader会执行响应的Get或Put操作完成后,根据日志条目阈值maxraftstate和当前日志条目数量RaftStateSize判断是否需要命令Raft进行Snapshot快照压缩操作。
-
如果需要,则调用MakeSnapshot方法,将自身的KVDB,RequestID等信息制作成snapshot, 并调用Raft库的Snapshot接口。
-
Leader 安装Snapshot , 这分为三部分,修剪log Entries [] , SnapShot 通过Persister进行持久化存储, 之后在Appendentries中将本次的SnapShot信息发送给落后的Follower
-
最后返回执行结果给WaitChannel
对于Follower :
· if message.SnapshotValid {
kv.GetSnapshotFromRaft(message)
}
- 1 . Leader执行 InstallSnapshot 的RPC方法后,Raft 层会获取 snapshot数据, 裁剪log, 通过ApplyCh上报给Server (此时SnapshotValid: true)
-
2 . Follower的Applyloop 收到请求,调用CondInstallSnapshot() 来询问是否可以安装snapshot
// 从Raft中获取快照日志 func (kv *KVServer) GetSnapshotFromRaft(message raft.ApplyMsg) { kv.mu.Lock() defer kv.mu.Unlock() if kv.rf.CondInstallSnapshot(message.SnapshotTerm, message.SnapshotIndex, message.Snapshot) { // 追加快照日志 kv.ReadSnapshotToInstall(message.Snapshot) kv.lastSSPointRaftLogIndex = message.SnapshotIndex } }
-
3 . CondInstallSnapshot() 判定snapshot安装条件,持久化snapshot, 并通知Server可以InstallSnapshot
核心代码
KVServer数据结构
type KVServer struct {
mu sync.Mutex
me int
// 每个KVServer对应一个Raft
rf *raft.Raft
applyCh chan raft.ApplyMsg
dead int32 // set by Kill()
// 快照日志中,最后日志条目的State
maxraftstate int // snapshot if log grows this big
// Your definitions here.
// 保存put的数据,key : value
kvDB map[string]string
// index(Raft pper) -> chan
waitApplyCh map[int]chan Op
// clientId : requestId
lastRequestId map[int64]int
// last Snapshot point & raftIndex
lastSSPointRaftLogIndex int
}
启动KVServer
// 启动KVServer
func StartKVServer(servers []*labrpc.ClientEnd, me int, persister *raft.Persister, maxraftstate int) *KVServer {
// call labgob.Register on structures you want
// Go's RPC library to marshall/unmarshall.
DPrintf("[InitKVServer---]Server %d", me)
// 注册rpc服务器
labgob.Register(Op{})
kv := new(KVServer)
kv.me = me
kv.maxraftstate = maxraftstate
// You may need initialization code here.
kv.applyCh = make(chan raft.ApplyMsg)
kv.rf = raft.Make(servers, me, persister, kv.applyCh)
// You may need initialization code here.
// kv初始化
kv.kvDB = make(map[string]string)
kv.waitApplyCh = make(map[int]chan Op)
kv.lastRequestId = make(map[int64]int)
// 快照
snapshot := persister.ReadSnapshot()
if len(snapshot) > 0 {
// 读取快照日志
kv.ReadSnapshotToInstall(snapshot)
}
// 循环读取Raft已经应用的日志条目命令
go kv.ReadRaftApplyCommandLoop()
return kv
}
Put和Get
// RPC方法
func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {
// Your code here.
if kv.killed() {
reply.Err = ErrWrongLeader
return
}
_, ifLeader := kv.rf.GetState()
// RaftServer必须是Leader
if !ifLeader {
reply.Err = ErrWrongLeader
return
}
op := Op{
Operation: "get",
Key: args.Key,
Value: "",
ClientId: args.ClientId,
RequestId: args.RequestId,
}
// 向Raft server 发送命令
raftIndex, _, _ := kv.rf.Start(op)
DPrintf("[GET StartToRaft]From Client %d (Request %d) To Server %d, key %v, raftIndex %d", args.ClientId, args.RequestId, kv.me, op.Key, raftIndex)
// waitForCh
kv.mu.Lock()
// chForRaftIndex为保存Op的chan,raftIndex为Raft Server的LastLogIndex+1
// 用于实现RPC调用Raft.Start时,保存RPC返回的Op,通过 Raft Server 的 lastLogIndex获取
// 通过raft的lastLogIndex,就能得到该日志条目保存的value,并保存到KVDB中
chForRaftIndex, exist := kv.waitApplyCh[raftIndex]
// Loop Apply ,技术上要求线性化
// 不存在该记录,表明调用还未返回结果,则继续等待调用返回
if !exist {
kv.waitApplyCh[raftIndex] = make(chan Op, 1)
chForRaftIndex = kv.waitApplyCh[raftIndex]
}
// RPC调用完成
kv.mu.Unlock()
// Timeout
select {
// 超过一致性要求的时间,则需要通过lastRequestId,从KVDB中获取结果
case <-time.After(time.Millisecond * CONSENSUS_TIMEOUT):
DPrintf("[GET TIMEOUT!!!]From Client %d (Request %d) To Server %d, key %v, raftIndex %d", args.ClientId, args.RequestId, kv.me, op.Key, raftIndex)
_, ifLeader := kv.rf.GetState()
// 该client的最新RequestId是否是newRequestId,不是,则返回最新RequestId
// 该步骤保证了client并发调用KVServer时,根据最新的RequestId,得到最新的结果
if kv.ifRequestDuplicate(op.ClientId, op.RequestId) && ifLeader {
// 根据命令获取该client最新RequestId得到并保存在KVDB的value
value, exist := kv.ExecuteGetOpOnKVDB(op)
if exist {
reply.Err = OK
reply.Value = value
} else {
reply.Err = ErrNoKey
reply.Value = ""
}
} else {
reply.Err = ErrWrongLeader
}
// 在一致性的有效时间内:
case raftCommitOp := <-chForRaftIndex:
DPrintf("[WaitChanGetRaftApplyMessage<--]Server %d , get Command <-- Index:%d , ClientId %d, RequestId %d, Opreation %v, Key :%v, Value :%v", kv.me, raftIndex, op.ClientId, op.RequestId, op.Operation, op.Key, op.Value)
// 该已提交到Raft的RPC请求,是本次的Op命令
if raftCommitOp.ClientId == op.ClientId &&
raftCommitOp.RequestId == op.RequestId {
// 则从KVServer的Map直接获取value
value, exist := kv.ExecuteGetOpOnKVDB(op)
if exist {
reply.Err = OK
reply.Value = value
} else {
reply.Err = ErrNoKey
reply.Value = ""
}
} else {
reply.Err = ErrWrongLeader
}
}
kv.mu.Lock()
// Get结束后,删除chan map中raftIndex对应的Op
delete(kv.waitApplyCh, raftIndex)
kv.mu.Unlock()
return
}
Put方法
// RPC方法
func (kv *KVServer) PutAppend(args *PutAppendArgs, reply *PutAppendReply) {
// Your code here.
if kv.killed() {
reply.Err = ErrWrongLeader
return
}
_, ifLeader := kv.rf.GetState()
// RaftServer必须是Leader
if !ifLeader {
reply.Err = ErrWrongLeader
return
}
op := Op{
Operation: args.Op,
Key: args.Key,
Value: args.Value,
ClientId: args.ClientId,
RequestId: args.RequestId,
}
// 向Raft server 发送命令
raftIndex, _, _ := kv.rf.Start(op)
DPrintf("[PUTAPPEND StartToRaft]From Client %d (Request %d) To Server %d, key %v, raftIndex %d", args.ClientId, args.RequestId, kv.me, op.Key, raftIndex)
// waitForCh
kv.mu.Lock()
// chForRaftIndex为保存Op的chan,raftIndex为Raft Server的LastLogIndex+1
// 用于实现RPC调用Raft.Start时,保存RPC返回的Op,通过 Raft Server 的 lastLogIndex获取
// 通过raft的lastLogIndex,就能得到该日志条目保存的value,并保存到KVDB中
chForRaftIndex, exist := kv.waitApplyCh[raftIndex]
// Loop Apply ,技术上要求线性化
// 不存在该记录,表明调用还未返回结果,则继续等待调用返回
if !exist {
kv.waitApplyCh[raftIndex] = make(chan Op, 1)
chForRaftIndex = kv.waitApplyCh[raftIndex]
}
// RPC调用完成
kv.mu.Unlock()
// Timeout
select {
// 超过一致性要求的时间,则需要通过lastRequestId,从KVDB中获取结果
case <-time.After(time.Millisecond * CONSENSUS_TIMEOUT):
DPrintf("[TIMEOUT PUTAPPEND !!!!]Server %d , get Command <-- Index:%d , ClientId %d, RequestId %d, Opreation %v, Key :%v, Value :%v", kv.me, raftIndex, op.ClientId, op.RequestId, op.Operation, op.Key, op.Value)
// 该client的最新RequestId是否是newRequestId,不是,则返回最新RequestId
// 该步骤保证了client并发调用KVServer时,根据最新的RequestId,得到最新的结果
if kv.ifRequestDuplicate(op.ClientId, op.RequestId) {
reply.Err = OK
} else {
reply.Err = ErrWrongLeader
}
// 在一致性的有效时间内:
case raftCommitOp := <-chForRaftIndex:
DPrintf("[WaitChanGetRaftApplyMessage<--]Server %d , get Command <-- Index:%d , ClientId %d, RequestId %d, Opreation %v, Key :%v, Value :%v", kv.me, raftIndex, op.ClientId, op.RequestId, op.Operation, op.Key, op.Value)
// 该已提交到Raft的RPC请求,是本次的Op命令
if raftCommitOp.ClientId == op.ClientId &&
raftCommitOp.RequestId == op.RequestId {
reply.Err = OK
} else {
reply.Err = ErrWrongLeader
}
}
kv.mu.Lock()
delete(kv.waitApplyCh, raftIndex)
kv.mu.Unlock()
return
}