6.824/6.5840 Lab 2: Key/Value Server

故事里能毁坏的只有风景

谁也摧毁不了我们的梦境

弦月旁的流星划过了天际

我许下 的愿望 该向谁 去说明

——我是如此相信

完整代码见: https://github.com/SnowLegend-star/6.824

还是那句话,尽量只是参考思路而不是照抄 

先阅读几遍实验说明的Introduction部分,Lab的核心就在下面这段话:

客户端可以向键值服务器发送三种不同的RPC:Put(key, value)、Append(key, arg)和Get(key)。服务器维护一个内存中的键值对映射。键和值都是字符串。Put(key, value)为特定键安装或替换值,Append(key, arg)将arg追加到键的值上并返回旧值,Get(key)获取键的当前值。对于不存在的键,Get应该返回一个空字符串。对于不存在的键,Append应该表现得像现有值是零长度字符串一样。每个客户端通过Clerk与服务器进行Put/Append/Get方法的RPC交互。Clerk管理与服务器的RPC交互。

Client有三个操作请求,而Server负责接收这三个操作请求并返回。那么该如何着手呢?先给KVServer声明一个map[string]string类型的映射来维护映射。有了操作对象,接下来就可以有的放矢了。

再看看什么是线性化:

操作的线性化(Linearizability)是分布式系统中一致性模型的一种,它确保操作看起来像是一次一个地立即执行的。具体来说,线性化具有以下特性:

  1. 实时性:每个操作在某个瞬间发生,这个瞬间称为线性化点(linearization point)。
  2. 顺序性:如果一个操作在另一个操作之前完成,那么第一个操作的线性化点必须在第二个操作的线性化点之前。

简单来说,线性化保证了系统操作的可见顺序与实际执行顺序一致,这对开发和调试分布式系统非常重要,因为它使系统行为更具可预测性。

线性化的具体要求

  1. 实时顺序:假设有两个操作A和B。如果操作A在操作B之前完成(即,A的调用返回时,B还未开始),那么A的效果对B应该是可见的。换句话说,操作B应该能够观察到操作A的结果。
  2. 原子性:操作要么完全发生,要么完全不发生。任何一个操作都不能部分完成。对于键值存储来说,这意味着读和写操作都必须是原子的。

 

例子

假设我们有一个键值存储系统,系统中有两个客户端,Client 1 和 Client 2。以下是操作序列:

Client 1 向键 "x" 写入值 "A"(Put("x", "A"))。

Client 2 从键 "x" 读取值(Get("x"))。

Client 1 将键 "x" 的值改为 "B"(Put("x", "B"))。

Client 2 再次从键 "x" 读取值(Get("x"))。

线性化要求:

如果 Client 2 在操作3之前进行操作2(即读取操作),它应该读取到的是值 "A"。

如果 Client 2 在操作3之后进行操作2(即读取操作),它应该读取到的是值 "B"。

如果 Client 2 在操作1之后进行操作2(即读取操作),它不能读取到操作1之前的任何值(即不能读取到空值或其他旧值)。

这样,任何并发操作的结果都必须与某个串行化执行顺序一致,即每个操作看起来都是在某个时刻瞬间完成的。

为什么线性化很重要?

线性化使得系统行为对开发人员和用户来说更加直观和可预测。它避免了许多由于并发和网络延迟导致的一致性问题,是构建可靠分布式系统的基础。例如,在银行系统中,如果转账操作不是线性化的,用户可能会看到账户余额不一致的情况,这会导致严重的问题。

总之,线性化确保了系统中的所有操作都可以按某种顺序一一排列,使得分布式系统在面对并发操作时仍然具有一致的行为。

了解了上述两点,可以正式开始阅读part01了。

Key/value server with no network failures (easy)

您的第一个任务是在没有消息丢失的情况下实现一个解决方案。

即不用考虑Server的回复丢失导致Client需要重新发送请求的情况。

您需要在client.go中向Clerk的Put/Append/Get方法添加RPC发送代码,并在server.go中实现Put、Append()和Get() RPC处理程序。

按部就班就行。注意一点——Server中所有涉及并发的方法应当加锁操作

当您通过测试套件中的前两个测试时,您就完成了这个任务:“one client”和“many clients”。

假如part01没有出现眼花的情况应该是很快就可以完成的,但不巧的是我经典看岔代码导致出现了个bug。

  • 要严格注意字符串拼接的顺序。
