0. 写在前面的话

  一直从事分布式对象存储工作,在分布式对象存储的运营,开发等工作中,数据一致性是至关重要的。因此想写一篇关于分布式一致性的文章。一来,可以和大家分享。二来,可以提高自己的文字提炼能力也可以当作备忘。

  本篇文章并不是raft的一篇科普文,不着重介绍raft的具体过程,这些具体过程raft论文中都详细阐述,在此不再赘述,而是着重于raft中选举以及日志复制过程如何保证数据的一致性的阐述。

    #如果对raft不了解的同学,建议搜索raft论文译文+原文对照看

    #分享的都是个人体会和思考的过程可能不是很严谨      

    #不求全,可能有些细节会被省略,但求能够把raft最主要的东西说清楚

1. 什么是分布式一致性协议

  #分布式:多个节点互为副本,副本之间可以是平等关系也可以是主从关系

  #一致性:保证各个节点上的数据,只要是提交的数据一定是保证一致的

2. 为什么需要分布式一致性协议

  在分布式对象存储系统中,数据的安全性是通过多份冗余副本的保证的,这就要求系统能够保证多份副本上的数据一致,比如对于对象存储系统的master管理着整个集群的副本关系,block状态等元数据信息。为了防止master单点,一般会采取一主多从的方式,而在存储系统中由于故障导致的数据迁移,空间回收等场景都会导致元数据发生改变,这些元数据的改变如何同步到其他备机上对提升整个分布式的数据安全性有着至关重要的作用。

