Mit 6.824 Lab3 KV Raft实现

870 阅读9分钟

paper地址:nil.csail.mit.edu/6.824/2021/…

前言

建议在实现 Lab3 之前,结合 Lab2 的实现以及 Raft 论文进行实现 Lab3,即基于 Lab2 实现的 Raft 库构建容错键/值存储服务。

开始

整体架构

  1. 首先,结合 Lab3 的 paper 中给出的架构图进行理解本架构,下面会给出个人理解的通俗版本的图,来帮助理解。

  2. 其次,如果读过paper和Raft论文,应该会清楚一个要点:每个KVServer(raftServerId) 对应paper架构图里的 State Machine,也就是状态机,而每个 KVServer 对应 Lab2 实现的 Raft peer,并且 KVServer 之间是借助 Raft Service 来实现共识性,不直接交互的。

  3. 根据 paper 里对 Lab3 要求的描述,可以清楚 KVServer 通过ClientId可以知道 Client 的请求来自具体哪个客户端,同时保存每个客户端的请求信息和状态,所以每个客户端请求过来时,都赋予了一个刚生成的唯一ID,并且同一个请求对应唯一的序列号(ClientId),这两个 ID 就可以确定唯一性请求。这些在 client.go 和 server.go 就有具体代码和注释说明。

  4. 客户端的Id用 nrand() 随机生成唯一ID,经过测试最多有7个客户端ID且不会重复,每个 Client 维护一个 lastRequestId,通过mathrand(len(KVServer))生成,表示每一次请求的 Seq 序列号 clientId。

  5. KVServer 通过维护了 lastRequestId,使得 Client 并发调用时,能通过最新的 RequestId, 得到最新的结果,保证应用程序的强一致性,这个强一致性通过定时器实现一段时间内(500ms)的分布式数据强一致性。

请求和响应流程:

image-20220225232641612

请求响应流程,以Put/Get为例子:

  1. KVServer 收到 Client 的Request请求后,通过raft.Start() 提交Op给raft 库, 然后通过Chan机制,等待Raft 返回结果到 waitApplyCh, 也就是等待Raft应用日志到状态机后,才通过给chan缓冲区放入响应数据来响应给KVServer。
  2. 在Raft的所有peer 进行 apply当前请求的命令Op后, 每个Server会在单独的线程ApplyLoop 中等待不断到来的ApplyCh中的Op,直到 ApplyCh 缓冲区得到 Raft 的响应。
  3. Raft库 执行这个Op(GET全部执行,重复的PUT, APPEND不执行)
  4. Leader 等待Apply Loop 完成,之后根据Op中的信息将Raft库中的执行结果返回给Wait Channel , 中才有Wait Channel 在等结果
  5. 最后Leader将执行结果封装后返回给Client

具体请求流程

image-20220305161613271

  1. go kv.ReadRaftApplyCommandLoop()这个Loop里,监听读取KVServer的applyCh
  2. 通过在KVServer里的 applyCh chan raft.ApplyMsg ,借助管道chan的机制,实现写阻塞,也就是实现了在请求响应过程中,节点 peers监听一个chan 管道,管道接收到,才能触发接下来的操作。
  3. 只有Leader负责接受client的请求,才能触发以上条件,follower不主动触发,只能等待被leader同步
  4. 接受到请求后,Leader判定条件是否准确,正确则交由Raft#Start(Op)方法,接下来就阻塞等待方法的回调,等待结果返回。
  5. 根据需要设置 WaitChan 等待结果,同时设置Timeout,用来判定是否响应超时
  6. 之后Raft会将Leader的 Op 执行结果同步给所有Follower,ApplyEntry 同步到上层的每个KVServer
  7. 根据管道的返回时间是否超时,判定是否在强一致性的时间内能否得到响应
    1. 如果超时:
      1. 会先进行ifRequestDuplicate() 判断RequestId是否过时,依然是最新的RequestId 则从Leader的状态机中执行 Op 命令,返回本地日志执行命令后得到的结果
    2. 不超时,表示在一致性的有效时间内,只需要判断Raft响应的clientId和RequestId是否相同,即是否是最新的请求,是则表明KVServer的KVDB中有Op.key的最新数据,保证了数据的强一致性。
  8. 执行完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

image-20220305171907264

对于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
}