6.824/6.5840 Lab 4: Fault-tolerant Key/Value Service

We are the champions my friend

And we'll keep on fighting till the end

We are the champions

——We Are The Champions

完整代码见: GitHub - SnowLegend-star/6.824: As we advance, the trials grow ever more arduous, and now we stand before an even mightier summit, one that demands our highest prowess and unwavering resolve to conquer.

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

这次的Lab和Lab2确实有点类似,不同之处是Put和Append都不应向客户端返回值。这里不返回值指的是忽略Value,但是可以设置Err字段来确保操作的成功处理。

令人辛酸的是貌似我踩完了这个Lab所有的坑,从最粗糙的方法一步步改进直到最后构架出一份几乎无懈可击的代码,可谓是筚路蓝缕

下面来看具体的实验要求。

Part A: Key/value service without snapshots (moderate/hard)

实验说明的开头就有点令人费解——ClerksPut()Append()Get()RPC发送到与Raft节点关联的kvserverleader。我们先弄清楚Client和Clerk和区别,然后再分析Clerk请求处理流程。

  1. 某个Client A通过它的抽象Clerk A 向kvserver发送操作请求operation
  2. Kvserver中的某个Server B接收到了这个operation。由于kvraft中和Server和raft中的Server一一对应,所以这个Server B在raft中的peer我们记为Server B1。接下来就有两种情况
    1. Server B1是raft中的Follower,Server B也是kvraft中的Follower。则Server B会拒绝这个operation,Clerk A1收到错误信息Err会继续向其他kvserver发送operation
    2. Server B2是Leader,Server B也是Leader。则Server B会接收这个operation,处理完毕后向Clerk A1返回相应的Value和Err

具体的代码实现主要Clerk和Server。我们先分析Clerk端的实现。

Clerk端和Lab02的Clerk端实现大同小异——如果向kvserver发送operation返回失败,就继续向下一个kvserver继续发送。值得注意的一点是:我们该用nrand()来给谁生成唯一的id。给每个operation都生成一个唯一id?还是给每个Client对于的Clerk都生成唯一的id呢?这正是我在上一个lab中埋下的伏笔。

Raft的作者在论文的6.3节中其实给出了可行的方案。我们可以利用(CliendId, CommandIndex)来确定一条唯一的operation。当然,为每条operation都生成唯一的id也是可以通过part A的,但是有个隐患,在此先按下不表。我一开始没看到这张图,用的方法就是后者。

Clerk端的实现还有一个小tip——记录上一次成功通信的Leader。这样就不必每次都从kvserver 0进行尝试了,虽然测试点一共就设置5个kvserver。

然后就是重点了——如何实现Server端?先梳理kvraft处理operation的流程

Get()、Put()、Append()的大致框架就不再赘述了,记得加上去重判定。

  1. kvserver收到来自Clerk的operation后,会立即把operation提交给下一层的raft进行一致性同步。由于Clerk可能会提交重复的operation,所以需要记录operation完成情况的映射opCompleteState来去重。
  2. Raft对这个operation的同步结束后,会把它提交给kvserver。
  3. Kvserver接收这个operation后,会在自己的kvstorage上应用这个operation。注意,raft提交的operation也可能是重复的

会遇到的第一个难点就是#3。在kvserver等待raft传回的msg时,该选择阻塞式等待还是利用goroutine进行并行等待?很显然应该实现后者。“Test: ops complete fast enough (4A)”这个测试点就是测试并行等待的实现。我最初也是抱着试一试的心态实现阻塞式等待,果不其然连“TestBasic4A”这个最初的测试点也没能通过。

如果并行执行,假设kvserver发送了operation X给raft进行同步,那如何确保kvserver正确收到raft同步完成之后的operation X?毕竟在kvserver(身份为Leader)等待raft返回X的期间,Clerk又持续发送了许多不同的operation。首先,我们可以利用channel实现并行等待。又利用map进行适当映射,确保每个channeloperation一一对应

由于我们需要用到kv.rf.Start(operation)来获取raft中log的最新index,将这个index作为key,operation绑定的channel作为value,以此来维护一张map。

func (kv *KVServer) waitForResult(indexOld int) ResultFromRaft {kv.mu.Lock()//如果没有这个Ch就创建一个if _, ok := kv.reslutCh[indexOld]; !ok {kv.reslutCh[indexOld] = make(chan ResultFromRaft)}waitCh := kv.reslutCh[indexOld]kv.mu.Unlock()select {case result := <-waitCh:return resultcase <-time.After(time.Millisecond * 1000):return ResultFromRaft{Err: ErrTimeout}}
}

