Raft论文解读

一、切入点    
    · Raft是一个分布式一致性算法

    · 如何能更快更好的对大规模数据集进行存储和计算?
        1. 更好的机器   (垂直扩展)
        2. 更多的机器   (水平扩展)
      在越来越多的数据的时代背景下,垂直扩展存在一个瓶颈,很明显在面对大数据集的时候,水平扩展是更好的办法。
    
    · 如何让跨网络的机器之间协调一致的工作?
        1. 状态的立即一致性 (A写入,B立刻变更数据)
        2. 状态的最终一致性 (A写入,B最终会达到该状态)
    
    · 如何应对网络和节点的不可靠?
        从高到低设置了三个可用性的级别:可读写、可读、不可用;
    
    · 组织集群中的机器来使其状态最终一致并允许局部失败的算法被称为一致性算法;

    · Paxos算法由来已久,作为功能和性能最完善的一致性算法,它的缺点是难以理解和实现;
      Raft为了让一致性算法易于实现,简化了Paxos,同时尽量提供于Paxos一样的功能与性能;
    
    · Raft是一种为了管理复制日志的一致性算法;
      它提供了和Paxos算法相同的功能和性能,但是它的算法结构和Paxos不同,使得Raft算法更加容易理解并且更容易构建实际的系统;
      为了减少需要考虑的状态数量以提升可理解性,Raft将一致性算法分解为几个关键模块,
      例如:leader elements(领导人选举)、log replication(日志复制)和高可用;
      同时,Raft还支持一种新机制来允许集群成员的动态变化,利用数据冗余来保证高可用;


二、介绍
    · 一致性共识算法允许集群的机器像一个整体一样工作,即使其中的部分机器Down也能继续工作下去;
      为了增加性能,一致性共识算法不要求集群中所有的机器的状态机都一致,只要求集群的状态机正确即可;

    · Paxos作为最优秀的一致性算法,它的实现过于复杂使得很难在实际的应用中实现;
      Raft以可理解性为切入点,重点不在于算法为什么能工作,更重要的是能供很清楚的知道它为什么能工作;
      相比于Paxos,Raft分解了算法(leader选举、日志复制、安全性),同时还缩减了状态空间(减少了不确定性和服务器之间不一致的方式);

    · Raft的特点:
        1. 强领导性(Strong leader):Raft比其他共识算法拥有更强的领导形式,
                  例如日志条目只能由Leader发起(Paxos中任何节点作为提案人);
                  极大的简化了对复制日志的管理并使得Raft更容易理解;

        2. leader选举(Leader election):Raft使用随机的计时器来选举领导人,
                     避免了多个节点同时候选分票导致选举时间过长;
          
        3. 成员关系调整(Membership changes):Raft易使用了一种共同一致的方法来处理集群配置变换的问题;
                       其中两个不同的配置的大多数机器会重叠,这样可以保证在成员变换时依然可以保持高可用;


三、复制状态机(Replicated state machines)
    · 在面对一个大规模的集群时,小概率的机器故障是不可避免的,为了在部分机器Down时仍然能保持集群的正常运作可以利用复制状态机的方式;
      共识算法的目标就是保证集群上所有节点的状态一致,节点要执行的指令可以分为两种,读和写;
      读指令并不会修改状态机的状态,因此为了保证集群各个节点状态的状态一致,只要将写指令同步给其他节点就能实现集群一致性;

    · 在最理想时情况下,我们期望将所有节点同步再传递下一条指令,这样可以让所有的节点都像单机一样变更状态;
      但是现实的问题是网络问题往往是不可控的,写入命令不可能被同时执行,所以我们只能退而求其次,
      将写指令以日志的形式顺序的发布给其他节点,只要节点能按照固定的序列逐条执行所有的指令,那么它们的状态最终就是一致的;
    
    · 所以一致性算法的任务就是保证复制日志的一致性;
      server上的共识模块接收来自clent的命令,并将它们添加到日志中;
      然后再与其他server上的共识模块进行通信来保证每一个server上的日志最终都能以相同的顺序包含相同的请求,即使部分机器在中途故障;
      一旦指令被以正确的序列复制到各个server上,并按照日志顺序来处理它们,这样服务器集群看起来就像一个高可靠的状态机;

    · 问题的定义:
        · 输入:写命令;
        · 输出:所有节点最终处于相同的状态;
        · 面临的问题:
            1. 网络不确定性:在非拜占庭情况下,出现网络 分区/冗余/丢失/乱序 等问题下要保证正确;
            2. 基本可用性:只要集群中大部分节点能可运行且能保持相互通信,那么集群就应该要能正确的响应客户的请求;
            3. 不依赖物理时序来保证一致性:不依赖物理时钟和极端消息延迟来保证一致性;
            4. 快速响应:一条指令可以尽可能快的(集群中大多数节点响应一轮PRC调用时) 完成,不能依赖于集群中最慢的响应;
          
    · 问题的可行解:
        1. 有一个Leader节点负责发送日志到其他Follower,并决定日志的序列;
        2. 读请求可以由任何节点处理,但是写请求必须被重定向到Leader节点来同一下发日志;
        3. Leader先写入自己的日志,标记为待提交(同步),再同步给半数以上的节点,当follower响应ok,再标记为以提交;
        4. 日志最终由leader按顺序应用于状态机,其他follower最终应用到状态机;
        5. 当leader故障时,follower会通过心跳函数感知到,并重新选举出新的leader来保证集群的正常运行;
        6. 当有新节点加入或退出集群,可以将配置信息同步给整个集群;

    · Raft设计理念:
        1. Raft必须提供一个完整的实际的系统实现基础,这样才能大大减少开发者的工作;
        2. Raft必须在任何情况下都是安全的,并且在大多数情况下都是可用的,并且大部分操作是高效的;
        3. Raft必须要易于理解,额能让人形成直观的认识,以便于在系统的构建中进行必然的扩展;
      基于上述的设计理念,做出了以下的设计:
        1. Raft尽可能的将问题分解为几个相对独立的、可被解决的、可解释、可理解的子问题;
           在Raft算法中,问题被分解为了leader选举、日志复制、安全性、成员变更几个部分;
        2. Raft通过减少需要维持的状态的数量来简化需要考虑的状态空间,使得系统更加连贯并且在可能的时候消除不确定性;
           所有的日志不允许有holes,Raft限制了日志之间变成不一致状态的可能性;
           同时,正确的使用不确定性来减少状态空间的数量,例如利用排名和随机时间来简化Raft中leader选举算法;
      