kv.kvStorage[args.Key] = args.Value + oldValue

       我这里直接把顺序拼接反了,导致最后的输出和要求输出完美形成对称~

Key/value server with dropped messages (easy)

基本上通过了part01后,就只会有 “Test: unreliable net, many clients ...”这一个fail的测试点了。所以我们的注意力就只要集中在处理这一点上。

下面来看实验说明:

现在,您应该修改解决方案以在消息丢失的情况下继续运行(例如,RPC请求和RPC回复)。如果消息丢失,那么客户端的ck.server.Call()将返回false(更准确地说,Call()等待回复消息的时间间隔,如果在该时间内没有收到回复,则返回false)。您将面临的问题之一是Clerk可能需要多次发送RPC直到成功。然而,每次调用Clerk.Put()或Clerk.Append()都应该只执行一次,因此您必须确保重发不会导致服务器执行请求两次。向Clerk添加代码以在未收到回复时重试,并向server.go添加代码以在需要时过滤重复。以下是处理重复检测的指导。

开始我还以为要自己设置时间间隔来处理Call()出现消息丢失的情况。后来才发现Call()是自动处理信息丢失的情况从而返回false,我属实是多虑了。

Hints

1、您需要唯一标识客户端操作,以确保键值服务器只执行每个操作一次。

每个客户端都有唯一的Clerk。每个操作都有唯一的id。在Server维护一个操作请求处理情况的映射即可。

是不是Server端又要添加一个处理Client端请求操作成功从而返回消息的handler?可要可不要。

2、您需要仔细考虑服务器必须维护的状态以处理重复的Get()、Put()和Append()请求,如果有的话。

对这个hint的理解很重要。所谓重复处理,就是Server会再次调用资源去处理已经处理好的operation。

我们发现Get()这个operation就是从Server中获取一个对应的Value即可。无论是否重复处理都只要一步操作,所以在这里进行过滤Get()重复与否其实结果都是一致的。但是,如果Client需要通过中转才会访问到Server,我们倒是可以在中转处设置一个cahce来减少对Server的重复访问

Put()的重复请求过滤与否也无所谓。毕竟它也只有一步操作哈哈哈。

重点是Append()。如果是第一次处理Append(),那Server需要获得对应Key的oldValue,并且修改给定Key对应的Value。但是对于重复的Append(),我们只需要返回oldValue即可,不用重复修改Key对应的Value了。

3、您的重复检测方案应该快速释放服务器内存,例如每个RPC隐含着客户端已经看到了其上一个RPC的回复 。可以假设一个客户端一次只会调用一个Clerk。

上述要求的意思是,在设计重复检测机制时,服务器需要高效地管理和释放内存,避免长时间保留无用的记录。具体来说,每个RPC请求隐含地表示客户端已经成功处理了之前的RPC请求的回复。因此,服务器可以安全地删除那些已经被客户端确认接收的RPC请求记录。

这意味着服务器只需要保留每个客户端最近处理的RPC请求记录,不需要保存所有的历史记录

一言蔽之,part02要完成的工作就两点:

  • Server维护每个operation的完成情况opComplete
  • Server维护每个operation得到的处理结构opReply
  • Server收到Client发来的operation已完成的消息后,删除这个operation的完成情况和处理结果,即删除opComplete和opReply对应的表项。

最后贴一张Lab通过的截图~

具体代码

这里贴份代码吧,具体代码可以去github看。

server.go

package kvsrv

import (
	"log"
	"sync"
)

const Debug = true

func DPrintf(format string, a ...interface{}) (n int, err error) {
	if Debug {
		log.Printf(format, a...)
	}
	return
}

type KVServer struct {
	mu sync.Mutex

	// Your definitions here.
	kvStorage  map[string]string //存储键值对映射
	opReply    map[int64]string  //维护每个请求应该收到的回复
	opComplete map[int64]int     //维护每个请求是否完成
}

func (kv *KVServer) TaskComplete_Handeler(args *TaskCompleteArgs, reply *TaskCompleteReply) {
	kv.mu.Lock()
	defer kv.mu.Unlock()
	delete(kv.opComplete, args.Identifier) //处理完这个请求后直接删除记录
	delete(kv.opReply, args.Identifier)    //及时删除记录
}

