【MIT6.824】lab3 Fault-tolerant Key/Value Service 实现笔记

news2024/11/23 2:56:36

引言

lab3A的实验要求如下:

Your first task is to implement a solution that works when there are no dropped messages, and no failed servers.

You’ll need to add RPC-sending code to the Clerk Put/Append/Get methods in client.go, and implement PutAppend() and Get() RPC handlers in server.go. These handlers should enter an Op in the Raft log using Start(); you should fill in the Op struct definition in server.go so that it describes a Put/Append/Get operation. Each server should execute Op commands as Raft commits them, i.e. as they appear on the applyCh. An RPC handler should notice when Raft commits its Op, and then reply to the RPC.

You have completed this task when you reliably pass the first test in the test suite: “One client”.

Add code to handle failures, and to cope with duplicate Clerk requests, including situations where the Clerk sends a request to a kvserver leader in one term, times out waiting for a reply, and re-sends the request to a new leader in another term. The request should execute just once. These notes include guidance on duplicate detection. Your code should pass the go test -run 3A tests.

lab3B的实验要求如下:

Modify your kvserver so that it detects when the persisted Raft state grows too large, and then hands a snapshot to Raft. When a kvserver server restarts, it should read the snapshot from persister and restore its state from the snapshot.

总体而言,我们需要在lab2所实现的raft系统上构建一个简单的key-value存储系统,这个系统需要支持客户端的Put/Append/Get操作,同时需要支持Raft的持久化和快照功能。本系统的要求是线性一致的,即每个动作都能被当做是在一个唯一的时刻进行原子执行的,具体一致性相关的内容,可查看之前的文章:分布式系统中的线性一致性。
代码可以在https://github.com/slipegg/MIT6.824中得到。所有代码均通过了1千次的测试。

lab3A 实现

lab3A不涉及到Raft的快照功能,主要是要完成整个系统功能的构建。在实验时测试3A时,测试代码将会不断调用客户端的Put/Append/Get操作,然后检查是否所有的操作都被正确执行。

首先通过一个map来存储key-value,如下中的KVMachine所示:

type KVMachine struct {
	KV map[string]string
}

func (kv *KVMachine) Get(key string) (string, Err) {
	value, ok := kv.KV[key]
	if !ok {
		return "", ErrNoKey
	}
	return value, OK
}

func (kv *KVMachine) Put(key string, value string) Err {
	kv.KV[key] = value
	return OK
}

func (kv *KVMachine) Append(key string, value string) Err {
	oldValue, ok := kv.KV[key]
	if !ok {
		kv.KV[key] = value
		return OK
	}
	kv.KV[key] = oldValue + value
	return OK
}

func newKVMachine() *KVMachine {
	return &KVMachine{make(map[string]string)}
}

然后是Client端的实现,首先Client在初始化时会随机生成一个数字当做自己的id,同时它也专门维护每个请求的唯一id。Client的Put/Append/Get操作都是通过RPC调用Server端的Put/Append/Get操作来实现的,如果Server端返回了错误,告诉当前Server不是leader,那么Client就会重新发送请求到下一个Server去,直到找到leader并执行请求成功了为止。Client端的PutAppend/Get操作的实现如下,Get也是类似,就是错误处理稍微不同,不再赘述:

func (ck *Clerk) PutAppend(key string, value string, op string) {
	DPrintf("{Clinetn-%d} try to %s {'%v': '%v'}\n", ck.clientId, op, key, value)
	args := PutAppendArgs{Key: key, Value: value, Op: op, ClientId: ck.clientId, RequestId: ck.requestId}
	for {
		var reply PutAppendReply
		if ck.servers[ck.leaderId].Call("KVServer.PutAppend", &args, &reply) && reply.Err == OK {
			DPrintf("{Clinetn-%d} %s {'%v': '%v'} success\n", ck.clientId, op, key, value)
			ck.requestId++
			break
		} else {
			ck.leaderId = (ck.leaderId + 1) % int64(len(ck.servers))
			time.Sleep(100 * time.Millisecond)
		}
	}
}

每个Server端都会维护一个KVMachine,并且也连接到一个专门的raft节点,它的主要作用就是将客户端的请求转化为raft节点的日志,然后等待raft节点将日志提交后接收到raft节点的信息,将日志应用到自己的KVMachine中,然后返回给客户端。