四、Raft一致性算法的实现
    · Raft是用于管理复制状态机的算法,Raft会选举出一个leader,并且由leader来全权管理复制日志;
      leader从客户端接收日志条目(log entries),吧日志复制到其他服务器上,并告诉follower什么时候可用安全将这条日志条目应用于状态机上;
      leader的出现简化了复制日志的管理,
      例如:leader可用决定日志的序列,而不需要通过多余的过程和其他server协商,并且数据都是从leader流向其他server,当leader发生故障,这种情况下只需要选举出下一个leader就行;

    · Raft的三个子问题:
        1. leader选举:当集群中不存在leader时,会选举出新的leader;
        2. 日志复制:leader从client接收lig entries然后复制到集群中其他的server,并强制要求其他节点的日志和自己保持一致;
        3. 安全性:在Raft中安全性体现在
                   如果有任何的server节点已经运用了一个确定的日志条目到他的状态机,那么其他的server不能在同一逻辑时序上应用一个不同的指令;
                   也就是说每一台机器的应用日志条目的顺序是一样的,而且如果每一个日志条目都先在leader节点上应用,会更加简化Raft算法的实现;
    
    · Raft算法的逻辑:
        开始集群中所有的机器都start up,都处于Follower状态,同时每台机器都会启动一个心跳定时器,定时器的时间在一个范围内随机生成,当接收到leader的心跳请求时会重新计时;
        当某个Follower的定时器time out都没有收到来自Leader的心跳请求,starts election向所有节点发起选举请求,同时启动选举超时计时器,该节点进入Candidate状态,
        如果选举超时计时器time out,会重新发送选举请求;
        当Candidate receives votes from majority of Followers 收到大多数节点的True的投票,Candidate进入Leader状态,开始向所有Followers发送心跳信息;
        如果Candidate discovers current leader or new term 收到了 当前或者新的 leader的心跳请求,会回退到Follower状态;
        如果Leader收到了更新的leader的心跳请求,也会回退到Follower状态;
      为了解决多个节点同时发起选举请求,导致不断分票重选,所以会给定时器设置一个指定范围内随机的值

五、Raft的参数
    · 所有机器上持久化的状态:
        1. currentTerm:当前机器已知最新的leader任期(从0开始,一个单调递增的数);
                        例如可以在收到两个leader请求时,判断哪一个leader是有效的;
        2. voteFor:在当前任期内收到的选票的candidate ID,如果没有置为null;
                    用于保证该节点只能投一票;
        3. log[]:日志条目,每个条目包含了用于状态机的指令、以及leader接收到该条目的Term、逻辑时序Index;
                  如果leader收到其他leader的log的Term大于currentTerm,那么说明当前集群中有更新的leader,会回退到Follower状态并将currentTerm置为Term;
                  Index用于维持指令逻辑上的时序;

    · 所有机器上不需要持久化的状态:
        1. commitIndex:已知已提交的最高的log Index;(初始为0,单调递增);
                        已提交指的是已经被同步给集群半数以上节点;
        2. lastApplied:已经被应用到状态机的最高log的Index(初始为0,单调递增);
                        应用指的是已经应用于本状态机,与其他状态机无关
      
    · leader上不需要维持的状态:
        1. nextIndex[]:对于每一台机器,需要发送到该机器的下一个log的Index;

        2. matchIndex[]:对于每一台服务器,已知已经复制到该服务器的最高log Index;
    
  
六、Raft的实现
    · 追加条目(AppendEntries) RPC:
        · 由leader调用,用于日志条目的复制,同时也被当作心跳使用;

        · 参数
          1. term:leader的任期号,用于让Follower判断该请求是否有效;
          2. leaderID:用于让Follower收到Client的请求时重定向到Leader;
          3. prevLogIndex:发送的log之前的那条log的Index;
          4. prevLogTerm:发送的log之前的那条log的Term;
          5. entries[]:需要复制的日志条目,如果本次RPC用于心跳,这个entries为空;
          6. leaderCommit:leader已知的已提交的最高的log Index;

        · 返回值:
          1. term:其他Server当前所属的任期,用于让leader判断自己是否需要更新;
          2. success:如果Follower维护的条目和prevLogTerm、prevLogIndex匹配上,则为True;
        
        · Follower判断逻辑:
          1. 如果收到的leader term小于自身所属的任期,代表leader过时,返回false;
          2. 如果Follower中没有与prevLogIndex、prevLogTerm对应的log,为了维持log顺序,返回false;
          3. 如果收到的log和已经存在的log冲突,说明Index已经存在,但是old log的term小于new log的,删除old log以及后续所有的log;
          4. 追加log中尚未存在的新条目;
          5. 如果leaderCommit大于Follower的commitIndex(已知已提交的最大log Index),则把commitIndex置为(leaderCommit 或者 上一条新条目的Index中较小的那个);
        
    · 请求投票(RequestVote) RPC:
        · 由Candidate负责调用,用于收集选票数量;    

        · 参数:
          1. term:任期号,用于Follower判断该Candidate是否配参选;
          2. candidateID:候选者的ID,用于让Follower保存本轮投票的投票情况;
          3. lastLogIndex:Candidate的最大的log Index;
          4. lastLogTerm:Candidate的最大的log Term;

        · 返回值:
          1. term:当前任期号,用于让Candidate判断是否需要去更新自己的任期号;
          2. voteGranted:Candidate获得该Follower选票时为True;

        · Follower判断逻辑:
          1. 如果term < Follower自身的currentTerm,返回false;
          2. 如果votedFor为空,或者为candidateID,并且Candidate的log至少和Follower一样新,那么就投票给它

    · 规则:
        · All Server:
          1. 如果commitIndex > lastApploed,说明集群中成功复制到半数以上机器的的log条目大于了当前机器执行了的;
             则lastApplied递增,并将log[lastApplied]应用到状态机中;
          2. 如果收到的RPC请求或者响应中,携带的任期号term > 本机的currentTerm,说明当前集群中处于的任期大于当前机器的任期;
             令currentTerm = term,并切换为Follower;

        · Follower:
          1. 响应来自Candidate和Leader的请求;
          2. 如果在心跳定时器time out之前 收到了当前leader的心跳/日志 或者 收到了Candideta的选举请求,保证Follower不变;
             反之说明当前Follower和Leader之间可能已经失联且当前没有可能其他候选人,
             本机变为Candideta;
        
        · Candidate:
          1. 在转变为Candidate后会开始选举的工作:
             a. 自增当前的任期号currentTerm,表示当前期望竞选的任期;
             b. 给自己投一票;
             c. 重置选举定时器;
             d. 发送请求投票的RPC给其他所有服务器;
          2. 如果收到了半数以上的选票(多数派协议) ,本机变为Leader;
          3. 如果接收到来自新Leader的信息,说明本轮任期已经存在leader;
          4. 如果选举定时器time out,再次发起投票请求;

        · Leader:
          1. 一旦称为Leader会发送空的追加条目(AppendEntries) 作为心跳请求给所有的server;
             且会周期性的不断发送心跳请求;
          2. 如果收到来自client的客户端请求,先将条目加入到本机的log中,当条目被应用到状态机后响应客户端;
          3. 如果有一个Follower对应在leader的nextIndex[]中的值小于last Index,会直接发送 nextIndex[FollowerID]到last Index的所有日志条目;
             如果发送成功:更新nextIndex[FollowerID]和matchIndex[FollowerID]的值;
             如果因为日志不一致而失败,Follower log[Index-1]的Term和Leader的不一致,可能是由其他Leader发送导致的;
          4. 假设存在N>commitIndex,使得大多数的matchIndex[i]>N 以及 log[N].term == currentTerm成立;
             说明当前实际成功复制到半数以上的本任期的logIndex > 本机记录的成功复制到半数以上的本任期的logIndex;
             则将本机记录的commitIndex置为N;
    
    · 五大特性:
      1. 选举安全特性(Election Safety):
          对于一个给定的Term,最多只会有一个leader被选举出来
          (因为leader的选举必须要经过法定人数的同意,一个节点一轮term只能投一个一个机器);
      2. Leader只追加原则(Leader Append-Only):
          Leader不会删除或者覆盖掉自己的日志;
      3. 日志匹配原则(Log Matching):
          如果某个日志条目的某个Index位置的任期号相同,就可以认为两个Log从头到该Index的内容一致;
          (因为同意任期只能有一个Leader,所有的Log都由该Term的同一个Leader下发,
           所以同样的Term同样的Index的Log一定相同,且下发日志时能保证之前的日志和Leader的日志一致);
      4. Leader完全特性(Leader Completeness):
          如果某个日志条目在某个Term中已经被提交,那么这个条目一定必然出现在更大Term的Leader中;
          (一个条目如果要被提交,说明该条目已经在半数以上的机器中了,一个Candideta想要获取选票,则Candideta的log至少要比半数以上的Follower要新;
           因为能保证log中的时序,所以Leader的log一定会比半数以上的机器更完整;
           换句话说,投票给该Leader的机器中至少有一台 同时也在参与提交的机器中,那么Leader因为能保证比半数以上所有的机器要新,那么就肯定至少比已提交的条目要新);
      5. 状态机安全特性(State Machine Safety):
          如果一个服务器已经将某一Index的条目应用到其状态机中,则其他任何服务器在该Index不会应用不同的条目;

  
