6.824/6.5840 Lab 3: Raft——Part 3B3C

芙蓉花又栖满了枝头 奈何蝶难留

漂泊如江水向东流

望断门前隔岸的杨柳 寂寞仍不休

我无言让眼泪长流

——山外小楼夜听雨

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

在完成Lab之前,务必把论文多读几遍,力求完全理解Leader选举、log日志等过程。

Part 3B: log (hard)

我开始以为完成PartA后面的三个部分就好办了,后来才发现过于too young too naïve。PartB的hard难度不是白标的,真正实现起来太恶心了。在着手解决问题之前,我们要建立一种思想:由于Leader是会不断发送heartbeat的,这从宏观看来就是一种for循环,所以对于未成功接收到heartbeatServer不需要用for循环重复发送直至成功。其次,调用for循环可能使进程或者goroutine长时间持有lock,不利于并发性。sendVoteRequest()的内部实现同样如此。

其次,我们可以发现hearbeat和appendEntry本质上没什么区别。只需要把普通heartbeat看成是附加了一个空的entry就行。

在阅读完TA关于lock的建议之后,我们要注意到一点,及时把可能会改变的变量保存起来。例如rf.currentTerm这个参数,可能在刚开始调用sendHeartBeat()时这个参数还是1,等到了设置args的时候这个参数就变成2了。可以用currenTermConst=rf.currentTerm来保存,这样如果在sendHeartBeat()的执行中rf.currenTerm发生了改变我们也可以及时发现。

下面来讲讲figure 2中几个需要理解透彻的变量。

  1. commitIndex:注意,所有Server都会有自己的commitIndex,这就可以抽象出一个applier()函数完成entry提交工作。对于Leader来说,有半数Server(包括Leader本身)在自己的log中存储了某个entry后,Leader就可以视作这个entry是时候被提交了。而对于普通的Follower来说,则是用到#5:commitIndex = min(leaderCommit, index of last new entry)。这里可能有人会有疑问,什么时候会出现leaderCommit和index of last new entry不等的情况呢?假设A是Leader,B是Follower。
  1. leaderCommit>index of last new entry。B作为Follower发生了宕机,导致B的log要远远落后于A。LogA的index=7,LogB的index=4。B恢复后重新加入集群,A会根据nextIndex[B]来实现两者的同步。假设两者之前的log已经达成了同步,那A会在nextIndex[B]=5(即PrevLogIndex=4)处发现两者同步了。此时A会给B发送一个index=5的entry来帮助B补充自己的log。在B看来,leaderCommit=7,但是自己的index of last new entry=5,那它的commitIndex=5。
  2. leaderCommit<index of last new entry。假设A和B的log已经同步了,且两者的index=8。现在Client向Leader添加了新的entry,Leader在发送heartbeat时就会顺便把这条新的entry发送给B。如果这个时候大多数Server还没有接收到这条entry,那在B看来LeaderCommit=8,但是自己的index of last new entry=9。
  1. lastApplied:很明显一条entry要先可以commit,再进行apply。把commit看做是apply的先决条件。
  2. nextIndex[]:重中之重。这条属性我感觉特别绕,自己在纸上模拟了几次才完全理解它的含义。BTW,不要感觉自己看懂了就直接开始代码实现,一定要尝试在纸上模拟具体情况确保自己真正理解后才动手写代码。nextIndex[]难在使用和实现上。

最开始我是在sendHeartBeat()中对Leader的nextIndex[i]进行初始化的,但这样就产生了一个问题。每次发送heartbeat时,nextIndex[]原有的内容都被重新初始化了。如果所有Server的日志都已经对齐还好,万一出现日志未对齐的情况,这就导致不得不在sendHeartBeat()内部一次性完成所有Follower日志的对齐工作,导致goroutine长时间占有lock从而严重影响了并发。所以可以考虑在某个Server当选Leader的时候才进行nextIndex[]的初始化。在这个Leader在位期间,它的nextIndex[]一直由heartbeat进行同步状态的维护。

还有,根据TA在《Students' Guide to Raft》中提到的,用len(rf.log)给nextIndex[]赋值的时候一定要慎重,因为rf.log的长度是极易发生变化的。

最后一点,不能在完成PartB时nextIndex[i]—是不可行的,会挂在“leader backs up quickly over incorrect follower logs”这个测试点中。用课上提到的根据Term快速回退才行。

  1. matchIndex[]:它的作用乍一看也是让人摸不着脑袋。对于如何判断大多数Server接收到了某个entry,我最开始的想法是和vote一样,为每个Leader维护一个applyEntrySuccess。如果某个Follower成功接收entry那applyEntrySuccess就加1。

