MIT6.824(lab3A-kv存储)

枯木逢春不在茂,
年少且惜镜边人。

写在前面

今天的考试还算顺利,不过今天的我,心里也是莫名的不舒服,不舒服?不舒服就来写博客吧,3A要求基于Raft实现分布式kv server

实现过程

要求:

  1. 单个客户端,是串行的
  2. 多个客户端可以并发的发起请求
  3. 一旦有客户端读取到新值,那么其他的客户端也都应该读到该值

由于自己的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中一直有个条目超时,实在不知哪里问题暂时放下这个问题

  1. 线性一致性要求,客户端必须串行发起OP,并行发起OP服务端无法保证前1个OP生效,实际工程里我认为应该客户端对请求排队编号seqId,串行提交给KV server。
  2. 只要保证每个client的ID能够唯一,那么seqId无需持久化,保证clientId唯一还是比较简单的,比如用:ip+pid+timestamp就可以。
  3. 写入幂等性通过比较clientId最后一次提交日志的seqId,来确定是否可以执行当前OP。
  4. 读取的一致性则是通过向raft写入一条read log实现的,当read log被提交时再把此时此刻kvStore的数据返回给RPC,那么RPC看到的数据一定是符合线性一致性的,即后续的读操作一定可以继续读到这个值。
  5. RPC服务端要做超时处理,因为很有可能leader写入本地log后发生了选主,那么新leader会截断掉老leader写入的log,导致对应index位置持续得不到提交,所以要通过超时让客户端重新写入log来达成提交,得到最终结果。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值