七、Raft Basic
    · 一个Raft集群包含若干个server节点;
      一个集群的可容错量为ceil(N/2),也就是说在一个经典案例的5节点集群中,允许最多两个节点发生故障;
      在任何时刻,每一个server都只能处于三个状态之一(Leader、Follower、Candideta);
      在一个正常运作的集群中,只会有一个Leader,且其他的节点都是Follower;
      Follower是被动的,它们不会主动的发出请求,只会简单响应来自Leader的请求;
      Leader会处理来自Client的请求,如果Client交互的节点是Follower,会将消息重定向到Leader,所以所有的节点中都会知道当前的Leader是哪一个;
      当前Follower无法和Leader通信时,会转变为Candideta,它会尝试成为集群中新的Leader;
    
    · Raft将时间分割为任意长度的Term,任期用整数的形式来表示;
      每一个任期从一次选举开始,一个或多个Candideta会尝试成为本Term的Leader;
      如果其中一个Candideta成为Leader,那么直到它出现故障之前都不会发生变化;
      在某些情况下,选举会因为分票而失败,这种情况下,本Term会以没有Leader结束;
      但是很快又会发起新一轮的选举;
    
    · Term在Raft中充当逻辑时钟的作用,每个节点都会记录自己所在的任期,所以它们也能判断出收到的请求是不是过时的;
      当发现自己的任期过时的,它都会无条件的将自己变为Follower,且更新自己的Term;
      如果发现发送者的Term过时,会直接拒绝这次请求;
      
    · Raft集群中的server节点之间使用RPCs通信,并且只需要两种基本的RPCs;
      请求投票(RequestVote):由Candideta在选举期间发起;
      附加条目(AppendEntries):由Leader发起,用于同步Cilent发来的请求,以及提供心跳机制;
    

八、Leader election
    · Raft使用心跳的机制来触发Leader选举,Follower不会主动去向Leader发起请求通知存活,而是由Leader主动发起;
      每一个Follower中都拥有一个心跳定时器用于判断Leader是否失联,每收到来自Leader的请求都会将定时器重置;
      如果定时器time out,也就是选举超时,就可以认为当前集群中没有Leader,转为Candideta发起新的选举请求;
    
    · 开始发出RequestVote之前,Follower会先将自身的Term+=1,来向其他节点表示自己是集群中更新的Term;
      然后他会并行地向集群中其他的节点发送RequestVote;
      Candideta状态会保存到 1. 赢得选举成为Leader、2. 其他节点成为Leader、3. 一段时间后没有节点成为的leader;

    · 当一个Candideta获得了从整个集群的半数以上节点中获得对本次Term的选票,就可以认为成为了这次Term的Leader;
      每一个节点在同一个Term只能对一个节点投票,所以它们会记住向哪一个Candideta投了票,并且在收到RequestVote时会更新自己的Term以表示正在参与哪一个Term的选举;
      当Candideta成为了Leader后,会向其他的节点发送心跳消息来通知其他节点,在本次Term中的Leader是谁;
    
    · 一个Candideta可能会从其他服务器中接收到其他Leader的AppendEntries RPC;
      如果这个消息大于等于Candideta的Term,Candideta要么已经过时了,要么本轮Term已经存在Leader了;
      Candideta会将自己变为Folower;
      如果这个消息小于Candideta的Term,说明集群中Leader过时了,Candideta会拒绝这次请求保持Candideta状态;

    · Candidate还有可能直到选举定时器time out也不会得到结果,这时候会继续Term+=1,开始新一轮的选举;
  
    · 为了防止选票被瓜分导致选举频繁发生,会选择将选举定时器设置为一个固定的区间而不是一个固定的值;
      