func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {
	// Your code here.
	kv.mu.Lock()
	defer kv.mu.Unlock()

	//只有未完成的操作才可以被执行
	reply.Value = kv.kvStorage[args.Key]

}

func (kv *KVServer) Put(args *PutAppendArgs, reply *PutAppendReply) { //Put方法不需要返回值
	// Your code here.
	kv.mu.Lock()
	defer kv.mu.Unlock()

	if kv.opComplete[args.Identifier] == 0 {
		kv.opComplete[args.Identifier] = 1
		kv.kvStorage[args.Key] = args.Value
	}

}

func (kv *KVServer) Append(args *PutAppendArgs, reply *PutAppendReply) {
	// Your code here.
	kv.mu.Lock()
	defer kv.mu.Unlock()

	if kv.opComplete[args.Identifier] == 0 {
		kv.opComplete[args.Identifier] = 1
		oldValue := kv.kvStorage[args.Key]
		kv.kvStorage[args.Key] = oldValue + args.Value
		reply.Value = oldValue

		kv.opReply[args.Identifier] = oldValue //保存这个operation处理的结果
	} else {
		reply.Value = kv.opReply[args.Identifier]
	}

}

func StartKVServer() *KVServer {
	kv := new(KVServer)

	// You may need initialization code here.
	kv.kvStorage = make(map[string]string)
	kv.opReply = make(map[int64]string)
	kv.opComplete = make(map[int64]int)
	return kv
}

client.go

package kvsrv

import (
	"crypto/rand"
	"math/big"

	"6.5840/labrpc"
)

type Clerk struct {
	server *labrpc.ClientEnd
	// You will have to modify this struct.
	identifier []int64 //每个请求的标识符集合
}

func nrand() int64 {
	max := big.NewInt(int64(1) << 62)
	bigx, _ := rand.Int(rand.Reader, max)
	x := bigx.Int64()
	return x
}

func MakeClerk(server *labrpc.ClientEnd) *Clerk {
	ck := new(Clerk)
	ck.server = server
	// You'll have to add code here.
	ck.identifier = make([]int64, 0)
	return ck
}

// fetch the current value for a key.
// returns "" if the key does not exist.
// keeps trying forever in the face of all other errors.
//
// you can send an RPC with code like this:
// ok := ck.server.Call("KVServer.Get", &args, &reply)
//
// the types of args and reply (including whether they are pointers)
// must match the declared types of the RPC handler function's
// arguments. and reply must be passed as a pointer.
func (ck *Clerk) Get(key string) string {

	// You will have to modify this function.
	id := nrand()
	args := GetArgs{
		Key:        key,
		Identifier: id,
	}
	reply := GetReply{}

	ok := ck.server.Call("KVServer.Get", &args, &reply)

	for ok == false {
		// fmt.Println("Get操作失败,自动重试")
		ok = ck.server.Call("KVServer.Get", &args, &reply)
	}

	argsTaskComplete := TaskCompleteArgs{
		Identifier: id,
	}
	replyTaskComplete := TaskCompleteReply{}
	ok = ck.server.Call("KVServer.TaskComplete_Handeler", &argsTaskComplete, &replyTaskComplete)

	for ok == false {
		// fmt.Println("Server没有收到Get操作完成的通知")
		ok = ck.server.Call("KVServer.TaskComplete_Handeler", &argsTaskComplete, &replyTaskComplete)
	}

	return reply.Value
}

// shared by Put and Append.
//
// you can send an RPC with code like this:
// ok := ck.server.Call("KVServer."+op, &args, &reply)
//
// the types of args and reply (including whether they are pointers)
// must match the declared types of the RPC handler function's
// arguments. and reply must be passed as a pointer.
func (ck *Clerk) PutAppend(key string, value string, op string) string {
	// You will have to modify this function.
	id := nrand()
	args := PutAppendArgs{
		Key:        key,
		Value:      value,
		Identifier: id,
	}
	reply := PutAppendReply{}

	ret := ""
	ok := ck.server.Call("KVServer."+op, &args, &reply)

	for ok == false {
		// fmt.Printf("%v操作失败\n", op)
		ok = ck.server.Call("KVServer."+op, &args, &reply)
	}
	ret = reply.Value

	argsTaskComplete := TaskCompleteArgs{
		Identifier: id,
	}
	replyTaskComplete := TaskCompleteReply{}
	ok = ck.server.Call("KVServer.TaskComplete_Handeler", &argsTaskComplete, &replyTaskComplete)

	for ok == false {
		// fmt.Println("Server没有收到PutAppend操作完成的通知")
		ok = ck.server.Call("KVServer.TaskComplete_Handeler", &argsTaskComplete, &replyTaskComplete)
	}

	return ret
}