将客户端请求转化为日志传递给raft部分的代码如下,Get请求也是类似的。注意这里对于重复执行过的Put、Append会直接进行返回,因为运行结果只会是OK,所以直接返回OK即可,而Get请求不需要判断是否重复执行,因为Get请求需要获取的实最新的数据,来一次就执行一次即可。

func (kv *KVServer) PutAppend(args *PutAppendArgs, reply *PutAppendReply) {
	// Your code here.
	defer DPrintf("{KVServer-%d} finishes %s {%s: %s}, the reply is %v\n", kv.me, args.Op, args.Key, args.Value, reply)
	kv.mu.RLock()
	if kv.isDuplicate(args.ClientId, args.RequestId) {
		kv.mu.RUnlock()
		reply.Err = OK
		return
	}
	kv.mu.RUnlock()

	logId, _, isLeader := kv.rf.Start(Op{PutAppendArgs: args})

	if !isLeader {
		reply.Err = ErrWrongLeader
		return
	}

	DPrintf("{KVServer-%d} try to %s {%s: %s} with logId: %d\n", kv.me, args.Op, args.Key, args.Value, logId)

	kv.mu.Lock()
	ch_putAppend := kv.getNotifyCh_PutAppend(logId)
	kv.mu.Unlock()

	select {
	case result := <-ch_putAppend:
		reply.Err = result.Err
	case <-time.After(MaxWaitTime):
		reply.Err = ErrTimeout
	}

	go func() {
		kv.mu.Lock()
		delete(kv.notifyChs_PutAppend, logId)
		kv.mu.Unlock()
	}()
}

当raft节点将日志分发给了大部分的节点后,就可以将日志提交,然后提醒Server端将日志应用到自己的KVMachine中。代码如下所示。注意对于Get请求,需要判断这时候节点是不是leader,Term是否还相同,以防止由于applyCh传递时间过长,这时候节点已经不是leader,没有最新的数据了。对于Put、Append操作需要判断是否已经是重复执行过的操作,如果是,直接标记为OK即可,不需要再次执行,同样也需要判断当前还是不是leader,如果是才有权限返回给客户端执行结果。

func (kv *KVServer) applier() {
	for !kv.killed() {
		select {
		case msg := <-kv.applyCh:
			if msg.CommandValid {
				kv.mu.Lock()
				if msg.CommandIndex <= kv.lastApplied {
					DPrintf("{KVServer-%d} reveives applied log{%v}", kv.me, msg)
					kv.mu.Unlock()
					continue
				}
				kv.lastApplied = msg.CommandIndex

				op := msg.Command.(Op)
				if op.GetArgs != nil {
					DPrintf("{KVServer-%d} apply get %v.", kv.me, op.GetArgs.Key)
					value, err := kv.kvMachine.Get(op.GetArgs.Key)
					reply := GetReply{Err: err, Value: value}

					if currentTerm, isLeader := kv.rf.GetState(); isLeader && currentTerm == msg.CommandTerm {
						if ch, ok := kv.notifyChs_Get[msg.CommandIndex]; ok {
							ch <- reply
						}
					}
				} else if op.PutAppendArgs != nil {
					var reply PutAppendReply
					if kv.isDuplicate(op.PutAppendArgs.ClientId, op.PutAppendArgs.RequestId) {
						DPrintf("{KVServer-%d} receives duplicated request{%v}\n", kv.me, msg)
						reply.Err = OK
					} else {
						DPrintf("{KVServer-%d} apply %s {%s: %s}.\n", kv.me, op.PutAppendArgs.Op, op.PutAppendArgs.Key, op.PutAppendArgs.Value)
						if op.PutAppendArgs.Op == "Put" {
							reply.Err = kv.kvMachine.Put(op.PutAppendArgs.Key, op.PutAppendArgs.Value)
						} else if op.PutAppendArgs.Op == "Append" {
							reply.Err = kv.kvMachine.Append(op.PutAppendArgs.Key, op.PutAppendArgs.Value)
						}
						kv.lastPutAppendId[op.PutAppendArgs.ClientId] = op.PutAppendArgs.RequestId
					}

					if _, isLeader := kv.rf.GetState(); isLeader {
						if ch, ok := kv.notifyChs_PutAppend[msg.CommandIndex]; ok {
							ch <- reply
						}
					}
				} else {
					DPrintf("{KVServer-%d} receives unknown command{%v}", kv.me, msg)
				}

				if kv.isNeedSnapshot() {
					DPrintf("{KVServer-%d} needs snapshot\n", kv.me)
					kv.snapshot(msg.CommandIndex)
				}
				kv.mu.Unlock()
			} 
        }
    }
}