九、Log replication
    · 当一个Leader被选举出来,他就开始为client提供服务;
      客户端的每一个请求都包含一条被复制状态机执行的指令;
      Leader把这条指令作为一条Log entry Append到本机的log[]中,然后并行地发起AppendEntries RPCs给其他的server节点;
      以便于其他节点能够复制这条Entry,当这条entries被安全地复制,Leader会应用这条entry到本机的状态机中,然后把执行的结果返回给客户端;
      如果Follower故障(网络、机器),Leader会不断的重复尝试AppendEntries RPCs,直到所有的Follwoer都最终存储了所有的Log entries然后复制到集群中其他的server,并强制要求其他节点的日志和自己保持一致;

    · Leader来决定什么时候什么时候把Entries应用到状态机中是安全的,这种log entries被称为committed;
      Raft保证所有已提交的log Entries都是持久化的,且会被所有可用状态机执行;
      当Leader将创建的log entry复制到半数以上的节点上时,log entry就会 committed;
      同时Leader中之前的所有entry也是committed,包括由其他Leader创建的;
      Leader会记录被提交了的entry的Index为commitIndex,同时附加在发送的请求中;
      这样其他的节点也能知道Leader的commitIndex;
      当节点知道集群中提交到哪个Index,节点就能保证集群中这条entry是有效的,因此就能将这条entry应用到状态机中;
    
    · 日志匹配特性:
        1. 如果在不同节点的日志中的两个entry拥有相同的Index和Term,那么它们一定是相同的指令;
        2. 如果在不同节点的日志中的两个entry拥有相同的Index和Term,那么它们之前的entry也是一样的;
      
    · 特性证明:
        1. Leader最多在一个Term里在一个Index上创建唯一的entry,同时这条entry附带的Index属性是不变的;
        2. 在AppendEntries RPC时,Leader会把发送的entry的前一个entry的 Index和Term包含在log中;
           如果在接收方Follower的log[]中找不到对应的entry,则会拒绝这条entry;
        
    · 证明特性正确:
        已知Follower只会按照Index顺序接收的entry,
        已知发送的entry是该Follower没有接收过的,
        已知附带的Index和Term对应的entry是应该要存在于Follower Log[]中的;
        当log[]为空时,特性二一定成立;
        当Follower从一个开始接收entry,会从第一个开始保证前一个entry存在于log[]中;
        因此当AppendEntries返回成功时,Leader就知道Follower的日志段一定是和自己相同的了;
    
    · 正常情况下,Leader和Follower保持一致性,所以一致性检查不会失败,Follower也会正常的接收entry;
      但是,当Leader Down时会导致不一致的出现,Follower可能会丢失一些在新Leader中存在的entry,当然也可能会拥有一些Leader没有的;
      当一个Leader成功当选,Follower中的entries可能会是不同Term的;
      例如:当Term=2的Leader A将一些entries Append到自己的log[]中,但是在提交之前崩溃了;
            当这台机器重启,在Term=3时又被重新当选,并且又添加了一些log Entries到自己的log[]中;
            在Term=2和3的log被提交之前,这个节点又Down了,并且后续持续Down;
            这时候就会出现这样的情况,这台机器中的log[]虽然Index被占用了,但是因为没有同步到其他节点,
            所以其他节点不会保存这个节点中的Term=2、3的entries,相同Index的Entries放的可能是Term=4/5/6...
      
    · 在Raft中,Leader会通过强制Follower直接duplicate自己的log来处理不一致的问题;
      这意味着Follower中Index相同,Term不同的entry(都相同说明entry相同) 会被Leader的覆盖;
      要使得Follower和自己的一致,那就要找到最后两者一致的Index,删除后续所有的entries,并重新发送Leader自身的那一段;

    · Leader会维持一个nextIndex[]记录对应的节点下一个需要发送哪个Index的entry,以支持完成一致性检查;
      当Leader刚刚上任,它初始化所有节点的nextIndex值都是自身LastIndex+1;
      如果一个节点无法满足一致性检查,Leader会将对应的nextIndex递减并重试,直到能够找到一致的部分;
      当然可用通过优化来减少被拒绝PRCs的次数,例如 当AppendEntries RPC的请求被拒绝时,Follower可用返回所冲突的Term和该Term对应最小的Index;
      通过这种方法可用一次性跨越该冲突任期的所有entries,就可以不用一点点的减少nextIndex,而是直接跨过一个Term;
      
    · Leader永远不会覆盖或删除自己的Log[];

    · 一致性特性:
       Raft能够接受,只要半数以上的机器能运作(能够相互通信) 就能复制并应用新的Log Entries;
       并且因为多数派协议,提交的速度并不依赖于集群中最慢的哪个;
  