func (ck *Clerk) Put(key string, value string) {
	ck.PutAppend(key, value, "Put")
}

// Append value to key's value and return that value
func (ck *Clerk) Append(key string, value string) string {
	return ck.PutAppend(key, value, "Append")
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux-异步IO和存储映射IO

异步IO 在 I/O 多路复用中&#xff0c;进程通过系统调用 select()或 poll()来主动查询文件描述符上是否可以执行 I/O 操作。而在异步 I/O 中&#xff0c;当文件描述符上可以执行 I/O 操作时&#xff0c;进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其它的任务…

R 语言科研绘图第 1 期 --- 折线图-基础

在发表科研论文的过程中&#xff0c;科研绘图是必不可少的&#xff0c;一张好看的图形会是文章很大的加分项。 为了便于使用&#xff0c;本系列文章介绍的所有绘图都已收录到了 sciRplot 项目中&#xff0c;获取方式&#xff1a; R 语言科研绘图模板 --- sciRplothttps://mp.…

企业中数据防泄漏如何防范?有哪些防泄密措施?

企业数据不仅是业务运营的核心&#xff0c;也是企业竞争力的关键所在。 然而&#xff0c;随着信息技术的快速发展&#xff0c;数据泄露的风险也随之增加。 数据一旦泄露&#xff0c;不仅可能导致企业经济损失&#xff0c;还可能损害企业声誉&#xff0c;甚至引发法律纠纷。 …

汽车控制软件下载移动管家手机控车一键启动app

移动管家手机控制汽车系统是一款实现车辆远程智能控制的应用程序‌。通过下载并安装特定的APP&#xff0c;用户可以轻松实现以下功能&#xff1a;‌远程启动与熄火‌&#xff1a;无论身处何地&#xff0c;只要有网络&#xff0c;即可远程启动或熄火车辆&#xff0c;提前预冷或预…

基于事件驱动构建 AI 原生应用

作者&#xff1a;寒斜 AI 应用在商业化服务的阶段会面临诸多挑战&#xff0c;比如更快的服务交付速度&#xff0c;更实时、精准的结果以及更人性化的体验等&#xff0c;传统架构限制于同步交互&#xff0c;无法满足上述需求&#xff0c;本篇文章给大家分享一下如何基于事件驱动…

如何查看阿里云ddos供给量

要查看阿里云上的 DDoS 攻击量&#xff0c;你可以通过阿里云的 云盾 DDoS 防护 服务来进行监控和查看攻击数据。阿里云提供了详细的流量监控、攻击日志以及攻击趋势分析工具&#xff0c;帮助用户实时了解 DDoS 攻击的情况。以下是九河云总结的查看 DDoS 攻击量的步骤&#xff1…

华为HarmonyOS 让应用快速拥有账号能力 - 获取用户手机号

场景介绍 当应用对获取的手机号时效性要求不高时&#xff0c;可使用Account Kit提供的手机号授权与快速验证能力&#xff0c;向用户发起手机号授权申请&#xff0c;经用户同意授权后&#xff0c;获取到手机号并为用户提供相应服务。以下只针对Account kit提供的手机号授权与快…

React 的学习记录一:与 Vue 的相同点和区别

目录 一、学习目标 二、学习内容1️⃣——React的特点 1.组件化设计 2.单向数据流 3.声明式 UI 4.虚拟 DOM 5.Hooks 6.JSX 7.React Native 三、React与vue的比较总结 四、总结 一、学习目标 时间&#xff1a;两周 内容&#xff1a; React的特点React的入门React的…

使用epoll监测定时器是否到达指定时间,并执行回调函数

总览&#xff1a;Linux提供了定时器&#xff0c;暴露出来了文件描述符&#xff0c;所以我们使用epoll帮助我们监测&#xff0c;时间到达后&#xff0c;epoll_wait返回&#xff0c;于是我们根据fd&#xff0c;找到对应的回调函数&#xff0c;然后执行。从而达到定时执行函数的目…

鸿蒙征文|鸿蒙技术分享:使用到的开发框架和技术概览

目录 每日一句正能量前言正文1. 开发环境搭建关键技术&#xff1a;2. 用户界面开发关键技术&#xff1a;3. 应用逻辑开发关键技术&#xff1a;4. 应用测试关键技术&#xff1a;5. 应用签名和打包关键技术&#xff1a;6. 上架流程关键技术&#xff1a;7. 后续维护和更新关键技术…

【MIT-OS6.S081笔记0.5】xv6 gdb调试环境搭建

补充一下xv6 gdb调试环境的搭建&#xff0c;我这里装的是最新的15.2的gdb的版本。我下载的是下面的第二个xz后缀的文件&#xff1a; 配置最详细的步骤可以参考下面的文章&#xff1a; [MIT 6.S081] Lab 0: 实验配置, 调试及测试 这里记录一下踩过的一些报错&#xff1a; 文…

Python和Java后端开发技术对比

在当今互联网技术飞速发展的时代&#xff0c;后端开发扮演着至关重要的角色。Python和Java作为两大主流的后端开发语言&#xff0c;各自具备独特的优势和应用场景。让我们深入了解这两种技术的特点和选择建议。 Java后端开发一直是企业级应用的首选方案。它以强大的类型系统、…

1.2.3 逻辑代数与运算

逻辑代数与运算 基本的逻辑运算常用逻辑公式 基本的逻辑运算 基本逻辑运算非常简单&#xff0c;只包含与、或、非、异或这4种。 这里主要留意对基本逻辑运算的不同叫法&#xff0c;符号表示。逻辑表达式、真值表概念。 与&#xff1a;A和B都为真时&#xff0c;结果才为真或…

解析生成对抗网络(GAN):原理与应用

目录 一、引言 二、生成对抗网络原理 &#xff08;一&#xff09;基本架构 &#xff08;二&#xff09;训练过程 三、生成对抗网络的应用 &#xff08;一&#xff09;图像生成 无条件图像生成&#xff1a; &#xff08;二&#xff09;数据增强 &#xff08;三&#xff…

零售餐饮收银台源码

收银系统早已成为门店经营的必备软件工具&#xff0c;因为各个连锁品牌有自己的经营模式&#xff0c;自然对收银系统需求各有不同&#xff0c;需要有相应的功能模块来实现其商业模式。 1. 适用行业 收银系统源码适用于零售、餐饮等行业门店&#xff0c;如商超便利店、水果生鲜…

我的第一个创作纪念日 —— 梦开始的地方

前言 时光荏苒&#xff0c;转眼间&#xff0c;我已经在CSDN这片技术沃土上耕耘了365天 今天&#xff0c;我迎来了自己在CSDN的第1个创作纪念日&#xff0c;这个特殊的日子不仅是对我过去努力的肯定&#xff0c;更是对未来持续创作的激励 机缘 回想起初次接触CSDN&#xff0c;那…

mac终端自定义命令打开vscode

1.打开终端配置文件 open -e ~/.bash_profile终端安装了zsh&#xff0c;那么配置文件是.zshrc&#xff08;打开zsh配置&#xff0c;这里举&#x1f330;使用zsh&#xff09; sudo open -e ~/.zshrc 2.在zshrc配置文件中添加新的脚本&#xff08;这里的code就是快捷命令可以进…

计算帧率、每秒过多少次

1、c #include <iostream> #include <opencv2/opencv.hpp> #include <string> #include <thread> #include <atomic>using namespace std;const int NUM_THREADS 1; // 线程数量std::atomic<int> frameCounts[NUM_THREADS]; // 每个线程…

【在Linux世界中追寻伟大的One Piece】读者写者问题与读写锁

目录 1 -> 读者写者问题 1.1 -> 什么是读者写者问题 1.2 -> 读者写者与生产消费者的区别 1.3 -> 如何理解读者写者问题 2 -> 读写锁 2.1 -> 读写锁接口 3 -> 读者优先(Reader-Preference) 4 -> 写者优先(Writer-Preference) 1 -> 读者写者…

PS的功能学习(修复、画笔)

混合器画笔工具 就像&#xff0c;电子毛笔 关键功能有两个&#xff0c;自带一个混合器色板 清理画笔是全清&#xff0c;换一支新的毛笔&#xff0c;执行完之后在判断是否载入画笔 载入画笔就是把前景色上的颜色进行叠加处理&#xff0c;重新混入当前的混合色 &#xff08;…