lab3B 实现

这里主要需要实现Server的持久化和快照功能,每个Server有一个自己的persister,其结构如下:

type Persister struct {
	mu        sync.Mutex
	raftstate []byte
	snapshot  []byte
}

其中raftstate部分是raft节点存储自身持久化状态用的,而snapshot节点是用来给Server存储自身状态用的,包括了Server的KVMachine状态以及lastPutAppendId。在Server启动时,会从persister中读取raftstate和snapshot,然后根据raftstate来初始化raft节点,根据snapshot来初始化KVMachine和lastPutAppendId。代码如下所示:

func (kv *KVServer) reloadBySnapshot(snapshot []byte) {
	if snapshot == nil || len(snapshot) < 1 {
		return
	}

	var kvMachine KVMachine
	var lastPutAppendId map[int64]int64

	r := bytes.NewBuffer(snapshot)
	d := labgob.NewDecoder(r)
	if d.Decode(&kvMachine) != nil ||
		d.Decode(&lastPutAppendId) != nil {
		DPrintf("{KVServer-%d} reloadBySnapshot failed\n", kv.me)
	}

	DPrintf("{KVServer-%d} reloadBySnapshot succeeded\n", kv.me)
	kv.lastPutAppendId = lastPutAppendId
	kv.kvMachine = kvMachine
}

当Server在apply节点时,按照要求,如果raft的日志信息过大,就触发快照功能,将Server的状态保存到snapshot中,同时让raft节点生成快照。如下所示:

func (kv *KVServer) snapshot(lastAppliedLogId int) {
	w := new(bytes.Buffer)
	e := labgob.NewEncoder(w)
	if mr, lr := e.Encode(kv.kvMachine), e.Encode(kv.lastPutAppendId); mr != nil ||
		lr != nil {
		DPrintf("{KVServer-%d} snapshot failed. kvMachine length: %v, result: {%v}, lastPutAppendId: {%v}, result: {%v},",
			kv.me, len(kv.kvMachine.KV), mr, kv.lastPutAppendId, lr)
		return
	}

	data := w.Bytes()
	kv.rf.Snapshot(lastAppliedLogId, data)
	DPrintf("{KVServer-%d} snapshot succeeded\n", kv.me)
}

由于快照的引入,Server也可能需要apply快照,即对上述的applier函数再多加一个msg类型的判断,如下所示:

else if msg.SnapshotValid {
				kv.mu.Lock()
				kv.reloadBySnapshot(msg.Snapshot)
				kv.lastApplied = msg.CommandIndex
				kv.mu.Unlock()
			}

相关问题

为什么Get操作不能直接读leader的本地数据?

在Raft系统中,当面临网络分区情况时,原本的leader如果位于一个小分区,那么他就不知道其实大分区中已经有了一个新leader了,这样如果client还是连接的原本的leader,并且是直接读取该leader的本地数据,那么就会面临读取到过时数据的问题,导致系统线性不一致。

所以解决这个问题的关键在于确定节点真的是leader,这里采取的是一个简单的方法,即将这个Get操作作为一个log日志放入raft系统中,直到raft系统将这个log日志提交后,才返回。实际上还有优化的空间,一个方法是在raft接受到了一个Get操作后,立刻执行心跳,如果接收到了过半的节点的心跳回复,那么就证明了这个节点是真的leader,这样就可以直接返回数据了,这就避免了将Get操作放入raft系统中的开销。还有一种方法是叫做Lease Read,它的吞吐更大,详情可参考深入浅出etcd/raft —— 0x06 只读请求优化。

applier中是否有机会出现重复执行的put、append操作?

有机会出现。例如当客户端发送后,Server将其提交给了Raft,但是Raft没有在规定时间内返回,那么就会返回超时,然后客户端再去循环提交一轮,再一次提交给这个节点的时候,节点此时可能还是没有收到Raft的返回,所以会再次提交给Raft,这样就会出现重复提交的情况。而在applier中就会只执行第一次提交的操作,后续的提交都会被忽略。