3. raft是如何做到保证数据一致性

  raft作为一种比较好理解的分布式一致性算法(相对于paxos来说啦,其实要理解还是有点难度的!),是通过选举机制,日志复制来保证系统数据的一致性

  笔者认为,raft协议理解的难度并不在于raft定义的那些操作,而是在于raft定义的操作和raft系统遵守的原则之间的关系,以及为什么系统遵循这些原则就能保证数据的一致性呢?所以刚开始了解raft协议时给人的感觉就是比较散乱而不清晰。

  因此下面将会着重试着从raft操作是如何保证系统始终遵守这些原则之间,以及这些原则为什么能保证数据的一致性来阐述

  #raft基本概念:

    提交的日志索引(commitIdx):已经同步至大部分节点的log的下标,表明数据是安全状态,是不会再被修改的日志数据。

    任期(term):相当于系统的逻辑时钟,每一个任期中只能有一个leader,可以理解为系统每进入一轮选举时任期+1。可以把任期想象成一个朝代,一朝天子一朝臣,leader就是这朝的皇帝,从节点就是这朝的臣子。term是理解raft的一个关键点。

    日志索引(logIdx):raft日志索引是一个的下标连续&单调递增的,例如(1,2,3,4,5,..,n,n+1)。

    状态机执行日志索引(applyIdx):发送给状态机(是个抽象的概念可以是对象存储系统,也可以是其他的系统)执行的日志下标。

  

  #raft基本操作

    #选举:

      功能描述:

        请求其他节点为自己竞选leader投票,超过半数节点投票给自己就会转化成leader节点

      发起方:

        角色:候选者

        操作名称:RequestVote

        操作参数:

          #当前最新的日志条目索引(当前日志的term+logIdx)【规则b会用到】

          #当前的term号 【规则a会用到】

          #自身的ID,用于告诉选民我是谁

      响应方:

        角色:所有

        响应规则:

          a.请求方term大于自身的term。(小于的话,就好比生在新中国的我们来了个古代人来参选主席,你说你会答应吗?)

          b.请求方的日志条目包含自身的日志条目。(通过笔记两方的日志条目索引来判断,后面会介绍为什么这种判断可以保证)。对方知道的东西还不如你多,说要当你领导你干吗?)

          c.在同一个任期内只能投一次票,多个竞选者按照先来先得的投票原则。

          在上面a,b,c条件都满足的情况下,会投出神圣的一票给对方,否则不投。

    #日志同步:

      发起方:

        功能描述:

          #leader把客户端写到leader的日志的条目复制给从节点

  ·         #在leader上任时会进行一次主从数据的同步(可以理解为当权者掌权后清除异己,主把从上和自己不同的记录都给删掉,直到保持一致!)

          #充当心跳报文,维持leader的存在,抑制从节点进入竞选。(leader刷存在感的方式)

        发起方:

          角色:leader

          操作名称:appendEntrtries

          操作参数:

            #当前任期:term

            #entries[]:日志数据数组,记录将要复制给从节点的日志条目

            #prevLogIndex / prevLogTerm:entries[0]日志的前一个日志对应的logIdx / prevLogIndex对应日志条目的任期号(很多raft介绍文章是:最新日志之前的日志的索引值,但是本人参考raft原文以及逻辑推理觉的并不是最新日志之前)

            #leaderId

            #CommitIdx:leader上已经提交的日志索引

          内部维护的数据结构: 

            #nextIndex[]:维护每一个从节点下一次需要复制的日志条目索引数组

      响应方:

        角色:非leader角色

        响应规则:

          a.进行term检查,如果term小于自身,拒绝更新日志,直接返回False。(上一届领导的命令肯定不能听)

          b.进行一致性检测:如果在prevLogIndex处的日志的任期号与prevLogTerm不匹配时,返回 false

          c.如果一条已经存在的日志与新的冲突(index 相同但是任期号 term 不同),则删除已经存在的日志和它之后所有的日志。

          d.添加任何在已有的日志中不存在的条目

          e.更新commitidx,如果主的commitidx>自身的commitidx,则 自身的commitidx = 主的commitidx。(本人认为在实际raft系统中由于满足领导人完全原则所以不会存在从的commitidx>主的commitidx情况)

  #raft遵循的原则

    #领导人只增加原则:领导人永远不会覆盖或者删除自己的日志,它只会增加条目

    #领导人完全原则:再一个任期提交的日志一定,出现在任期更大的领导人日志中。

    #选举安全原则:一个任期内最多只能有一个leader

    #日志匹配原则:如果两个日志在相同的索引位置上的日志条目的任期号相同,那么我们就认为这个日志从头到这个索引位置之间的条目完全相同

    #状态机安全原则:如果一个服务器已经将给定索引位置的日志条目应用到状态机中,则所有其他服务器不会在该索引位置应用不同的条目

  #raft和遵循原则的之间的因果关系

     #领导人只增加原则:

      raft系统中日志的修改来自两方面,1.appendEntrtries,根据appendEntrtries的响应描述可知,日志可以append,删除修改。2,客户端日志写入,是append操作

      在raft的appendEntrtries操作定义中响应方是非leader,所以leader只能介绍客户段的append日志操作-->原则得证

   #选举安全原则:

     在选举操作响应规则c中规定在同一个任期内只能投一次票

      证明:同一个任期内只能投一次票:则这个任期中的投票次数和节点个数相等的(2N+1),其中大多数票投个A,成为leader,B就不能得到大多数的投票。

    #领导人完全原则:

      在选举的操作中,响应规则b,要求候选者的日志条目包含自身的日志条目,才可以投票给该候选者。

      一个候选者得到大多数的节点的投票才能成为leader -->leader的日志能够包含大多数节点(Node_Majority_set)的日志条目,并且当前leader的任期一定大于Node_Majority_set日志中的term。

      证明:  

      反证法:假设存在一条日志已经提交了但是在Node_Majority_set_1中不存在节点包含这条记录

        由于已经提交的日志是已经存在于大多数节点(Node_Majority_set_2)中的日志,Node_Majority_set_1&Node_Majority_set_2 != NULL,因此必定存在节点包含这条记录,所以得出矛盾,结论正确

      因此已经提交的日志条目一定包含在Node_Majority_set_1的节点中(不一定全包含,可能是部分节点),而前面的论证leader的日志能够包含大多数节点的日志条目(节点已提交的日志条目是所有日志条目的子集),所以新leader的日志一定包含所有已经提交的日志条目。

   #日志匹配原则:

      命题:如果两个日志在相同的索引位置上的日志条目的任期号相同-->该日志索引处前面的索引上对应的日志条目完全相同

      根据appendEntrtries响应规则中b的描述:在在prevLogIndex处的日志的任期号与prevLogTerm不匹配时,返回 false

      归纳证明:初始化时所有的节点的在LogIdx=0处的任期号自然相同。

      当LogIdx=N处的任期号相同时,appendEntrtries leader同步复制 LogIdx = N+1的日志时,会比对LogIdx=N的进行任期相同的比较,如果相同会写入 LogIdx = N+1的日志条目,由于所有的从节点复制来自同一个主节点所以任期相同

      因此归纳证明可得结论

    #状态机安全原则:

      首先发送给状态机的日志必须是已经提交的日志,如果一个日志log1已经提交,那么在该日志的索引位置处不会存在另一条log2已经提交但是和该日志条目不一样的日志

      假设存在log2已经被提交,说明在log2处的索引的日志在大多数节点保持一致,同理log1在log1索引处的日志条目在大多多数节点保持一致。

      (log1_idx == log2_idx) 所以必然得到 log2 == log1

  #这些原则为什么能保证数据的一致性

    # 领导人只增加原则+选举安全原则:保证raft系统中最多只有一个leader,并且日志复制只从leader单向流动到从节点(个人任务系统命名为raft中文漂流,是不是就是因为日志复制是单向的流动的呢)。

    # 领导人完全原则:能够保证新的leader中包含所有已经提交的日志,已经提交的日志是不会再修改的,从而保证新的leader产生也不会对已经提交的日志产生修改操作。

    #根据日志匹配原则可以保证如果两个日志在相同的索引位置上的日志条目的任期号相同-->该日志索引处前面的索引上对应的日志条目完全相同:

     在appendEntrtries操作的响应规则c规定如果一条已经存在的日志与新的冲突(index 相同但是任期号 term 不同),则删除已经存在的日志和它之后所有的日志。然后复制eader的同步的日志条目,和leader保持一致。

     如果不冲突:根据日志匹配原则可以判断前面的日志一定也是和leader保持一致的,把新的日志条目添加后,和leader保持一致。

  综上所述:这些原则能够保证系统中最多只存在一个leader,而且leader包含之前任期的所有已提交的日志条目,日志条目只从leader流向从节点,在主从日志同步阶段能够保证日志的一致。

  

  

    

  

 

 

  

05-11 20:11