3.6 Safety(安全性)
这一节很多细节需要消化,对Raft算法的细节深入理解很有帮助
这一节很多细节需要消化,对Raft算法的细节深入理解很有帮助
这一节很多细节需要消化,对Raft算法的细节深入理解很有帮助
前一节描述了Raft如何选择Leader和复制log entry。然而,到目前为止所描述的机制并不足以确保每个状态机以相同的顺序执行完全相同的命令(即apply相同的log entry)。例如,当Leader提交几个log entry时,Follower可能不可用,然后可以选为Leader,用新log entry覆盖这些条目;因此,不同的状态机可能执行不同的命令序列。
本节通过添加对Leader的限制来完成Raft算法。该限制可确保任何给定term的Leader包含以前term中已经commit的所有log entry(图3.2中的Leader完整性属性)。考虑到选举的限制,我们就会使承诺的规则更加精确。最后,我们给出了前导完整性属性的证明示图,并展示了它如何导致复制状态机的正确行为。
3.6.1 Election restriction(选举限制)
在任何基于Leader的共识算法中,Leader最终必须存储所有已提交的log entry。在一些共识算法中,如viewstamped Replication算法中,即使最初不包含所有提交的log entry,也可以选择Leader。这些算法包含了额外的机制,以识别缺失的log entry,并将它们传递给新的Leader,无论是在选举过程中或之后不久。不幸的是,这导致了相当多的额外的机制和复杂性。Raft使用了一种更简单的方法,它保证了前一个term的所有已经commit的log entry从选举时起就出现在每个新Leader的log里面,而不需要将这些log entry通过复制/追加的形式向新Leader进行同步。这意味着log entry只向一个方向流动,从Leader到Follower,并且Leader永远不会覆盖或删除其日志中的现有log entry。
Raft使用投票过程来阻止candidate赢得选举,除非这个candidate日志包含所有已提交的log entry(也就是说,只有保存了所有已经提交了的log entry的candidate才可能成为Leader。那是怎么保证的呢?接着向下看)。candidate必须与集群的大多数服务器联系才可能当选,这意味着每个已提交的log entry必须出现在至少一个服务器的log中。如果candidate的日志至少与该大多数的其他日志一样最新(其中“最新”定义在下文),那么它将包含所有已经提交的log entry。RequestVote RPC实现了这个限制:RPC包含有关candidate日志的信息,如果某个Follower的日志比candidate的日志更最新,则拒绝给这个Candidate投赞成票。
Raft通过比较两个服务器上log entry的index和term来比较两个log entry的新旧。当两个服务器上的log中最新的那个log entry所属的term不一样,那么term大的log算是新的;如果term一样,那么在该term中最新的log entry所具有的index大的算是新的。(就是先比term-粗粒度的比,然后比index-细粒度的比)。
3.6.2 提交前任Leader的log entry
如第3.5节所述,Leader知道:一旦某个log entry存储在大多数服务器上,那么这个log entry就是已提交了。如果一个Leader在提交log entry之前崩溃(这时候该Leader也不知道这个log entry是否会成功提交),未来新选出的Leader将尝试完成该log entry的向其它Follower的复制工作(前提是这个log entry是已提交的)。但是,新选出的Leader不能立即得出某个log entry是否已经被提交。图3.7说明了一种情况,即旧的log entry存储已经在大多数服务器上,但仍然可以被未来的Leader覆盖。
我们来看看为什么会产生上述的问题
系统中有5个服务器,其中有1个Leader和4个Follower。
(状态1):S1是Leader(当前的term=2),S2-S4是Follower。这是,S1收到来自客户端的请求,需要新增一个log entry,那么S1就会发送AppendEntries RPC给S2-S5,但是呢,由于网络的问题,只有S1(Leader自己)和S2按照S1的要求追加了这个log entry,这时候,S1宕机了。此时,系统中的各服务器log的状态就如图3.7的(a)所示;
(状态2):S1宕机后,S5通过选举成为了新的Leader(term=3)(想一想S5为什么能当选?因为S5可以获得S3和S4的选票,加上自己的选票,满足了多数派),S5也收到了另一个客户端的请求,需要新增log entry,S5自己先将这个log entry追加进了自己的log中,这时,还没来得及发送AppendEntries RPC给其它任意的服务器,S5就宕机了。此时,系统中的各服务器log的状态就如图3.7的(b)所示;
(状态3):S5宕机后,S1重启成功并且再次被选为Leader(term=4)(想一想S1为什么能再次当选?),这时候,S1发现自己的log entry(index=2)没有被大部分的服务器所复制,那S1就先发送AppendEntries RPC给S2-S5,让他们追加log entry(term=2,index=2的那个log entry)。S3完成了log entry(term=2,index=2的那个log entry)的追加(但是,term=2,index=2的那个log entry还未状态)。此时,S1又收到了一个来自客户端的命令,S1又要追加log entry(term=4,index=3的那个log entry);
(状态4-1):且S1仅本地追加成功了。这时候,S1又宕机了(注意,这时term=2,index=2的那个log entry仍未状态)。此时,系统中的各服务器log的状态就如图3.7的(c)所示。假设S5此时已经重启成功,而且其有一次成为了新Leader(term=5),这时候他就要将term=3,index=2的log entry向S1-S4去同步,就成了(d1)的情况,其覆盖了S1已经提交的term=2,index=2的logentry,并且迫使S1删除了term=4,index=3的log entry;(这有什么问题?想一想在(状态1)的时候,S1该如何回复客户端?)
(状态4-2):且S2和S3也复制了term=4,index=3的那个log entry,而且这个log entry已经被提交了。那么,S5就不会被选为新Leader。(为什么?因为term=4,index=3的那个log entry已经是被S1、S2和S3保存在了log中,S5如果想成为新Leader,S5需要拿term=3、index=2的log entry去与S1、S2、S3中至少一个的最新的log entry(term=4、index=3)去比,按照上面先比term后比index的规则,S5怎么都赢不了)
想一想出现上面的问题的原因是什么?图3.7中,新Leader提交前任Leader的log entry的方法是通过按照log的顺序先提交前任Leader的log entry再提交自己任期内的log entry,这会导致的问题就是:当前Leader提交的前任Leader的log entry的term还是前任的term编号,那其它Candidate再进行选举的时候,进行log entry比较的时候还是拿不到新的Leader的term号(这就出现了图3.7中,d1的情况)。那么Raft怎么解决这个问题呢?Raft中,新Leader不会按照log的顺序从前到后去提交前任Leader的log entry,而是将这个工作放在了提交自己任期内的log entry时一起完成,这样就保证了同步前任Leader的log entry的同时也同步自己的term到了对应的Follower,这样的话,有Candidate在进行选举时,进行log比较时,必然会比较当前的term,这样就避免了图3.7中d1的问题了。(这里有个猜测:Raft中log entry的replicate log entry是按照批量来replicate的,也就是说Leader通过自己维护的Follower的已提交的信息,每次要求Follower replicate entries[],而且entries[]里面至少有一个当前term的log entry)。
为了解决图3.7中的这个问题,Raft不是通过数副本的数量来判断某个log entry是否已经提交。Raft中log entry的提交分两类,第一类是本term内的log entry,此类log entry用数副本数量的方法来判断提交;第二类是前任Leader的log entry,Raft通过Log Matching Property来保证log entry能够提交,而Log Matching Property就是通过AppendEntries RPC请求时进行log entry检查来保证的(也就是)。
还是以图3.7来说明Raft的方法。在(c)时刻,S1不是先replicate term=2,index=2的log entry到S3,而是等到自己任期term=4内有新的客户端command请求过来,要保存新log entry时候,也就是途中的term=4,index=3的log entry需要replicate至Follower时,S1要求Follower对entries[(term=2,index=2),(term=4,index=3)]进行replicate,这样的话,就能保证S2,S3(或者多数派服务器)replicates相应的entries[(term=2,index=2),(term=4,index=3)]后,在自己的log中都会有term=4的信息(也就是最新的log entry的term=4),这样,图3.7中的d1就不会出现,也就是S5就不会成为新Leader,因为在选举的时候,需要S1-S3中的至少一个投赞成票,而S1-S3中的最新的log entry应该是term=4,index=3,根据上面描述的Candidate选举规则,S5的最新的log是term=3,index=2的log entry,在term比较中,S5就输了。
这里还有个疑问没解决:
就是S1将term=2,index=2 replictae到其它Follower服务器的话,那么term=2的那个Leader是如何回复其客户端的?term=2,index=2这个log entry中的command是算成功了?还是失败了?(后面还要再确认这个问题)
3.6.3 Safety Argument(安全性论证)
在描述了完整的Raft算法后,我们现在可以更准确地证明Raft算法Leader Completeness Property的正确性了(这个论证是基于安全证明;见第8章)。我们假设Leader Completeness Property不成立,然后我们利用反证法来证明。
假设任期term=T的Leader(记为LeaderT)在其任期内提交了一个log entry(记为LogT),但是LogT没有被后面的Leader保存。假设LeaderU(U是大于T中的最小的那个)没有保存LogT。
1.LogT一定是在LeaderU参选时候就没有在它的log里面(因为Leader不会删除也不会覆盖自己的log);
2.LeaderT已经将LogT replicate到大多数的服务器了,LeaderU又收到了大多数服务器的赞成票,那么,至少有一个投赞成票的服务器在给LeaderU投赞成票的时候已经保存了LogT(如图3.8中的S3)。S3就是产生矛盾的关键;
3.S3一定是在其给LeaderU投赞成票前已经保存了来自LeaderT的LogT ,否则,S3应该拒绝LeaderT的LogT AppenEntries RPC请求,因为如果S3是在给LeaderU投赞成票后才保存了来自LeaderT的LogT 的话。因为U>T,S3不应该保存这个LogT ;
4.S3仍然会保存来自LeaderT的LogT,即使它投赞成票给LeaderU,而且在选举期间,S3不会删除自己的log,也就是LogT;(因为Raft算法不会在选举期间删除log entry)
5.S3给LeaderU投赞成票,因此LeaderU中一定是有比S3还新的log entry,这就产生了以下的矛盾:
6.第一个矛盾:如果S3和LeaderU最后一个log entry的term相同,那么LeaderU一定至少有与S3一样长的log,那么LeaderU一定会包含LogT,这是一个矛盾;
7.第二个矛盾,如果S3和LeaderU最后一个log entry的term不相同,那么LeaderU一定比S3的log长,就是说在LeaderU的log在LogT之后还有新的log entry(假设为LogT+1),那么在对LogT+1进行复制的时候,会执行Log Matching日志匹配。LogT+1的成功复制说明了LogT已经匹配成功,也就是LeaderU需要带有LogT,这又是一个矛盾之处;
8.这就完成了反证,所有term>T的Leader必须保存了在term=T内已经提交的所有log entry;
9.Log Matching Property 日志匹配机制保证了在此后的Leader中一定会保存那些被间接提交的log entry(如图3.7中的d2)。
在说明完了Leader Completeness Property,我们就可以证明State Machine Safety Property(如图3.2描述),也就是说,如果某个服务器在自己的状态机上apply某个固定index的log entry,其它的服务器不会在这个log位置apply不同的log entry。在某个服务器apply一个log entry到自己的状态机的时候,服务器的这个log entry以及之前的所有log entry一定是和当前的Leader的内容是一样的,而且这些log entry一定是已提交的。
试想一下,在初始有效的term里面被服务器apply的log entry, Leader Completeness Property保证了后面的所有更大term的Leader一定会保存相同的log entry,因此后续的再提交log entry的服务器也一定会提交相同的log entry,这就是所说的 State Machine Safety Property。
最后,Raft要求服务器按照log的顺序来apply log entry。结合State Machine Safety Property,这就保证了所有的服务器将按照同样的log顺序,执行同样内容的log entry,这就实现了真正的状态机的一致性。