这段并行等待的代码我愿称之为画龙点睛之笔。实现并行等待的另一个重要部分是——kvserver如何接收来自raft提交的msg。我们可以模仿Lab03,创建一个goroutine来监听raft的提价情况。

func (kv *KVServer) applier() {for !kv.killed() {// var msg raft.ApplyMsgselect {case msg := <-kv.applyCh:if msg.CommandValid {op, ok := msg.Command.(Op) //将command重新转化为Op格式的内容if !ok {DPrintf("command转化为Op失败")}}}
}

结合applier()和waitForResult()两个函数就可以大致构建出并行等待的框架了。我还画了个流程图,可以更加直观地理解这两个函数的相互作用。

还有一点就是raft提交的operation是否过期的问题,我采用是“注意Raft的任期已改变”这一方案。这个问题一开始可能会让人摸不着脑袋,举个例子:

  1. 当 Clerk 发出请求并且 Leader 调用了 Start(),此时 Leader 将请求放入日志并等待日志复制和提交。然而,Leader 可能在完成这些步骤之前失去领导地位(例如,可能因为选举超时、网络分区等原因)。

这种情况下,Clerk 的请求没有完全处理好,新的 Leader 还未接管该请求并将其成功提交。因此,需要处理这种“前 Leader 丢失领导地位”的情况。

实验说明的hint给出了两种可行的方法:

  1. Raft 任期(Term)已改变:当节点发现自己的任期(Term)变得比其他节点的任期小(例如通过心跳或投票请求),它会意识到自己不再是 Leader,并返回一个错误状态,通知 Clerk 请求失败。
  2. 日志索引中出现了不同的请求:如果在 Start() 返回的索引处出现了不同的请求,说明这个索引被新 Leader 覆盖了,前 Leader 的日志项可能已经被放弃或覆盖,因此该请求没有被成功提交。

说实话上述两个方法还是显得有点晦涩,但是实现起来只需要几行代码即可解决。我这里选择的是方法1。如果raft中的Leader在对operation进行一致性同步的时候失去了领导地位,那这条operation就不再会被提交了,这种情况其实我们已经处理过。kvserver中 的Leader由于长时间没有收到这个operation的提交,会进行超时处理。

我们真正要考虑的情况是:raft中的Leaderoperation提交诶kvraft层面了,但是此时它因为某种缘故变成了Follower,那么kvraft收到的operation就不应该被应用并返回给Clerk。如果kvserver应用了这个operation,但是因为网络波动reply过了很长时间才返回给Clerk。Clerk可能会先进行超时处理,再次发送这个operation给kvserver中新的Leader。但是一段时间后Clerk又会重新收到这个迟到的reply,导致Clerk最终接收两次operation的结果。在我的实现中Clerk不会收到旧的reply。

这里可以直接用continue的原因:waitForResult()中对应index的channel一直在等待applier()向channel传入内容。如果直接进行continue,就会把过期的msg直接丢弃,applier()也不会将这个msg传入channel中,所以waitForResult()会出现超时等待。

Part A最后一个难点就是实现——如何在kvraft层面实现数据的一致性同步。我一开始理解错了实验说明给出的hint,还以为kvserver中的Leader只能接收raft中Leader提交的msg,忽略所有Follower提交的msg。真正的解决方法是让所有kvserver接收来自raftpeer所提交上来的msg,并把它们应用于各自的kvstorage。至于Get()方法也一样应用,只不过非Leader的kvservers可以丢弃Get()获得的value,只有Leader可以才可以返回处理结果valueErr

分析理解了上述三个难点,我们可以顺利通过4A的第一个测试了,但是一般会倒在第二个测试点“Test: ops complete fast enough (4A)”。究其原因就是在raft的实现层面中,Leader是每隔固定的时间发送Heartbeat。如果Client一瞬间发送大量的command也不能立即发送Heartbeat进行一致性同步。应该修改的地方就是一旦Leader收到新的command就立即发送Heartbeat进行同步

下面分享下我在完成Part A遇到的各种bug。

首先在applier()的实现上踩了大坑。

①前文提到,从raft提交的operation是可能重复的。存在这样一种情况:在某一时刻,大量Clients同时通过Clerk向kvserver发送大量op。其中包括操op X,但是raft没能在规定时间内处理完op X,于是一段时间后Clerk由于超时重发op X。注意此时kv.requestComplete[args.Identifier]=false,所以kvserver又会再次把op X提交给raft。

所以事实上由于一瞬间向raft提交的op数量过多,第一个op X已经被添加到Leader的Log中等待进行一致性同步。现在kvserver又向Leader 的Log中添加了op X。一段时间后,这两个op X都会由raft重新提交给kvserver。

②判断raft提交的msg合法性出现逻辑问题。我直接把逻辑判写在了最前面,这就导致了kvserver中只有Leader可以接收到来自raft的msg,而非Leader的kvserver会直接忽略它raft中的peer提交的msg。这样一旦出现了Leader的切换,新的Leader就无法得知旧Leader的kvstorage已经应用了哪些内容。

就像下面这样。最初所有的operation都由kvserver 3进行应用,而其他四个kvserver什么都不做。一旦kvserver 0成为了新Leader,它的kvstorage会从nil开始。

可恶的是当时我还没意识到这个逻辑判断存在的bug,想了个昏招——直接建立个全局的kvstorage应付了事。这样你还别说,几乎可以通过Part A了,就是不符合linearizability。而且引出了最棘手的bug。

单独测试TestPersistPartitionUnreliableLinearizable4A的时候没有任何问题,但是一旦结合其他测试点进行测试,TestPersistPartitionUnreliableLinearizable4A就无法满足linearizability。开始我还以为是偶然性问题,但是多次测试每次都是如此。这就不得不着手找出问题所在了。我猜测可能是多线程同步进行多种测试的时候,测试函数之间的全局变量globalKVStorageglobalRequestCompleteState产生了相互污染

现在又回到了之前的交叉口——到底是选择全局变量还是想办法进行kvserver之间的数据同步。前者被否定了,我们再次把目光转向后者。

其实在TestPersistOneClient4A这个测试的时候,我就考虑数据同步的事情,但是没有深入思考就搁置了。

关于msg合法性判断可行性的思考——对于身份为Follower的kvservers,屏蔽Get(),保留Put()/Append()的辨析。

对于身份为Leader的kvserver,我们暂且忽略。对于普通的kvserver,由于Clerk不会与之通信,所以它们不具有reslutCh。Leader 提交给raft的op进行一致性同步完成后,raft中所有的Server都会向上提交。也就是说对于kvserver中的Follower,也会同步接收到peer提交过来的msg。此时,这些Follower也应该把msg转化为op应用到自己的StateMachine中。但是,op应用到StateMachine的结果应该直接丢弃?Get()的结果可以直接丢弃。Put()/Append()的操作一旦应用返回值就无关紧要了。

③关于程序卡住的小心得。一旦发现程序突然卡住,就往死锁的方向上靠,仔细检查自己在某处逻辑判断处是否及时释放了锁。

像这种遇到特殊条件需要返回的情况,一定要记得先释放锁在返回。这就是一个很容易忽略的点。 

Part B: Key/value service with snapshots (hard)

完成Part A后,如果Lab03实现的raft不存在bug,那Part B简直就是手到擒来。很遗憾,我又成了倒霉蛋,raft存在的小bug搞得我汗流浃背。这里就不再赘述我再次和raft斗争的过程,希望你们看到这里的时候raft是浑然一体、无懈可击的masterpiece。

涉及到snapshot,我们首先即应该考虑应该保存哪些状态。参考在raft中实现snapshot的过程,显然本地存储kvstorageoperation完成情况的映射opCompleteState是都应该被保存的。别的状态是否需要保存存疑,就先尝试只保存这两个状态。

有趣的是我终于在这里理解了CommandValid和SnapshotValid真正的作用,之前一直不知道这两个变量到底怎么使用。

值得注意的一点是raft和kvraft在加载snapshot传入的参数是不一样的。之所以raft读入snapshot时需要第二个参数而kvsraft不需要,是因为所谓的第二个参数就是snapshotData。这个snapshotData是kvraft主动发送给raft进行保存的,对于kvraft来说就相当于一个临时变量,当然不用加载这个参数了。

SequenceNum提出的思考过程

还记得我在本文刚开始提到的两种方法来表示operation的唯一性吗?我用的就是为operation生成唯一的id。这种方法最终折戟于“Test: snapshot size is reasonable (4B) "snapshot too large 9497, should not be used when maxraftstate = 500"”

如果利用operation的唯一id作为opCompletetState的key,那opCompleteState的大小会随着operation数量的增多而线性递增。一旦operation数量过多,那就要花费大量空间来存储opCompleteState。

我们仔细观察Clerk端发送operation的过程:前文提到每个Client都利用自己包装后的Clerk与kvraft进行通信。虽然不同Clerk可能并发访问kvraft,但是在每个Clerk内部发送operation却是线性的

举个例子,有A B C三个Clerk与kvraft进行通信。A B C可能同时向kvraft发送operation,这是并发现象。考察单个Clerk A,它开始向kvraft 发送了operation A0,在operation A0被成功处理前,它不可以再发送相同类型(记住这一会儿要考的)的operation A1。

通过观察以上现象,我们为每个Clerk设置一个递增序列commandIndex就变得很自然了。对于某个commandIndex,任何比它小的commandIndex都是已经被处理过的。在Server端,或许我们只保存一个最后被应用的commandIndex就可以了。任何小于此commandIndex都视作重复op。

自此,opCompleteState的大小就可以被压缩到一个理想的值了。将不同Clerk的id作为key,将Clerk发送的operation附带的commandIndex作为value。对于任意operation,先获取它的ClientId,再比较它的commandIndex和opCompleteState[ClientId]的大小即可达成去重的判断。

美中不足的是采用这种方案还有一个漏洞。前面说道对于每个Clerk,上一个operation处理完之前不能继续处理下一个同类型的operation。但是却存在这样一种情况,Clerk A发送第一个操作Get(),用(ClientId=A, commandIndex=0)来标识这个operation。由于此时kvserver的kvstorage为空,将会返回Err=“NoErrkey”。Clerk A如果又接着发送第二个操作Put(),那它的标识符也是(ClientId=A, commandIndex=0)Clerk中是单线程操作的,如果卡在Get()就不可以同时调用Put()/Append()。

最后补充下Part B遇到的小bug。

①args.PrevLogIndex-rf.lastIncludedIndex<0。说明Follower的snapshot已经应用了,但是Leader的PrevLogIndex还没有更新。这种情况直接返回Success就行。

②类似的问题。Follower的snapshot要比Leader发送的这个snapshot更新。

说实话由于踩到了所有的坑,这个Lab我的完成时间还是相当长的。不停地重构代码简直要了我的老命。

 

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ShardingSphere 数据库中间件

数据库中的数据量猛增&#xff0c;访问性能也变慢了&#xff0c;优化迫在眉睫 ? 1. 关系型数据库本身比较容易成为系统瓶颈&#xff1a;单机存储容量、数据库连接数、处理能力都有限。 2. 当单表的数据量达到 1000W 或 100G 以后&#xff0c;由于查询维度较多&#xff0c;即使…

Webpack Tree Shaking 技术原理及应用实战,优化代码,精简产物

前言 在前端开发中&#xff0c;优化代码体积和提升应用性能是至关重要的课题。Webpack 提供了多种优化手段来帮助开发者实现这一目标&#xff0c;Tree Shaking 就是其中一种非常重要的优化技术&#xff0c;它通过在编译阶段移除未被使用的代码模块&#xff0c;从而显著减小最终…

【热门主题】000075 探索嵌入式硬件设计的奥秘

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【热…

[保姆式教程]使用目标检测模型YOLO11 OBB进行旋转目标检测:训练自己的数据集(基于卫星和无人机的农业大棚数据集)

之前写了一个基于YOLOv8z做旋转目标检测的文章&#xff0c;内容写得不够好&#xff0c;内容也比较杂乱。现如今YOLO已经更新到11了&#xff0c;数据集也集齐了无人机和卫星的农业大棚&#xff0c;所以这次就写一个基于YOLO11 OBB的农业大棚旋转检测。 1. 下载源码配置环境 在h…

Matplotlib 内置的170种颜色映射(colormap)

Matplotlib 提供了许多内置的颜色映射&#xff08;colormap&#xff09;选项&#xff0c;可以将数值数据映射到色彩范围——热力图、温度图、地图等可视化经常会用到。 # colormap 有两种引用形式plt.imshow(data, cmapBlues)plt.imshow(data, cmapcm.Blues) 颜色映射可以分为…

工业—使用Flink处理Kafka中的数据_ProduceRecord1

1 、 使用 Flink 消费 Kafka 中 ProduceRecord 主题的数据,统计在已经检验的产品中,各设备每 5 分钟 生产产品总数,将结果存入Redis 中, key 值为

剑指offer(专项突破)---字符串

总目录&#xff1a;剑指offer&#xff08;专项突破&#xff09;---目录-CSDN博客 1.字符串的基本知识 C语言中&#xff1a; 函数名功能描述strcpy(s1, s2)将字符串s2复制到字符串s1中&#xff0c;包括结束符\0&#xff0c;要求s1有足够空间容纳s2的内容。strncpy(s1, s2, n)…

915DEBUG-obsidianTemplater使用

Templater使用 tp函数不正常显示相应数据 模板使用方式不正确 <% tp.date.now("YYYY-MM-DD") %> 应该被放置在一个被Templater识别为模板的文件中&#xff0c;或者在你使用Templater的插入模板功能时输入。如果只是在一个普通的Markdown文件中直接输入这段代码…

OpenAI:AGI共5层,我们现在在第2层

迈向AGI顶峰的五层阶梯&#xff1a;我们正跨越的第二步 ©作者|潇潇 来源|神州问学 在2024年的OpenAI开发者日&#xff08;Dev Day&#xff09;上&#xff0c;我们见证了人工智能领域的一系列重大进展。OpenAI的CEO Sam Altman提出了一个关于通用人工智能&#xff08;AGI…

Python从入门到入狱

Python是从入门到入狱&#xff1f;这个充满调侃意味的说法在程序员圈子里流传甚广。表面看&#xff0c;它似乎是在嘲笑这门语言从简单易学到深陷麻烦的巨大反差&#xff0c;实际上却隐藏着很多值得深思的问题。要解读这个话题&#xff0c;得从Python的特点、使用场景以及潜在风…

AMEYA360 | 杭晶电子:晶振在AR/VR中的应用

晶振在AR/VR设备中扮演重要角色&#xff0c;为其核心电子系统提供稳定的时钟信号&#xff0c;确保设备的高性能运行。 以下是晶振在AR/VR应用中的具体作用&#xff1a; 01、图像处理与同步 1、晶振为图形处理单元(GPU)和显示芯片提供精准的时钟信号&#xff0c;支持高速图像渲染…

【前端】小程序实现预览pdf并导出

小程序实现预览pdf并导出 一、前言二、需要的wx api三、完整代码 一、前言 小程序没办法直接导出pdf或一些文档&#xff0c;只能借助api先将文件下载下来并打开&#xff0c;再让用户手动去保存。之前做“小程序当前页面截图转pdf导出”功能的时候&#xff0c;小程序好像也无法…

使用 postman 传递 binary 类型的图片到后端接口遇到的坑

使用 psotman 传 binary 类型图片报错&#xff1a; -2024-12-04 [http-nio-9090-exec-1] WARN org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required r…

嵌入式系统应用-LVGL的应用-平衡球游戏 part1

平衡球游戏 part1 1 平衡球游戏的界面设计2 界面设计2.1 背景设计2.2 球的设计2.3 移动球的坐标2.4 用鼠标移动这个球2.5 增加边框规则2.6 效果图2.7 游戏失败重启游戏 3 为小球增加增加动画效果3.1 增加移动效果代码3.2 具体效果图片 平衡球游戏 part2 第二部分文章在这里 1 …

Linux 无界面模式下使用 selenium

文章目录 前言什么是无界面模式&#xff1f;具体步骤安装谷歌浏览器查看安装的谷歌浏览器的版本下载对应版本驱动并安装Python 测试代码 总结个人简介 前言 在 Linux 服务器上运行自动化测试或网页爬虫时&#xff0c;常常需要使用 Selenium 来驱动浏览器进行操作。然而&#x…

大数据新视界 -- Hive 数据湖集成与数据治理(下)(26 / 30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

2.经典规划控制方法-深蓝学院

规划算法 关于规划算法可以参考我的博客 这里说明一下机械臂规划算法的特性以及规划算法的扩展 任务规划 - 路径规划 - 轨迹优化 过程&#xff1a;路径规划&#xff1a;笛卡尔空间通过IK求解得到关节空间&#xff08;构型空间c-space&#xff09;&#xff0c;然后通过关节插值…

基于Matlab计算机视觉的车道线识别与前车检测系统研究

随着自动驾驶技术的发展&#xff0c;车道线识别和前车检测成为智能驾驶系统中的核心技术之一。本实训报告围绕基于计算机视觉的车道线识别与前车检测系统展开&#xff0c;旨在通过处理交通视频数据&#xff0c;实时检测车辆所在车道及其与前车的相对位置&#xff0c;从而为车道…

Java Web 2 JS Vue快速入门

一 JS快速入门 1.什么是JavaScript&#xff1f; 页面交互&#xff1a; 页面交互是指用户与网页之间的互动过程。例如&#xff0c;当用户点击一个按钮&#xff0c;网页会做出相应的反应&#xff0c;如弹出一个对话框、加载新的内容或者改变页面的样式等&#xff1b;当用户在表…

OpenHarmony-4.GPIO驱动

GPIO 1.功能简介 GPIO&#xff08;General-purpose input/output&#xff09;即通用型输入输出。GPIO又俗称为I/O口&#xff0c;I指的是输入(in&#xff09;&#xff0c;O指的是输出&#xff08;out&#xff09;。可以通过软件来控制其输入和输出&#xff0c;即I/O控制。通常&…