但是,由于我们处于一个分布式系统中,无法确保每个Follower接收entry的进度是一致的。可能Follower B在接收index=4的entryX,Follower C却在接收index=6的entryY,那不就得维护一个record[服务器数量][Leader日志长度]的二维数组了。慢着,这个二维数组好像可以从record[M][N]=true/false压缩为record[i]=接收到日志index为i的Server数量,那这不就是matchIndex[]数组嘛。

除此之外,还要注意log[N].term == currentTerm的重要性。可以进行commit的entry一定是在当前Term中产生的,当前Term的Leader不可以随意提交老旧Term中生的entry。《raft-extended》的5.4.2就是在说明这一点。

  1. prevLogIndex:它的值为 nextIndex[i]-1,相当于nextIndex[]的附属。那为什么要同时存在nextIndex[]和PrevLogIndex呢?直接用其中一个来维护日志对齐不好吗?

从宏观上来看,nextIndex[]用来维护Leader和Follower之间日志复制的进度,而PrevLogIndex则是确保Leader和Follower日志的一致性。

这里有一个小技巧,令heartbeat携带的entries[]比PrevLogIndex慢一步。例如Leader的index=5,Follower1的index=4。初始化nextIndex[1]=6,我们现在检查Follower1中PrevLogIndex=5的情况。此时heartbeat携带的entries[]应该为空,如果Follower1中PrevLogIndex=5的entry存在,那直接匹配成功。如果匹配失败,那nextIndex[1]=5,PrevLogIndex会减少为4,此时heartbeat携带的entries[]包含Index=5的entry。如果现在匹配成功了,那直接把entries添加到Follower1.Log的末尾就行,匹配失败则继续递减。

  1. prevLogTerm也类似,就不在赘述。
  2. success:我们要注意到如果Follower的Term要大于Leader的Term,Success应该是false。这种情况出现在旧的Leader宕机后又重新加入集群,确保旧的Leader会及时结束自己的统治生涯。如果Follower和Leader的日志不匹配,Success也是false。
  3. lastLogTerm: 这里也是我发现自己在PartA遗漏的一个小问题。日志更长的Server不一定更新。在《raft-extended》的5.4.1中提到,“If the logs have last entries with different terms, then the log with the later term is more up-to-date. If the logs end with the same term, then whichever log is longer is more up-to-date”切不可忽略这一点。

最后是我在完成lab时遇到的各种bug。

最麻烦的bug就是死锁问题。我在尝试使用更细粒度的锁时,发现“basic agreement”直接超时了。尝试了几次依然如此,我初步判断是什么地方出现死锁了。把sendHeartBeat()看了好几遍也没发现什么错误。直到偶然看了眼Start(),发现这里调用sendHeartBeat()之前就获取了lock,而我在sendHeartBeat尝试再次获取lock就必然会死锁。怪不得打印的日志看不到给Leader添加新的entry。

还有一个问题是注意goroutine的异步性。可能当前Server已经不是Leader了,但是这个Server在Leader状态发出的rpc调用还没结束。

nextIndex的快速回退算是最好解决的问题了。主要是在实现之前我就考虑要不要采用快速回退的方法,由于偷懒就没用。想着等所有测试点都过了再自己优化下,倒是在这里得提前进行这一步优化了。

最后贴张通过的图

 

 

Part 3C: persistence (hard)

完成part2有点懈怠了,就摆烂了好几天。Part C说实话并没有什么特别的任务点,简单完成persist()和readPersist()后再选择合适的位置对状态进行保存即可。

从figure2中我们可以知道需要保存的就三种状态,currentTerm、voteFor和log[]。如果Part B实现得比较完美的话,那么Part C是可以直接通过的。Part C就是加强版的PartB。我就是完成了持久化函数后直接通过了。

通过后我还以为自己真是个天才,Part B实现得过于完美了。于是神便治下傲慢之罪,让我在50次测试中接受拷打。

仔细阅读了问题日志之后,我发现自己第一个问题是nextIndex的快速回退执行有问题,有时候nextIndex来不及回退完成测试就结束了。出现这种问题的原因应该是我只在每次心跳中执行nextIndex的快速回退。

这时摆在我眼前的路有两条:一是发现日志匹配失败后就立即处理nextIndex直至匹配成功,而不是等到下一次心跳才接着处理nextIndex。二是适当调快心跳频率,同时考虑将conflictIndex设置为Follower的commitIndex。如果我可以接收Leader向Follower进行了过度重传,那在Follower的commitIndex不等于0的情况下,发生冲突直接将conflictIndex设置为commitIndex是不是可行的呢?我打算牺牲Leader进行重传的日志条目数量来换取日志的快速对齐,一番权衡之下我选择了第二种方法。