十、Safety
    · 上面描述了Raft算法是如何选举和复制日志的;
      但是仍然不能充分保证每个状态机会按照相同的顺序执行相同的指令;
      例如 一个Follower可能会进入不可用状态,同时Leader已经提交了若干logEntries,然后这个Follower可能会被选举为Leader并覆盖掉这些LogEntries;
      因此,不同的状态机可能会执行不同的指令序列;
      如何能保证如何Leader对于给定的Term,都拥有了之前Term的所有committed的logEntries;
      即 领导人完整特性 和 状态机安全特性;

    · 选举限制:
        · 在任何基于Leader的一致性算法中,Leader都必须存储所有已经提交的日志条目;
          在某些一致性算法中,某个节点即使一开始并没有包含所有committed的logEntries,它也能被选为Leader;
          这些算法会采用一些额外的机制在选举时将识别丢失的entries并把它们发送给Leader,但是这种方法会导致额外的机制和复杂性;
        
        · Raft则采用投票的方式限制没有拥有所有提交Entries的节点称为Leader;
          而基于多数派协议,Raft必须要获得半数以上的节点的投票才能成为Leader,同时如果一个Entry被标记为已提交,则至少需要半数以上节点的同步;
          那么给Leader投票的节点中,至少有一个节点拥有全部committed的Entries;
          那么我们只要保证Candideta拥有的Entries至少和这个节点一样新,那么就可以认为Leader拥有完整的commited Entries;
          所以可以在RequestVote RPC中增加这样的限制:RPC中包含了Candideta的log信息,然后投票人会拒绝掉哪些日志没有自己的新的投票请求;
          其中log信息会包括lastLogIndex 和 lastLogTerm;
        
        · Raft通过比较lastLogIndex 和 lastLogTerm来定义谁的日志更新;
          如果两个lastLogTerm不同,则认为Term大的更新;
          如果两个lastLogTerm相同,就比较lastLogIndex,Index大的更新;

    · 提交之前任期内的logEntries
        · Leader知道只要一条Entry被存储在了半数以上节点的log[]中,就可以认为它是committed;
          如果leader在提交Entry之前Down了,后续的leader会尝试复制本条Entry;
          然而,一个leader不能断定一个之前Term里的entries被保存在半数以上条目一定是提交了的;
          
        · 一个例子:
          1. 在abcde五个节点中,leader a的Term=2,它向集群中的b节点中成功写入Term=2的entry 2:2(Index:Term);
          2. a崩溃,e依据cde的选票成为了Leader,开始发送Term=3的entry 2.3(Index:Term);
          3. 但是e没来得及同步其他节点就崩溃了,a依据abcd的选票成为了Leader,开始复制2:2,这时候成功复制到了集群多数派abc中;
          4. 这时a崩溃,e上任,因为所有节点都没有Term>3的日志,用2:3同步并覆盖了2:2;
          
        · 可以发现在上面这个例子中,即使2:2被成功同步到了所有节点,但是仍然可能会被后续的日志覆盖;
          所以必须要对其做出优化;
          如果在第3步a第二次上任时立刻将自己的Term=4的no-op尝试同步出去,这样所有的节点都会把Term置为4同时打上3:4的log;
          就不会让节点e选举成功,因为节点e的lastLogTerm=3<4;
          基于log Matching,即使只有1:1entry的节点会最终被同步上2:2和3:4;
          这样就可以通过计算3:4的同步数量来保证2:2的提交;
          为了不干扰正常状态机的允许,会把携带的指令置为空,这也是为什么Raft的newLeader上任时会立即发送心跳信息的原因;

        · Raft永远不会通过计算副本数量来commit之前Term的Entries,只会commit当前Term的Entries;
          一档当前Term的Entires committed,用于Log Matching日志匹配特性,之前的Entries也会被间接的提交;
          换句话就是,即使每个任期只会commit的本任期的Enties,之前Term的Entries就算已经被复制到了大多数机器中,但是因为还没被提交,所以也会允许覆盖;
        
    · 安全性论证
        · 在给定完整的Raft算法后,就可以论证Leader的完整性了;
          首先假设Leader Completeness不存在,假设Term=1的Leader A提交了一条Entry,但是这条Entry没有被存储到未来某个的Leader中;
          因为Leader提交了这条Entry,说明这条Entry在集群半数以上的机器中;
          假设Term=3的Leader B是第一个没有这条Entry的Leader;
          B成为新Leader的条件是获得半数以上的节点选票,那么肯定有至少一台机器C即拥有这个Entry,又参与了投票;
          C肯定是先接受A的提交再参与B的选举,因为如果反过来C就会因为Term=2>1不会接收Entry;
          B要获得C的选票,则B的log[]至少比C的要新,矛盾出现;
          RequestVote会携带B的lastLogIndex和lastLogTerm;
          如果B、C的lastLogTerm相同,那么B的lastLogIndex一定不小于C的,基于Log Matching,B一定包含C的log[];
          如果B、C的lastLogTerm不同,那么B的lastLog创建者Leader X肯定会拥有这条Entry(因为B是第一个) ,因为X的Term=lastLogTerm、Index=lastLogIndex,所以B也一定包含这条Entry;
          基于这两条矛盾,所有比A大的Leader一定会包含所有来自A的committed Log;
        
        · 通过Leader Completeness,可以证明 State Sachine Safety;
          即 集群中所有机器会在同一个Index上应用同一条Entry(别忘了只有被提交的Entry才会被应用);
          在一个机器上应用一个Entry,它的日志必须和Leader的Log[]在该Entry之前的所有log一样,并且被提交了;

        · Raft要求机器按照Log的Index时序应用Entry,这就意味着集群中所有节点会应用相同的指令到自己的状态机中,
          也说明了集群中所有节点最终会保持一致性;
    
    · Follower和Candideta故障
        · 上面过程中,都只考虑的Leader故障的情况,
          Follower和Candideta故障的处理方式更加简单;
          如果两者故障,Leader发送的RPCs都会失败,Raft处理失败的做法是无线重试;
          如果期间崩溃的机器重启了,这些请求也就会响应成功;
          如果机器在响应成功后因为网络故障无法回复Leader,在重启后会不断收到重复的请求,但是因为Leader发送的请求都是幂等的,所以重试不会造成任何问题;
          例如一个Follower收到AppendEntries RPCs,但是它已经拥有了这个Entry,那么他会自动忽略这个请求;

    · 时间和可用性
        · Raft的要求之一就是安全性不能依赖于时间:整个系统不能因为某些事件运行速度产生错误的结果;
          但是可用性(集群可以及时响应celint的能力) 不可避免的会依赖于事件,
          例如 消息交换比服务器故障间隔时间长,Candideta没有足够长的时间来赢得选举;没有一个稳定的Leader,Raft会无法正常工作;

        · Leader Election是Raft中对时间要求最关键的方面;
          Raft如果想要正常的选举出一个稳定的Leader,必须要满足以下要求:
              广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障时间(MTBF)
          广播时间指的是 一个请求收发的时间;
          选举超时时间指的是 机器没有收到心跳消息进入Candideta状态的心跳定时器;
          平均故障时间指的是 对于一台机器而言两次故障之间的平均时间;
          
          广播时间必须是最小的,这样Leader才能发送稳定的心跳消息来阻止Folloer进入Candideta状态;
          选举超时时间要比平均故障时间小,否则机器很难感知集群中是否Leader的存活情况;

          广播时间和平均故障时间是由集群性能决定的,选举超时时间是由人为设定的;
          一般会选择10ms到500ms;
  