只用lastPutAppendId记录最后一次的Put、Append操作的id是否可行?

可行。因为系统中Put、Append操作的结果只会是ok,所以不需要记录每次的Put、Append操作的id,同时由于raft系统中一旦apply了就是永久apply了,并且前面的操作也都apply了,不存在回退的情况,所以如果当前操作的id小于最新一次Put、Append操作的id,那么就说明是重复执行了,直接返回ok即可。

运行结果

代码通过了1k次的测试,如下图所示。

请添加图片描述

参考资料

  • 深入浅出etcd/raft —— 0x06 只读请求优化

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1607981.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

✌粤嵌—2024/4/12—插入区间✌

代码实现&#xff1a; 解题思路&#xff1a;先将数组 newInterval 插入到数组 intervals 的末尾&#xff0c;再转换成合并区间 /*** Return an array of arrays of size *returnSize.* The sizes of the arrays are returned as *returnColumnSizes array.* Note: Both returne…

组合预测 | Matlab实现ICEEMDAN-SMA-SVM基于改进完备集合经验模态分解-黏菌优化算法-支持向量机的时间序列预测

组合预测 | Matlab实现ICEEMDAN-SMA-SVM基于改进完备集合经验模态分解-黏菌优化算法-支持向量机的时间序列预测 目录 组合预测 | Matlab实现ICEEMDAN-SMA-SVM基于改进完备集合经验模态分解-黏菌优化算法-支持向量机的时间序列预测预测效果基本介绍程序设计参考资料预测效果 基本…

两个查国内产业信息新闻数据的必备网站

产业经济信息网&#xff1a;产业经济信息网由报业协会主管主办&#xff0c;成立于1997年&#xff0c;是由报业行业报委员会发起&#xff0c;几十家权威行业媒体共同组建的、国内最大的行业信息发布网站之一。网站所拥有的“产经数据库”容纳了54家行业媒体的信息数据200多万条&…

Nextjs学习入门 - 创建第一个项目

1 通过npx创建一个nextjs项目 通过命令创建&#xff1a; npx create-next-applatest 得到如下项目结构图&#xff1a; my-app- src //源代码目录- app //引用目录- favicon.ico //网站图标- globals.css //全局css- layout.tsx //布局文件- page.tsx //页面 路径"…

C语言C/S架构PACS影像归档和通信系统源码 医院PACS系统源码

C语言C/&#xff33;架构PACS影像归档和通信系统源码 医院PACS系统源码 医院影像科PACS系统&#xff0c;意为影像归档和通信系统。它是应用在医院影像科室的系统&#xff0c;主要的任务是把日常产生的各种医学影像&#xff08;包括核磁、CT、超声、各种X光机、各种红外仪、显微…

YOLO-World——S(cvpr2024)

文章目录 Abstract成果 MethodPre-training Formulation: Region-Text PairsModel ArchitectureYOLO DetectorText EncoderText Contrastive HeadTraining with Online VocabularyInference with Offline Vocabulary Re-parameterizable Vision-Language PANText-guided CSPLay…

JavaSE 有这一篇就够(呕心狂敲41k字,只为博君一点赞!)

目录 一. 基础语法 1. 数据类型 2. 基本数据类型转换 3. 运算符 3. 循环语句 5. 定义方法 6. 数组 二. 面向对象 1. 类和对象 2. 构造方法 3. 方法的重载 4. this关键字 5. static关键字 6. 代码块 7. 访问权限修饰符 8. 面向对象的三大特征 封装 继承…

开关到模拟量全覆盖钡铼IOy系列模块集成热电阻、热电偶等传感器

钡铼IOy系列模块作为一种创新的工业自动化解决方案&#xff0c;以其灵活的自由拼接设计和丰富的接口类型&#xff0c;在工业级DI/DO/AI/AO集成方案中扮演着重要角色。其中&#xff0c;其在集成热电阻、热电偶等传感器方面的能力更是为工业控制系统带来了全新的可能性。 开关到…

BNB链融合

BNB Chain融合 BNB Chain目前有BNB智能链&#xff08;BSC&#xff09;&#xff0c;BNB信标链 BNB信标链&#xff1a;用作质押和投票的治理层&#xff0c;采用BEP-2代币标准BNB智能链(BSC)&#xff1a;用作EVM兼容层&#xff0c;提供DApp、DeFi服务、共识层、多链支持和其他Web3…

