当前位置: 首页 > 科技观察

图解Raft共识算法:日志是如何复制的?_0

时间:2023-03-14 14:38:04 科技观察

Raft日志格式在Raft算法中,需要实现分布式一致性的数据称为日志。大多数做Java后端的人说起日志,一般都会想到项目通过log4j等日志框架输出的信息。Raft算法中的数据提交记录会按照时间顺序追加,Raft也会严格按照时间顺序并按照一定的格式写入日志文件:如上图所示,Raft日志以日志条目的形式存在(LogEntry)来组织,每个日志条目包含一个命令,tenure信息,以及该日志条目在日志中的位置信息(索引值LogIndex)。命令:客户端请求发送的执行命令,有点绕。我觉得可以理解为客户端需要存储的日志数据。索引值:日志项在日志中的位置。需要说明的是,索引值是一个连续的单调递增的整数。任期号:创建此日志条目的领导者的任期号。日志复制过程Raft的复制过程大致如下:leader收到客户端的请求,创建一个新的日志条目,并追加到本地日志中,然后leader将新的日志条目发送到该条目是复制到关注者的本地日志。当领导者收到大多数追随者的成功响应时,日志条目将应用于状态机。可以理解为日志条目写入成功,最后leader返回日志写入成功的消息响应给client,流程如下图所示:可以看出在复制过程中Raft,leader收到大部分follower的成功响应,将日志项应用到状态机。它不响应跟随者,而是直接用成功消息响应客户端。这是一种优化方法。同时,Raft会在下一次RPCappendlog请求中追加这个logitem信息。以上只是复制过程,没有任何问题。在这个过程中,难免会出现节点宕机等问题。在这种情况下,Raft是如何处理的呢?如何保证日志的一致性?上面提到,一般情况下,当leader的logappendRPC请求响应成功后,leader和follower的日志保持一致。但是在leader突然宕机的情况下,可能会导致leader和follower的日志不一致。这种情况会加剧leader后续一系列宕机的问题:注:例子来自Raftpaper。如上图,当leader选举成功后,follower可能会出现a-f的情况:a-b表示follower的日志条目落后于当前leader;c-d表示follower的部分日志项还没有提交;e-f的情况稍微复杂一些,以上两种情况都存在。还原一下上图的情况是怎么发生的:假设一开始e是leader,f在term2被选举为leader,写了好几条logentry后,在appendRPCrequest中crash,被选举重新启动后成为领导者(第3个任期),并在写入多个日志条目后崩溃;ewasre-electedastheleader(termnumber4)atthistime,successfullycopiedseverallogentries,Atthesametime,someofthemwerenotsuccessfullyappendedtomostofthefollowersandcrashedagain.同时,followerb在复制部分日志项后崩溃;supposeawaselectedastheleaderinterm5,andcwaselectedasleaderinterm6.Theleader,beforecopyingallthelocallogstootherfollowers,crashedagain.在第7学期,d被选为领导者。写了好几条日志后,他在appendRPC请求中崩溃了,最后形成了上图的情况。面对以上情况,Raft是如何解决日志一致性的呢?在Raft的日志机制中,为了简化日志一致性的行为,有两个非常重要的特性:如果不同日志中的两个条目具有相同的索引和任期号,则它们存储相同的指令。如果不同日志中的两个条目具有相同的索引和任期编号,则它们之前的所有日志条目也相同。第一个特点是Raft日志条目在日志中不会发生变化,所以只要日志条目的索引值和任期号相同,就可以认为它们存储了相同的指令数据信息。第二个特点是leader会强制follower复制自己的日志,解决日志不一致的问题。leader会在appendRPC请求时附加上要复制的日志和之前的日志项相关信息。如果follower无法匹配到一条包含相同索引位置和任期号的日志条目,那么他将拒绝接收新的日志条目,然后leader将继续递减要复制的日志项的索引值,直到找到一条日志具有相同索引和术语编号的条目。日志项,最后直接覆盖follower之后的日志项。可以认为两个条目具有相同的索引和任期编号,因此所有先前的日志条目也相同。因此,Raft的logappending大致可以分为两步:leader找到follower和自己相同的最大logitem,也就是follower之前的log和leader的log相同;log实现日志一致性。下面我用一个例子来完整表达Raft在日志复制过程中是如何进行强制日志覆盖的。假设有一个leader和一个follower,他们的logentry复制如下:可以看出follower在第3个任期是leader,追加日志的过程中crash,重启后变成follower,然后新的leader追加到日志中,此时他的任期号为3的最后一个日志条目将被覆盖。我们先看Raft的appendentryRPC的请求参数:参数描述termleader的termleaderIdleaderID以便followers可以将clients重定向到leader,例如有时client向follower而不是leader发送请求)prevLogIndex的索引newlogentryprevLogTermnewlogentryentries[]之前的logentry需要保存(如果用作heartbeat,logentry的内容为空;为了提高效率,它一次可以发送多个)leaderCommitLeader已知最高日志条目的索引Leader追加和覆盖Follower过程如下:如prevLogIndex=7,prevLogTerm=3通过logappendRPC请求跟随follower;follower判断当前最新日志的term号与prevLogTerm不一致,拒绝追加;leader继续递减需要复制的日志项的index值,此时prevLogIndex=6,prevLogTerm=3;follower找到LogIndex=6,LogTerm=3的日志项,follower接受追加请求;leader然后在follower的LogIndex=6,LogTerm=3日志条目之后追加并覆盖日志条目。本文转载自微信公众号《后端进阶》,可通过以下二维码关注。转载本文请联系后端高级公众号。