选择了方法二后,最棘手的bug出现了。我发现服务器居然在相同index、相同term的情况下内容不同。

************************************************

Server A在term=2当选为Leader,BCD三个服务器的term=2。三个服务器的日志均为[{-1 -1} {1 1} {22 2}]此时A 断开连接了。

B在term=3的时候当选为Leader,同时向B的日志中添加一个新entry{121 3},此时B的日志为[{-1 -1} {1 1} {22 2} {121 3}]。B还没来得及向CD两个服务器发送这个新entry时,B断开连接。

同时A再次加入连接,ACD的日志均为[{-1 -1} {1 1} {22 2}]。现在A加入连接后,通过发送心跳得知自己的Term=2过时了,更改为Term=3。由于此时集群中还没有Leader,所以会选出Term=4的新Leader

旧的Leader被停止从而断开连接后,由于持久化状态不会保存ServerType,所以它再次加入连接后状态被更新为Follower。现在A加入连接后,状态是Follower而不是Leader,所以无法通过发送心跳得知自己的Term=2过时。由于此时集群中还没有Leader,如果A率先超时的话,那它就会在Term=3成为新的Leader是吗?此时如果再给A添加新的entry时,就会发现A的日志为[{-1 -1} {1 1} {22 2} {242 3}]。

等B再次加入集群的时候,集群中就会出现这种奇怪的日志冲突。

按理说上述情况时不存在的,因为每个Server只会在一个Term中投一次票。但是通过观察,我却发现群众里有坏人。

我真是没想到这里选举居然出了问题,遂加强条件判断。

后来发现加强条件判断还是有漏洞,原来问题出在Heartbeat()里面。如果某个Server已经在Term=X中投票了,于是Term=X一般会选举出一个Leader。但是这些投票的Server由于收到了来自Leader的心跳,就又会重新重置自己的voteFor。此时如果Leader挂掉了,重新连接了一个新的Server,这个Server就可以在Term=X再次获得投票。

照例贴张通过的图嘻嘻

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

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

相关文章

LeetCode - #150 逆波兰表达式求值

文章目录 前言1. 描述2. 示例3. 答案关于我们 前言 我们社区陆续会将顾毅&#xff08;Netflix 增长黑客&#xff0c;《iOS 面试之道》作者&#xff0c;ACE 职业健身教练。&#xff09;的 Swift 算法题题解整理为文字版以方便大家学习与阅读。 LeetCode 算法到目前我们已经更新…

【C++】continue语句、goto语句

1、continue 语句 作用&#xff1a;在循环语句中&#xff0c;跳过本次循环中余下尚未执行的语句。继续下一次循环。 注意&#xff1a;continue只能用于循环中。 示例&#xff1a; 代码&#xff1a; //continue的用法 #include<iostream> using namespace std; int ma…

最长最短单词

最长最短单词 C语言实现C实现Java实现Python实现 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 输入1行句子&#xff08;不多于200个单词&#xff0c;每个单词长度不超过100&#xff09;&#xff0c;只包含字母、空格和逗号。单词由至少一…

ESP32项目 --- 智能门锁(WiFi 蓝牙 OTA)

1 项目简介 1.1 项目概述 本项目是实现一款智能门锁中的智能控制部分, 可以应用在家庭, 办公室等任何使用门锁的场所. 本项目实现了以下主要功能: &#xff08;1&#xff09;通过按键配置密码 &#xff08;2&#xff09;通过按键输入密码开锁 &#xff08;3&#xff09;录…

洛谷题单-入门2-分支结构-python-下

找出出现的最早的最大值 count 0 list_number [] while True:list_number.append(list(map(int, input().split())))count 1if count 7:breaklist2_number_total []for i1,i2 in list_number:list2_number_total.append(i1i2)target max(list2_number_total)index 0 if…

框架模块说明 #05 权限管理_03

背景 权限设计可以分为两个主要方面&#xff1a;操作权限和数据权限。前两篇文章已经详细介绍了操作权限的设计与实现&#xff0c;以及如何将其与菜单关联起来的具体方法。本篇将聚焦于数据权限&#xff0c;为您深入讲解相关的设计与实现方式。 全局开关 Value("${syst…

Linux网络编程之---多线程实现并发服务器