NVIDIA NCCL 源码学习(十四)- NVLink SHARP

背景 上节我们介绍了IB SHARP的工作原理&#xff0c;进一步的&#xff0c;英伟达在Hopper架构机器中引入了第三代NVSwitch&#xff0c;就像机间IB SHARP一样&#xff0c;机内可以通过NVSwitch执行NVLink SHARP&#xff0c;简称nvls&#xff0c;这节我们会介绍下NVLink SHARP如…

EasyExcel追加写入数据,分批查询多次写入场景下,注意使用方式【OOM警告】

使用.withTemplate(file) 将临时数据文件和真实数据文件合并的方式&#xff0c;在生产环境大批量数据下&#xff0c;完全不可取&#xff0c;有很高的内存溢出风险 伪代码 public static void writeAppend(String fileName) {String filePath "tempDir".concat(Fil…

linux_python源码安装及基础设置odoo安装

python源码安装及基础设置 1、资源下载2、源码安装3、 yum安装pip4、pip安装虚拟环境1、安装虚拟环境库2、配置环境变量3、创建自己的虚拟环境 5、安装升级pip的两种方式1、get-pip.py升级2、安装源码升级 6、odoo部署 1、资源下载 python3.13 python版本库 2、源码安装 yum…

5款开源、美观、强大的WPF UI组件库

前言 经常看到有小伙伴在DotNetGuide技术社区微信交流群里提问&#xff1a;WPF有什么好用或者好看的UI组件库&#xff1f;,今天大姚给大家分享5款开源、美观、强大、简单易用的WPF UI组件库。 WPF介绍 WPF 是一个强大的桌面应用程序框架&#xff0c;用于构建具有丰富用户界面…

C++-结构体-指针-地址-指针的指针-地址的地址

经验证&#xff0c;仿真结果与预期一致。 #include <QDebug> struct test_years {int year;};//定义结构体 int main() {//定义三个结构体&#xff0c;s01,s02,s03test_years s01,s02,s03;s01.year 1000;//给s01结构体中year赋值s02.year 2000;//给s02结构体中year赋值…

OpenHarmony实战开发-Web自定义长按菜单案例。

介绍 本示例介绍了给Webview页面中可点击元素&#xff08;超链接/图片&#xff09;绑定长按/鼠标右击时的自定义菜单的方案。 效果预览图 使用说明 长按Web页面中的图片或者链接元素&#xff0c;弹出自定义的Menu菜单&#xff0c;创建自定义的操作&#xff0c;如复制图片、使…

如何使用 Cloudflare 和 Mailgun 设置自定义电子邮件

作为一名软件工程师&#xff0c;您可能考虑拥有一个专业的电子邮件账户&#xff0c;以及自己的网站&#xff0c;比如 “infoexample.com”. 但这可能会花费一定金额&#xff0c;您可能不愿意支付。 但您知道您可以免费做到吗&#xff1f;事实上&#xff0c;有一种方法可以做到…

牛客2024 【牛客赛文X】春招冲刺 ONT34 加油站【中等 贪心 C++、Java、Go、PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/a013a0691a0343aeb262ca1450d2fe4e 思路 贪心&#xff1a; 如果总的gas小于走完全程的cost&#xff0c;直接返回-1不需要再找了 如果确保了可以走完一圈之后&#xff0c;那么从index 0开始找&#xff0c; 当g…

lua 环境安装

下载地址&#xff1a; https://luabinaries.sourceforge.net/download.html 安装环境变量 检查一下是否安装成功&#xff0c;有版本号&#xff0c;打印一句话&#xff0c;如下表示成功 idea 安装插件&#xff0c;方便编写lua脚本 配置一下idea 运行测试 local function m…

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台级联时,下级平台未发流是什么原因?

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安…

破解费用管理迷局,企业费用管理从不止于报销

数字化变革浪潮下&#xff0c;各种企业费用报销软件如雨后春笋般不断涌现&#xff0c;企业报销效率大幅提升&#xff0c;部分财务处理流程得到固化和优化&#xff0c;报销早已不再是企业费时费力的财务难题。那么&#xff0c;企业费用管里如何实现呢&#xff1f; 企业费用贯穿于…