2023 MIT 6.5840/6.824 分布式系统 Lab3: 容错键值存储实现笔记

前言

笔者基于先前实现的 Raft 库继续实现了 Lab3:容错键值存储。受课程组的保密要求,无法公开源码,故撰写本文以记录实现思路和需要考虑的问题,旨在助力正在完成实验的读者。

容错键值存储

搭建完 Raft 之后,我们可以基于 Raft 库开发一些分布式服务,Lab3 即是一个分布式容错的键值存储服务。这个服务需支持 Get(读取值)、Put(写入值)、Append(追加值) 等操作,运行多个节点,由 Raft 库选出的主节点(Leader)对外提供服务。若集群出现网络分区、少数节点故障,不应影响到服务的正常运行,若主节点故障,则及时切换为新的主节点对外提供服务。

如图所示,在 Lab3 中,同一个客户端不会并发地发送请求,它们只会在一个请求响应成功之后再发送下一个,若一次未成功,则重试至成功为止,因此客户端不需要加锁。

我们实现的这个键值存储服务必须得是线性一致的,即若操作A的响应先于操作B的请求完成,则服务必须按顺序先执行操作A,再执行操作B。例如客户端 1 分别执行了Put(x, 'a')Append(x, 'c')Get(x),按照线性一致的要求,Get(x) 读到的值一定为 ac。对于单节点服务来说,实现线性一致是件再简单不过的事情,只需对每个操作加锁按顺序执行即可,而对于分布式服务,就存在着若干需要考虑的问题。

如图所示,Peer1 在 Term1 追加了三个操作日志,并全部复制给了 Peer2,因此三个日志都在 Peer1 提交并执行,此时 Peer1 的状态为 {"x": "ac", "y": "b"}。不巧的是,Peer1 和另外两个节点之间出现了网络分区,于是 Peer2 在 Term2 成为新的主节点,它仅仅提交了前两个日志,状态为 {"x": "a", "y": "b"},此时客户端 1 执行 Get(x) 的结果为 a,不满足线性一致。

为了解决这个问题,最简单的方案是把 Get 操作也追加至日志中,这样可以保证所有节点的所有操作都是严格按照相同的顺序执行的。如图所示,客户端发起操作请求(可以是 Get,也可以是 PutAppend),等待 Raft 将操作日志复制到超过半数节点上,则执行并响应结果。在生产级别的实现上,一般会采用 Read Index 或者 Lease Read 的方式来提高读性能。

为了唯一标识某条操作日志,可以给日志加上客户端 ID 和操作顺序号,也方便把命令执行后的结果响应给客户端。

go 复制代码
type Op struct {
	Key         string
	Value       string
	Type        OpType
	ClientId    int64
	SequenceNum int64
}

主节点随时可能故障,集群随时可能切换新的主节点提供服务,为了能"记得"客户端的命令运行情况,各个节点应该给每个客户端维护一个会话(Session)。其中,IsOnline 表示这个客户端是否在线,即是否正在等待响应,若在线则通过 OpReplyCh 将执行结果发给它。LastAppliedSequenceNum 表示该客户端最后一个被执行的操作 sequenceNum,如果已经执行则可以立即响应成功(Get 操作返回 LastGetValue),如果还未执行则等待 OpReplyCh 发来的结果。

go 复制代码
type Session struct {
	OpReplyCh              chan OpReply
	LastAppliedSequenceNum int64
	IsOnline               bool
	LastGetSequenceNum     int64
	LastGetValue           string
}

type KVServer struct {
	mu      sync.Mutex
	me      int
	rf      *raft.Raft
	applyCh chan raft.ApplyMsg
	dead    int32 // set by Kill()

	persister    *raft.Persister
	maxraftstate int // snapshot if log grows this big

	storage           map[string]string
	sessions          map[int64]Session
	snapshotCond      *sync.Cond
	lastIncludedIndex int
}

服务运行时间久了,同样会存在日志过长的问题。需要本服务及时监控到触发高水位并压缩日志。我们可以用等待-唤醒机制实现一个 snapshoter,每当日志被提交就唤醒它,它检查一旦日志大小超过阈值的 90% 就保存快照、截断日志。

go 复制代码
func (kv *KVServer) snapshoter() {
    kv.snapshotCond.L.Lock()
    defer kv.snapshotCond.L.Unlock()
    for !kv.killed() {
        for kv.maxraftstate == -1 || kv.persister.RaftStateSize() < kv.maxraftstate/10*9 {
                kv.snapshotCond.Wait()
        }
        kv.mu.Lock()
        w := new(bytes.Buffer)
        e := labgob.NewEncoder(w)
        e.Encode(kv.storage)
        e.Encode(kv.sessions)
        kv.rf.Snapshot(kv.lastIncludedIndex, w.Bytes())
        kv.mu.Unlock()
    }
}

如图所示,在实现过程中,还需注意几个问题:

脑裂:由于网络分区,集群被分为若干个互不连通的小集群,此时集群中可能有多个不同 Term 的主节点,且它们都认为自己是合法的主节点,对外提供服务。客户端需要识别到这种情况并及时切换到最新 Term 的主节点上,由于先前 Term 主节点所在集群节点数为少数,因此它不能达成共识,客户端可以设定一超时时间,若在规定时间内未能响应,则切换节点。

go 复制代码
for opReply.SequenceNum != args.SequenceNum {
    select {
    case opReply = <-session.OpReplyCh:
            reply.Err = opReply.Err
    case <-time.After(100 * time.Millisecond):
            reply.Err = ErrTimeout
            opReply.SequenceNum = args.SequenceNum
    }
}

同一操作被追加至日志多次 :例如,客户端 1 向 Peer1 发送 Append(x, 'c') 请求,Peer 1 把它复制到所有节点上后就故障了,由于未能按时响应,客户端 1 又再次向 Peer2 发送 Append(x, 'c') 请求,此时若 Peer2 向 Peer3 复制日志,则两条 Append(x, 'c') 命令都会被提交。这两条命令具有相同的 (clientId, sequenceNum),在状态机执行命令时,需过滤掉重复的命令。

参考资料

1\] [MIT6.824-2021 Lab3 : KVRaft](https://link.juejin.cn?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F463144886 "https://zhuanlan.zhihu.com/p/463144886") \[2\] [一致性模型与共识算法](https://link.juejin.cn?target=https%3A%2F%2Ftanxinyu.work%2Fconsistency-and-consensus%2F%23etcd-%25E7%259A%2584-Raft "https://tanxinyu.work/consistency-and-consensus/#etcd-%E7%9A%84-Raft")

相关推荐
yourkin6665 分钟前
RocketMQ 分布式事务方案
分布式·rocketmq
技术小泽13 分钟前
Kafka架构以及组件讲解
后端·性能优化
Victor3561 小时前
Redis(28)Redis的持久化文件可以跨平台使用吗?
后端
Victor3561 小时前
Redis(29)如何手动触发Redis的RDB快照?
后端
快乐就是哈哈哈9 小时前
《一文带你搞懂ElasticSearch:从零到上手搜索引擎》
后端·elasticsearch
大鸡腿同学9 小时前
身弱:修炼之路
后端
bobz9659 小时前
cpu 调度 和 gpu 调度
后端
AirMan9 小时前
深入揭秘 ConcurrentHashMap:JDK7 到 JDK8 并发优化的演进之路
后端·面试
bobz9659 小时前
Linux CPU 调度模型
后端
计算机学姐9 小时前
基于SpringBoot的社团管理系统【2026最新】
java·vue.js·spring boot·后端·mysql·spring·mybatis