下面我们来使用tcp集合多线程实现并发服务器 一.服务端 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h>typedef struct sockinfo {char ip[16];unsigne…

Linux C/C++编程之静态库

【图书推荐】《Linux C与C一线开发实践&#xff08;第2版&#xff09;》_linux c与c一线开发实践pdf-CSDN博客《Linux C与C一线开发实践&#xff08;第2版&#xff09;&#xff08;Linux技术丛书&#xff09;》(朱文伟&#xff0c;李建英)【摘要 书评 试读】- 京东图书 (jd.com…

网际协议(IP)与其三大配套协议(ARP、ICMP、IGMP)

网际协议&#xff08;Internet Protocol&#xff0c;IP&#xff09;&#xff0c;又称互联网协议。是OSI中的网络层通信协议&#xff0c;用于跨网络边界分组交换。它的路由功能实现了互联互通&#xff0c;并从本质上建立了互联网。网际协议IP是 TCP/IP 体系中两个最主要的协议之…

uniapp实现加密Token并在每次请求前动态更新(vue、微信小程序、原生js也通用!)

导语&#xff1a;在Web开发中&#xff0c;Token作为一种身份验证的机制&#xff0c;被广泛应用于前后端交互过程中。本文将为大家介绍如何在每次请求前动态设置加密的Token&#xff0c;并在请求一次后使Token值加1&#xff08;或其他动态改变的逻辑&#xff09;&#xff0c;从而…

IDL学习笔记(二)IDL处理卫星数据

IDL处理卫星数据 HDF文件数据集属性通用属性 常用HDF4操作函数常用的HDF5操作函数读取HDF文件的一般步骤 HDF4文件读取-----数据信息查询HDF4文件读取示例-----目标数据TIFF输出 HDF文件 数据集属性 数据集名称&#xff0c;是“&#xff1a;”前的一部分&#xff0c;不是long_…

论文阅读——量子退火Experimental signature of programmable quantum annealing

摘要&#xff1a;量子退火是一种借助量子绝热演化解决复杂优化问题的通用策略。分析和数值证据均表明&#xff0c;在理想化的封闭系统条件下&#xff0c;量子退火可以胜过基于经典热化的算法&#xff08;例如模拟退火&#xff09;。当前设计的量子退火装置的退相干时间比绝热演…

TCP/IP协议簇自学笔记

摘抄于大学期间记录在QQ空间的一篇自学笔记&#xff0c;当前清理空间&#xff0c;本来想直接删除掉的&#xff0c;但是感觉有些舍不得&#xff0c;因此先搬移过来。 曾经&#xff0c;我只知道socket函数能进行网络间数据的通信&#xff0c;知道tcp/ip协议也是用来进行网络数据…

c++领域展开第一幕——入门基础(命名空间、iostream、缺省参数、函数重载、nullptr、inline(内联函数))超详细!!!!

文章目录 前言一、c的第一个程序二、命名空间2.1 namespace 的价值2.2 namespace 的定义2.3 命名空间的使用 三、c的输入和输出四、缺省参数五、函数重载六、nullptr七、inline总结 前言 今天小编带着大家进入c的大门&#xff0c;虽然c难&#xff0c;但好事多磨&#xff0c;一起…

DM-VIO(ROS)+t265配置运行记录(ubuntu18.04+ros melodic)

在工作中需要对DM-VIO算法进行测试&#xff0c;于是配置并记录了一下&#xff1a; 首先运行ros接口的dm-vio&#xff0c;一定要先配置源码 https://github.com/lukasvst/dm-vio在这个网址把源码下载下来并解压&#xff0c;并安装一下依赖&#xff1a; sudo apt-get install …

基于Java Springboot成人教育APP且微信小程序

一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse 微信…

基于Java Springboot个人财务APP且微信小程序

一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse 微信…

PotPlayer 最新版本支持使用 Whisper 自动识别语音生成字幕

PotPlayer 最新版本支持使用 Whisper 自动识别语音生成字幕 设置使用下载地址 设置 使用 下载地址 https://www.videohelp.com/software/PotPlayer

【软考网工笔记】网络基础理论——传输层

IPSec协议 Internet协议安全性是一种开放标准的框架结构&#xff0c;通过使用加密的安全服务以确保在Internet协议&#xff08;IP&#xff09;网络上进行保密而安全的通讯。 工作在OSI模型的第三层网络层上&#xff0c;使其在单独使用时适于保护基于TCP或UDP的协议&#xff0…

Fastify装饰器:增强你的路由处理功能加入日志

Fastify以其出色的性能和扩展性脱颖而出。装饰器是Fastify提供的一个强大功能&#xff0c;它允许开发者在不修改核心代码的情况下&#xff0c;向请求&#xff08;Request&#xff09;和响应&#xff08;Response&#xff09;对象添加自定义属性和方法。本文将通过一个简单的示例…