十一、Cluster membership changes
    · 上述都是在一个假设稳定的集群中做出的判断,但是实际应用中,有时候会新增/减少/替换节点;
      通过暂停集群运行来冷更新集群配置是一个选择,但是让一个大规模的集群停止运行一段时间并不是希望看到的;
      希望看到的情况是能够在集群正常运行的情况下,更新集群的配置;
    
    · 为了让修改集群配置时是足够安全的,那么在修改的过程中,不能同时存在两个Leader;
      首先先分析以下为什么会出现两个Leader,因为期望目标是热更新,所以需要将集群配置以Raft一致性为依据下发到集群中;
      假设一个原来3节点的集群需要扩展到5节点,基于多数派协议,[1,2,3,4,5]的日志被同步到三个节点就算提交成功;
      那么剩下的两个节点的配置仍然是[1,2,3],这时Leader 1故障了,集群重新选举,
      两个配置为[1,2,3]节点基于多数派协议选举出了Leader 2,这时Leader 1重启,这样另外三个节点也能选举出一个Leader 2;
      这样在同一时刻就出现了两个Leader,也就是脑裂现象;
    
    · 为了保证安全性,更改配置消息必须使用新的方法(共同一致算法,joint consensus);
      也就是说,C_new由client发送给Leader后会先将C_new和C_old组合为C_old_new;
      同时也会有以下规则:
          1. Entries会被同步给新旧配置的所有机器;
          2. 新旧配置的机器都可以成为Leader;
          3. 达成一致(选举和提交) 需要在两种配置上都获得多数派的支持
      还是拿上面的例子举例;
      C_old=[1,2,3],C_new=[1,2,3,4,5],C_old_new=[1,2,3,-,4,5];
      Leader不会同步C_new,而是会尝试将C_old_new committed;
      当Leader 1故障,开始选举;
      对于C_old_new的节点,必须要拿到3票,且至少两票来自1/2/3,那么C_old的节点中肯定不会选出一个新的Leader;
    
    · 共同一致允许独立的机器在不影响安全性的前提下,在不同时间进行配置转换过程;
      此外,共同一致可以让集群在配置转换的过程中依然能够正常响应客户端的请求;

    · 集群配置在AppendEntries中以特殊的entry来存储和通信;
      当Leader收到改变C_old为C_new的请求,会先将两个配置合并为C_old_new,并尝试向集群中复制该Entre;
      一旦一个节点将config log添加到log[]中,他就会用这个配置来决定后续的决定(不需要提交再应用,这就是config log特殊之处);
          (这样并不会影响正确性,因为C_old_new如果没有超过法定协议也不可能会引起脑裂现象)
      这意味着Leader会使用C_old_new来判断这条entry是否被提交,
      如果Leader崩溃了,选举的Candideta可能会使用C_old或者C_old_new,具体情况要看是否收到了C_old_new;

    · 一旦C_old_new被提交(注意提交也需要两个配置的半数以上同步,也就是说1/2/3中的两个,1/2/3/4/5中的三个),
      那么无论是C_old还是C_new,如果不经过另一个配置的允许都不能单独做出决定,
      并且因为Leader Completeness(C_old_new比C_old新) Leader只会从C_old_new配置的节点中选举出来;
      这时候Leader会创建一条C_new配置的entry并复制给集群,这样C_new变更就是安全的了;
      当C_new基于C_new多数派committed,旧的配置就变得无关紧要了,同时不使用C_new的节点就可以被关闭了;
      总而言之,就是禁止C_new和C_old同时做出决定,防止应用两个配置的节点群看起来就像两个小集群;
      使用C_old_new来作为两个配置的状态,并且在C_old和C_new之间因为Leader完整性原则杜绝了C_old节点群能选出新Leader,
      以及因为需要C_old节点群中多数派做出决定的规则杜绝了C_old_new单独决定Leader

    · 三个问题:
        1. 新的节点可能初始化没有存储任何entries,当这些entries加入到集群中,
           那么它们需要一段时间来更新,这时候还不能提交新的entries,
           为了避免这段时间的可用性的间隔时间,新的服务器被当作仅复制的节点(没有投票权);
           一旦新的服务器追赶上了集群中的其他机器,重新配置为正常节点;
        
        2. 集群的Leader可能不是新配置的一员,也就是说Leader可能会成为被移除的节点;
           在这种情况下,Leader会在提交了C_new之后退回为Follower,这意味着有一段时间它仍然管理着集群;
           这段时间复制日志不会把自身作为大多数之一;当C_new提交后,会按照C_new发生Leader选举;
          
        3. 移除节点可能会扰乱集群;
           被移除的节点不会收到心跳信息,所以这些节点都会进入Candideta状态;
           它们会向其他节点不断发送拥有新Term的RequestVote,这样会导致Leader不断的退位(因为收到了大于currentTerm的Term),然后新Leader被重新正确地选举出来;
           这个过程会不断的重复,Leader的频繁变换会验证影响集群的可用性;

           为了避免这个问题,当服务器去欸的那个当前Leader存在时,服务器会忽略RequestVote RPCs;
           也就是说会设置一个选举阈值时间,在这个时间内收到RequestVote会直接忽略,这并不会影响选举的正确性质,因为没有修改RequestVote的任何规则;
           换句话说,当节点收到这个RequesrVote时,不会直接变成Follower参与投票,而是启动一个阈值定时器后正常工作,在阈值定时器time out之前只要收到了来自Leader的请求,就会不参与这次投票;
           这样做只能减小所带来的影响,并不能彻底解决问题;
        
    · 单节点变更机制
        · 一次性变更过多的节点会复杂化过程,且核心原理就是在任意时刻都不会让C_old和C_new单独形成多数派;
          例如上面的过程,如果不引用C_old_new,就会可能一个形成3个C_new、2个C_old的局面,其中C_new和C_old都可以在自己的认知范围内形成多数派(3/5、2/3);
          引入C_old_new和新规则之后,就不会出现这种情况,原3个C_old中至少有两个C_old_new,这样C_old永远不可能形成多数派(1/3),只有C_old_new中会出现多数派(3/5);
          为了简化这个过程,可以采用单节点变更机制,即一次只变更一个节点;

        · 假设现在有四个节点1/2/3/4,要添加一个节点5,C_new一定会被应用在原C_old的Leader 3上,假设提交C_new后分布情况为[1,2][3,4,5];
          在C_old节点1/2的认知来看至少需要(4/2)+1=3个投票,但是基于Leader Completeness这是不可能实现的;
          假设还没有提交,C_old=[1,2,4],C_new=[3,5];
          假设C_old能获取到1/2/4的投票成为Leader;同时C_new因为此时最多只能获取两票无法形成Leader;
          假设C_new能够成为Leader;那么它可以肯定获取到了三票,此时C_old也会因为选票不够输掉选举;
          所以就不可能同时形成两个独立的多数派
            
        · 假设现在有五个节点1/2/3/4/5,要减少一个节点5,C_new一定会被应用在原C_old的Leader 3上,假设提交C_new后分布情况为[1][2,3,4];
          在C_old节点1/2的认知来看至少需要ceil(5/2)=3个投票,但是基于Leader Completeness这是不可能实现的;
          同时C_new节点有三个,可以选举出Leader;
          假设还没有提交,C_old=[1,2,4],C_new=[3];
          假设C_old能获取到1/2/4的投票成为Leader;同时C_new因为此时最多只能获取2票无法形成Leader;
          假设C_new能够成为Leader;那么它可以肯定获取到了三票,此时C_old也会因为选票不够输掉选举;
          所以就不可能同时形成两个独立的多数派
        
        · 假设现在有三个节点1/2/3,要增加一个节点4,C_new一定会被应用在原C_old的Leader 3上,假设提交C_new后分布情况为[1][2,3,4];
          在C_old的认知看来需要ceil(3/2)=2个投票,但是基于Leader Completeness这是不可能实现的;
          同时C_new节点有3=(4/2)+1个,可以选举出Leader;
          假设还没有提交,C_old=[1,2],C_new=[3,4];
          假设C_old能获取到1/2/4的投票成为Leader;同时C_new因为此时最多只能获取2票无法形成Leader;
          假设C_new能够成为Leader;那么它可以肯定获取到了三票,此时C_old也会因为选票不够输掉选举;
          所以就不可能同时形成两个独立的多数派
        
        · 假设现在有六个节点1/2/3/4/5/6,要减少一个节点6,C_new一定会被应用在原C_old的Leader 3上,假设提交C_new后分布情况为[1,2][3,4,5];
          在C_old节点1/2的认知来看至少需要(6/2)+1=4个投票,但是基于Leader Completeness这是不可能实现的;
          同时C_new节点有3=ceil(5/2)个,可以选举出Leader;
          假设还没有提交,C_old=[1,2,4,5],C_new=[3];
          假设C_old能获取到1/2/4/5的投票成为Leader;同时C_new因为此时最多只能获取1票无法形成Leader;
          假设C_new能够成为Leader;那么它可以肯定获取到了3票,那么C_old的选票最多只有2票;
          所以也不可能同时形成两个独立的多数派;
        
        · 经过上面的分析,可以判断出单节点变更的理论正确性,但是在实际的实现过程中会遇到一些可用性的问题;
            1. 变更过程中Leader切换影响正确性的问题
                · Raft单步变更过程中如果发生Leader变化会出现正确性问题,可能会导致已提交的Log被覆盖掉;
                  一个例子:
                  现有四个节点1/2/3/4,需要增加两个节点 x/y,先单节点增加4;
                  1/2/3节点中Leader为1,现在加入x,同步日志还没提交成功,节点配置情况为
                      (2/3/4):C_old=[1,2,3,4],  (1/x):C_new=[1,2,3,4,x]
                  leader 1宕机,基于上面的分析,1/2/3/4/x都可能成为leader,假设4节点为newLeader;
                      (4可以获得(4/2)+1=3的选票,也就是2/3/4的)
                  这时候加入节点y,同步并提交成功,配置情况为:
                      (3/4/y):[1,2,3,4,y],(1/x):[1,2,3,4,x],(2):[1,2,3,4]
                      (因为4作为Leader,在他看来集群中只有1/2/3/4/y节点,所以只需要同步到3个节点上就算提交,并且基于这个配置同步了正常日志)
                  此时leader 4崩溃,重新选举leader 1;
                  1会尝试继续同步本机配置[1,2,3,4,x]
                  并且可以同步到2节点,因为2节点在上一轮中并没有被写入;
                  这时候集群中的配置情况是:
                      (3/4/y)=[1,2,3,4,y],(1//2/x)=[1,2,3,4,x]
                  在Leader 2看来同步的人数满足了ceil(5/2)=3,可以返回[1,2,3,4,x] commited;
                  然后基于Leader的强领导性将(3/4)的配置log和正常日志覆盖掉,集群也只添加了x节点;

                · 为什么会出现这种情况?
                  根本原因在于上一任的Leader的成员变更日志还没有同步到多数派就宕机了;
                  没有被同步的节点可以作为一个多数派选举出一个没有新配置的节点作为Laeder;
                  这时候如果传入另一份新的配置日志,就有可能形成另一个多数派;

                · 怎么解决?
                  和Raft commit一样,
                  newLeader上任后立即会提交一份no-op日志,然后再同步成员变更日志;
                  这条no-op用于保证跟上一任Leader未提交的日志至少有一个节点交集
                  这样就可以保证上一任Leader重新选为Leader,进而阻止了脑裂;
                
                · 基于解决方法重新梳理流程
                  现有四个节点1/2/3/4,需要增加两个节点 x/y,先单节点增加4;
                  1/2/3节点中Leader为1,现在加入x,同步日志还没提交成功,节点配置情况为
                      (2/3/4):C_old=[1,2,3,4],  (1/x):C_new=[1,2,3,4,x]
                  Leader 1宕机,基于上面的分析4成为了newLeader;
                  此时C:[1,2,3,4],所以需要获得三个节点的支持
                  立即发送no-op,可能会直接覆盖掉(1/x)的C_X entry,这里按照没有覆盖,至少同步到了[1,2,3,4]中的三个节点也就是2/3/4;
                  这时候节点Y加入,同步提交成功,配置情况为:
                      (3/4/y):[1,2,3,4,y],(1/x):[1,2,3,4,x],(2):[1,2,3,4]
                  此时leader 4 崩溃,节点1因为没有覆盖所以lastLogTerm所以只能获得1/4的支持,无法成为Leader;
                  节点2最新的log为no-op,但是3/y最新的log为配置同步log,Index大于节点2,所以也不能成为Leader;
                  所以3/4成为Leader,继续完成同步[1,2,3,4,y]的工作,不会覆盖任何已提交的log;
            
            2. Raft单步成员变更的可用性问题
                · 也就是说从abc集群变成bcd集群;
                  其中涉及到两步,也就是增加一个节点 和 删除一个节点;

                · abc如果选择先变成abcd就很容易可能会出现集群不可用的问题;
                  在实际应用中替换操作一般出现在同机房替换;
                  当abcd状态出现了网络分区,也就是ad无法和bc通信,这时候如果Leader挂掉,就会出现无法选出新Leader的情况,因为必须要3票才能成为Leader;

                · 解决问题的方法很简单,那就是先减少再增加;
                  减少可能发生的区域性故障带来的影响;
            
          
十二、Log compation
    · Raft以log形式传递指令,这这意味着,如果不使用特殊的方法优化log[];
      一个机器的log[]会记录从他第一次运行开始所有的log,这对机器恢复和存储就是不小的开销;
      所以需要方法去能够安全的清楚log[]中的陈旧的信息;

    · 快照时最简单的压缩方法,使用快照,整个系统的状态都可以被写入到一个稳定的持久化存储中作为一个checkpoint存在,
      然后把那个时间点之间的过时日志全部清理;
    
    · 增量压缩的方法,例如日志清理或者日志结构合并树都是可行的;
      这些方法每次支队一小部分数据进行操作,这样就分散了压缩的负载压力;
      首先,它们先选择一个积累了大量已经被删除或者被覆盖的对象的区域,然后重写那个区域内活跃的对象,然后释放掉那个区域的日志;
      状态机可以使用与快照相同的接口,但是log cheaning需要调整Raft算法;

    · 每个服务器独立的为已经被提交的日志创建快照;
      主要的工作包括将状态机的当前状态写入到快照中;
      Raft还在快照中包含少量的元数据:
      last included index是最后被快照所替换的log entry的Index;
      last included Term是最后被快照所替换的log entry的Term;
      保留这些元数据是为了支持AppendEntries对快照之后的第一个Entry的一致性检查,
      因为整个条目需要前一个日志条目的Index和Term;
      为了支持Cluster Membership changes,快照还包括距离log included index最近的配置日志;
      一旦服务器完成了快照的写入,就可以删除从最后一个到last included index的所有entries 以及之前的快照;

    · 虽然每个节点都可以独立的产生快照,但是某些情况下Leader会向落后的Follower发送快照;
      这种情况会出现在Leader已经将需要发送给Follower的nextIndexEntry时,一般会出现在新加入节点或者某个节点非常缓慢或失联了一段时间;
      大多数的节点是能正常更上Leader的同步的;

    · InstallSnapShot RPC(安装快照RPC)
        · Leader会将快照分块发送给需要的Follower;

        · Leader会使用InstallSnapShot RPC来向需要的Follower发送快照;
          当Follower通过这个RPC接收到快照时,它必须自己决定如何处理对于已经存在的日志;
          一般情况下快照会包含没有在接收者log[]中存在的日志(基于Log Matching,自然也会包含接收者所有的Log);
          也有可能会因为网络问题收到没有包含完整log[]的快照,那么会删除快照包含了的entries,快照后面的entries仍然有效;

        · 快照的存在似乎背离了强领导性原则,因为Follower可以在不知道Leader的情况下创建快照;
          但是这并不影响Raft的正确性,因为快照只会创建已经被提交的entries,之前证明了被提交的entries不会被修改覆盖删除;
          
        · 参数:
            1. term:Leader的任期号,用于判断收到的PRC是否有效;
            2. leaderID:用于让Follower收到Client的请求时重定向到Leader;
            3. lastIncludedIndex:快照中包含的最后一条logEntry的Index;
            4. lastIncludedTerm:快照中包含的最后一条logEntry的Term;
            5. offset:分块在快照中位置的字节偏移量;
            6. data[]:从偏移量开始的快照的原始字节;
            7. done:如果为True,说明是最后一个分块;

        · 返回的参数只有Term,用于让Leader知道自己是否已过期;

        · Receiver implmentation
            1. 如果term < currentTerm,直接拒绝;
            2. 如果是第一个分块(offset==0) ,创建一个新的快照文件;
            3. 在指定偏移量向文件中写入数据;
            4. 一直接收数据并ACK回复,知道收到的RPC.done==False;
            5. 保存快照文件,丢弃掉现有或者部分更小的快照文件;
            6. 如果现存的enties中有与 (lastIncludedIndex,lastIncludedTerm) 相同的Index和Term,则丢弃前面的entries;
            7. 否则丢弃所有的log;
            8. 使用快照重置状态机,并加载快照中的配置日志;

        · 快照的性能
            有两个问题会影响快照的性能:
              1. 服务器需要决定何时进行快照,从两个极端来看,
                 如果每提交一个Entry就快照一次,这样带来性能上的开销太大了;
                 如果一个集群的生命周期快照一次,那么快照将没有任何意义;
              2. 写入快照可能会需要大量时间,我们期望是能在写快照是能够正常的接收新的Entry;

            针对问题一:可以设定让log[]大小达到一个阈值就创建一个快照;
            针对问题二:采用写时复制,如Linux的fork(),可以用于创建整个状态机的内存快照;


十三、客户端交互
    · Raft的Client会随机发送请求给集群中的节点,Follower会将请求重定向到Leader节点;
      如果Leader Down,请求会超时,Client会重新发起请求;

    · Raft的目标是线性化语义,一次操作会只执行一次;
      但是一个请求可能会执行很多次,例如:
      如果Leader在commit一个Entry,但是在响应client之前崩溃了,那么client会以为超时,会和新的Leader重试这条指令;
      解决的方法就是Client会对每个指令都打上唯一的序列号,如果一条指令已经被执行了,那就不会执行重新执行;

    · 只读操作不会通过log的形式记录下来,但是如果不加限制可能会读到脏数据;
      以为响应client的leader可能已经被其他newLeader取代了;
      所以需要使用两个规则来保证是线性可读的;
        1. Leader必须有提交了哪些Entry的记录,Leader Completeness保证了Leader拥有所有的committed Log;
           但是在Leader刚上任时,并不知道哪些是committed,所以需要每次上任时发起一次no-op,
           这样既能保证已提交的entries不会被覆盖,也能通过保证no-op已提交来确保之前所有的entries已提交;
           并且以此来更新nextIndex[]和matchIndex[];
        
        2. 在处理只读请求之前,Leader必须检查它是否还有效,Raft会让Leader发送心跳消息来保证;


十四、性能优化
    1. 调节参数
        · 心跳发送时间:过快会增加网络负载,过慢会导致感知Leader崩溃能力变弱;
        · 选举超时时间:如果不使用随机时间,可能会导致选票被重复瓜分,增长选举时间;
      
    2. 流批结合
        · 首先可以做的就是batch(批处理),也就是说维持一个buf,先将写请求缓存到buf中,然后再整个写入;
          对于Leader来说可以一次收集多个request,然后批次的发送给Follower;
        
        · 如果只是使用batch,Leader还是需要等待Follower返回才能继续后面的流程,所以还可以使用pipeline来进行加速;
          Leader会维持一个nextIndex[]来表示应该给每个Follower发送哪个Index的entry,通常情况下可以认为网络是稳定的;
          所以可以采用流式发送的方法来向Follower发送AppendEntries,不必等到Follower响应再跟新nextIndex[];

    3. 并行追加
        · 上面提到的流程中,Leader必须要先将Entry写入自己的log[]再发送给Follower;
          但是只需要保证集群中半数以上的节点写入成功就能算做Commited,所以可以在Leader写入时支持并行的发送AppendEntries,
          如果此时Leader没有写入就进入重新选举模式,因为已经保证了半数以上的节点拥有最新的entry,那么newLeader也肯定写入成功了;

    4. 异步应用
        · 一个Log被半数以上的节点append时就时committed,committed的enties无论何时应用到状态机都不会影响数据的一致性;
          所以当一个entry被committed,可以使用一个线程去异步的应用它;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值