枯木逢春不在茂,
年少且惜镜边人。
写在前面
今天的考试还算顺利,不过今天的我,心里也是莫名的不舒服,不舒服?不舒服就来写博客吧,3A要求基于Raft实现分布式kv server
实现过程
要求:
- 单个客户端,是串行的
- 多个客户端可以并发的发起请求
- 一旦有客户端读取到新值,那么其他的客户端也都应该读到该值
由于自己的LAB3的代码参考了这位老哥的代码,本文只是个人复习+学习所用,大家可直接跳转去该链接学习。
type Clerk struct {
mu sync.Mutex
servers []*labrpc.ClientEnd
// You will have to modify this struct.
// 只需要确保clientId每次重启不重复,那么clientId+seqId就是安全的
clientId int64 // 客户端唯一标识
seqId int64 // 该客户端单调递增的请求id
leaderId int
}
seqId体现的是写请求的幂等性,如果一个写请求实际上已经写了,但是在返回的时候挂掉了,使用这个单调递增的请求Id可以避免重复写入问题。
func MakeClerk(servers []*labrpc.ClientEnd) *Clerk {
ck := new(Clerk)
ck.servers = servers
// You'll have to add code here.
ck.clientId = nrand()
return ck
}
这个函数是生成客户端用的
// Put or Append
type PutAppendArgs struct {
Key string
Value string
Op string // "Put" or "Append"
// You'll have to add definitions here.
// Field names must start with capital letters,
// otherwise RPC will break.
ClientId int64
SeqId int64
}
type PutAppendReply struct {
Err Err
}
关于PutAppend的PRC参数,需要带上ClientId int64,SeqId int64这两个参数。
func (ck *Clerk) PutAppend(key string, value string, op string) {
// You will have to modify this function.
args := PutAppendArgs{
Key: key,
Value: value,
Op: op,
ClientId: ck.clientId,
SeqId: atomic.AddInt64(&ck.seqId, 1),
}
DPrintf("Client[%d] PutAppend, Key=%s Value=%s", ck.clientId, key, value)
leaderId := ck.currentLeader()
for {
reply := PutAppendReply{}
if ck.servers[leaderId].Call("KVServer.PutAppend", &args, &reply) {
if reply.Err == OK { // 成功
break
}
}
leaderId = ck.changeLeader()
time.Sleep(1 * time.Millisecond)
}
}
这个函数就是RPC发起函数,如果返回错误,则说明在操作过程中leader发生了变化,需要切换leader重新发送。
func (ck *Clerk) currentLeader() (leaderId int) {
ck.mu.Lock()
defer ck.mu.Unlock()
leaderId = ck.leaderId
return
}
func (ck *Clerk) changeLeader() (leaderId int) {
ck.mu.Lock()
defer ck.mu.Unlock()
ck.leaderId = (ck.leaderId + 1) % len(ck.servers)
return ck.leaderId
}
type GetArgs struct {
Key string
// You'll have to add definitions here.
ClientId int64
SeqId int64
}
type GetReply struct {
Err Err
Value string
}
请求与应答。
Get的RPC调用参数和响应
func (ck *Clerk) Get(key string) string {
// You will have to modify this function.
args := GetArgs{
Key: key,
ClientId: ck.clientId,
SeqId: atomic.AddInt64(&ck.seqId, 1),
}
DPrintf("Client[%d] Get starts, Key=%s ", ck.clientId, key)
leaderId := ck.currentLeader()
for {
reply := GetReply{}
if ck.servers[leaderId].Call("KVServer.Get", &args, &reply) {
if reply.Err == OK { // 命中
return reply.Value
} else if reply.Err == ErrNoKey { // 不存在
return "";
}
}
leaderId = ck.changeLeader()
time.Sleep(1 * time.Millisecond)
}
}
Get的处理和putappend大致一样,需要注意的就是get的错误可能有两只,一种的是leader变化导致,另一种是发送的key值不存在导致的。
const (
OK = "OK"
ErrNoKey = "ErrNoKey"
ErrWrongLeader = "ErrWrongLeader"
)
type Err string
// Put or Append
type PutAppendArgs struct {
Key string
Value string
Op string // "Put" or "Append"
// You'll have to add definitions here.
// Field names must start with capital letters,
// otherwise RPC will break.
ClientId int64
SeqId int64
}
type PutAppendReply struct {
Err Err
}
type GetArgs struct {
Key string
// You'll have to add definitions here.
ClientId int64
SeqId int64
}
type GetReply struct {
Err Err
Value string
}
这里给出lab3需用用到的结构体,定义在common.go中
const (
OP_TYPE_PUT = "Put"
OP_TYPE_APPEND = "Append"
OP_TYPE_GET = "Get"
)
type Op struct {
// Your definitions here.
// Field names must start with capital letters,
// otherwise RPC will break.
Index int
Term int
Type string
Key string
Value string
SeqId int64
ClientId int64
}
type OpContext struct {
op *Op
committed chan byte
wrongLeader bool
ignore bool
keyExist bool
value string
}
type KVServer struct {
mu sync.Mutex
me int
rf *raft.Raft
applyCh chan raft.ApplyMsg
dead int32 // set by Kill()
maxraftstate int // snapshot if log grows this big
kvStore map[string]string // kv存储
reqMap map[int]*OpContext // log index -> 请求上下文
seqMap map[int64]int64 // 客户端id -> 客户端seq
// Your definitions here.
lastAppliedIndex int
}
这里给出服务器端的第一的op操作和kvsever结构体,reqMap存储正在进行中的RPC调用,seqMap记录每个clientId已提交的最大请求ID以便做写入幂等性判定。
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.
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.kvStore = make(map[string]string)
kv.reqMap = make(map[int]*OpContext)
kv.seqMap = make(map[int64]int64)
go kv.applyLoop()
return kv
}
这个函数负责开启服务,这里调用raft的MAKE函数,初始化raft集群,并且开启kvsever
func Make(peers []*labrpc.ClientEnd, me int,
persister *Persister, applyCh chan ApplyMsg) *Raft {
rf := &Raft{}
rf.peers = peers
rf.persister = persister
rf.me = me
// Your initialization code here (2A, 2B, 2C).
rf.role = ROLE_FOLLOWER
rf.leaderId = -1
rf.votedFor = -1
rf.lastActiveTime = time.Now()
rf.lastIncludedIndex = 0
rf.lastIncludedTerm = 0
rf.applyCh = applyCh
// rf.nextIndex = make([]int, len(rf.peers))
// rf.matchIndex = make([]int, len(rf.peers))
// initialize from state persisted before a crash
rf.readPersist(persister.ReadRaftState())
//rf.installSnapshotToApplication()
DPrintf("RaftNode[%d] Make again", rf.me)
// start ticker goroutine to start elections
go rf.electionLoop()
go rf.appendEntriesLoop()
go rf.applyLogLoop()
//go rf.ticker()
DPrintf("Raftnode[%d]启动", me)
return rf
}
func (kv *KVServer) PutAppend(args *PutAppendArgs, reply *PutAppendReply) {
// Your code here.
reply.Err = OK
op := &Op{
Type: args.Op,
Key: args.Key,
Value: args.Value,
ClientId: args.ClientId,
SeqId: args.SeqId,
}
// 写入raft层
var isLeader bool
op.Index, op.Term, isLeader = kv.rf.Start(op)
if !isLeader {
reply.Err = ErrWrongLeader
return
}
opCtx := newOpContext(op)
func() {
kv.mu.Lock()
defer kv.mu.Unlock()
// 保存RPC上下文,等待提交回调,可能会因为Leader变更覆盖同样Index,不过前一个RPC会超时退出并令客户端重试
kv.reqMap[op.Index] = opCtx
}()
// RPC结束前清理上下文
defer func() {
kv.mu.Lock()
defer kv.mu.Unlock()
if one, ok := kv.reqMap[op.Index]; ok {
if one == opCtx {
delete(kv.reqMap, op.Index)
}
}
}()
timer := time.NewTimer(2000 * time.Millisecond)
defer timer.Stop()
select {
case <- opCtx.committed: // 如果提交了
if opCtx.wrongLeader { // 同样index位置的term不一样了, 说明leader变了,需要client向新leader重新写入
reply.Err = ErrWrongLeader
} else if opCtx.ignored {
// 说明req id过期了,该请求被忽略,对MIT这个lab来说只需要告知客户端OK跳过即可
}
case <- timer.C: // 如果2秒都没提交成功,让client重试
reply.Err = ErrWrongLeader
}
}
这里面调用了raft中的raft中的start函数,这里对比看代码,putappend逻辑就是,将op写入raft,等待applyloop发送提交型号,然后返回rpc调用(这里涉及ignore 便是幂等性的体现)
func (rf *Raft) Start(command interface{}) (int, int, bool) {
index := -1
term := -1
isLeader := true
// Your code here (2B).
rf.mu.Lock()
defer rf.mu.Unlock()
if rf.role != ROLE_LEADER {
return -1, -1, false
}
logEntry := LogEntry{
Command: command,
Term: rf.currentTerm,
}
rf.log = append(rf.log, logEntry)
index = rf.lastIndex()
term = rf.currentTerm
rf.persist()
DPrintf("RaftNode[%d] Add Command, logIndex[%d] currentTerm[%d]", rf.me, index, term)
return index, term, isLeader
}
明显看到,这里服务器端将操作写入日志
func newOpContext(op *Op) (opCtx *OpContext) {
opCtx = &OpContext{
op: op,
committed: make(chan byte),
}
return
}
func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {
// Your code here.
reply.Err = OK
op := &Op{
Type: OP_TYPE_GET,
Key: args.Key,
ClientId: args.ClientId,
SeqId: args.SeqId,
}
// 写入raft层
var isLeader bool
op.Index, op.Term, isLeader = kv.rf.Start(op)
if !isLeader {
reply.Err = ErrWrongLeader
return
}
opCtx := newOpContext(op)
func() {
kv.mu.Lock()
defer kv.mu.Unlock()
// 保存RPC上下文,等待提交回调,可能会因为Leader变更覆盖同样Index,不过前一个RPC会超时退出并令客户端重试
kv.reqMap[op.Index] = opCtx
}()
// RPC结束前清理上下文
defer func() {
kv.mu.Lock()
defer kv.mu.Unlock()
if one, ok := kv.reqMap[op.Index]; ok {
if one == opCtx {
delete(kv.reqMap, op.Index)
}
}
}()
timer := time.NewTimer(2000 * time.Millisecond)
defer timer.Stop()
select {
case <-opCtx.committed: // 如果提交了
if opCtx.wrongLeader { // 同样index位置的term不一样了, 说明leader变了,需要client向新leader重新写入
reply.Err = ErrWrongLeader
} else if !opCtx.keyExist { // key不存在
reply.Err = ErrNoKey
} else {
reply.Value = opCtx.value // 返回值
}
case <- timer.C: // 如果2秒都没提交成功,让client重试
reply.Err = ErrWrongLeader
}
}
Get请求和putappend大同小异,只不过错误处理状态有些许不同,大致逻辑思路和putappend一模一样。
func (kv *KVServer) applyLoop() {
for !kv.killed() {
select {
case msg := <- kv.applyCh:
cmd := msg.Command
index := msg.CommandIndex
func() {
kv.mu.Lock()
defer kv.mu.Unlock()
// 操作日志
op := cmd.(*Op)
opCtx, existOp := kv.reqMap[index]
prevSeq, existSeq := kv.seqMap[op.ClientId]
kv.seqMap[op.ClientId] = op.SeqId
if existOp { // 存在等待结果的RPC, 那么判断状态是否与写入时一致
if opCtx.op.Term != op.Term {
opCtx.wrongLeader = true
}
}
// 只处理ID单调递增的客户端写请求
if op.Type == OP_TYPE_PUT || op.Type == OP_TYPE_APPEND {
if !existSeq || op.SeqId > prevSeq { // 如果是递增的请求ID,那么接受它的变更
if op.Type == OP_TYPE_PUT { // put操作
kv.kvStore[op.Key] = op.Value
} else if op.Type == OP_TYPE_APPEND { // put-append操作
if val, exist := kv.kvStore[op.Key]; exist {
kv.kvStore[op.Key] = val + op.Value
} else {
kv.kvStore[op.Key] = op.Value
}
}
} else if existOp {
opCtx.ignored = true
}
} else { // OP_TYPE_GET
if existOp {
opCtx.value, opCtx.keyExist = kv.kvStore[op.Key]
}
}
DPrintf("RaftNode[%d] applyLoop, kvStore[%v]", kv.me, kv.kvStore)
// 唤醒挂起的RPC
if existOp {
close(opCtx.committed)
}
}()
}
}
}
applyloop的逻辑就是监听 kv.applyCh管道,一旦有体现的日志。这里seqiD处理是非常精彩的,客户端原子的增加seqid,然后这里巧妙的比较递增性,服务器端不会理会过期的请求,最后唤醒阻塞的commited管道 也就是putappend 和 get handle函数。让他们进行判断 然后pRC返回。精彩!!!
写在后面
3A整体难度不是很大,但是在test中一直有个条目超时,实在不知哪里问题暂时放下这个问题
- 线性一致性要求,客户端必须串行发起OP,并行发起OP服务端无法保证前1个OP生效,实际工程里我认为应该客户端对请求排队编号seqId,串行提交给KV server。
- 只要保证每个client的ID能够唯一,那么seqId无需持久化,保证clientId唯一还是比较简单的,比如用:ip+pid+timestamp就可以。
- 写入幂等性通过比较clientId最后一次提交日志的seqId,来确定是否可以执行当前OP。
- 读取的一致性则是通过向raft写入一条read log实现的,当read log被提交时再把此时此刻kvStore的数据返回给RPC,那么RPC看到的数据一定是符合线性一致性的,即后续的读操作一定可以继续读到这个值。
- RPC服务端要做超时处理,因为很有可能leader写入本地log后发生了选主,那么新leader会截断掉老leader写入的log,导致对应index位置持续得不到提交,所以要通过超时让客户端重新写入log来达成提交,得到最终结果。