S++

千线一眼

导航

分布式算法 —— Raft算法

分布式算法 - Raft算法

Raft算法

算法简介

    不同于Paxos算法直接从分布式一致性问题出发推导出来,Raft算法则是从多副本状态机的角度提出,用于管理多副本状态机的日志复制。Raft实现了和Paxos相同的功能,它将一致性分解为多个子问题:Leader选举(Leader election)日志同步(Log replication)安全性(Safety)日志压缩(Log compaction)成员变更(Membership change)等。同时,Raft算法使用了更强的假设来减少了需要考虑的状态,使之变的易于理解和实现。


学习推荐

raft.github.io

Raft动画



角色

角色划分

    Raft将系统中的角色分为领导者(Leader)跟从者(Follower)候选人(Candidate):

  • 领导者(Leader):接受客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后告诉Follower提交日志。
  • 跟从者(Follower):接受并持久化Leader同步的日志,在Leader告之日志可以提交之后,提交日志。
  • 候选人(Candidate):Leader选举过程中的临时角色。


角色的状态转换

    Follower只响应其他服务器的请求。

    如果Follower超时没有收到Leader的消息,它会成为一个Candidate并且开始一次Leader选举。

    收到大多数服务器投票的Candidate会成为新的Leader。

    Leader在宕机之前会一直保持Leader的状态。

    Raft算法将时间分为一个个的任期(term),每一个term的开始都是Leader选举。

    在成功选举Leader之后,Leader会在整个term内管理整个集群。如果Leader选举失败,该term就会因为没有Leader而结束。



Leader选举

    Raft 使用心跳(heartbeat)触发Leader选举。当服务器启动时,初始化为Follower。Leader向所有Followers周期性发送heartbeat。如果Follower在选举超时时间内没有收到Leader的heartbeat,就会等待一段随机的时间后发起一次Leader选举。

    Follower将其当前term加一然后转换为Candidate。它首先给自己投票并且给集群中的其他服务器发送 RequestVote RPC,结果有以下三种情况:

  • 赢得了多数的选票,成功选举为Leader;
  • 收到了Leader的消息,表示有其它服务器已经抢先当选了Leader;
  • 没有服务器赢得多数的选票,Leader选举失败,等待选举时间超时后发起下一次选举。

    选举出Leader后,Leader通过定期向所有Followers发送心跳信息维持其统治。若Follower一段时间未收到Leader的心跳则认为Leader可能已经挂了,再次发起Leader选举过程。



日志同步

    Leader选出后,就开始接收客户端的请求。

    Leader把请求作为日志条目(Log entries)加入到它的日志中,然后并行的向其他服务器发起 AppendEntries RPC复制日志条目。

    当这条日志被复制到大多数服务器上,Leader将这条日志应用到它的状态机并向客户端返回执行结果。

    某些Followers可能没有成功的复制日志,Leader会无限的重试 AppendEntries RPC直到所有的Followers最终存储了所有的日志条目。

    当系统(leader)收到一个来自客户端的写请求,到返回给客户端,整个过程从leader的视角来看会经历以下步骤:

  • Leader追加日志条目
  • Leader并行发布AppendEntries RPC
  • Leader等待多数的回应
  • Leader将条目应用到状态机
  • Leader回复Client
  • Leader通知Follwers应用日志

    日志在某个节点状态如下:

    不难看出,logs由顺序编号的log entry组成 ,每个log entry除了包含command,还包含产生该log entry时的leader term。从上图可以看到,五个节点的日志并不完全一致,raft算法为了保证高可用,并不是强一致性,而是最终一致性,leader会不断尝试给follower发log entries,直到所有节点的log entries都相同。



安全性

    如果节点将某一位置的log entry应用到了状态机,那么其他节点在同一位置不能应用不同的日志。简单点来说,所有节点在同一位置(index in log entries)应该应用同样的日志。但是似乎有某些情况会违背这个原则:

Raft算法-7

阶段A:S1成为Leader,在term2提交的日志赋值到了S1、S2两个节点;

阶段B:S1下线了,重新进行Leader的选举,S5成为了term 3的Leader;

阶段C:S5尚未将日志推送到Followers就离线了,S1重新上线后被选举为term 4的Leader,此时S1会将自己的日志同步到Followers;

阶段D:S1又一次下线,触发一次选主,而S5有可能被选为新的Leader(term = 5 > 4且有最新的日志),然后S5会将自己的日志更新到Followers,于是S2、S3中已经被提交的日志(2,2)被截断了。

为了避免上面的情况,添加以下限制:

  • 拥有最新的已提交的log entry的Follower才有资格成为Leader

    这个保证是在RequestVote RPC中做的,Candidate在发送RequestVote RPC时,要带上自己的最后一条日志的term和log index,其他节点收到消息时,如果发现自己的日志比请求中携带的更新,则拒绝投票。日志比较的原则是,如果本地的最后一条log entry的term更大,则term大的更新,如果term一样大,则log index更大的更新。

  • Leader只能推进commit index来提交当前term的已经复制到大多数服务器上的日志,旧term日志的提交要等到提交当前term的日志来间接提交(log index 小于 commit index的日志被间接提交)。



日志压缩

    在实际的系统中,不能让日志无限增长,否则系统重启时需要花很长的时间进行回放,从而影响可用性。Raft采用对整个系统进行snapshot来解决,snapshot之前的日志都可以丢弃。

    每个副本独立的对自己的系统状态进行snapshot,并且只能对已经提交的日志记录进行snapshot。

Snapshot中包含以下内容:

  • 日志元数据。最后一条已提交的 log entry的 log index和term。这两个值在snapshot之后的第一条log entry的AppendEntries RPC的完整性检查的时候会被用上。
  • 系统当前状态。

    当Leader要发给某个日志落后太多的Follower的log entry被丢弃,Leader会将snapshot发给Follower。或者当新加进一台机器时,也会发送snapshot给它。发送snapshot使用InstalledSnapshot RPC。

    做snapshot既不要做的太频繁,否则消耗磁盘带宽, 也不要做的太不频繁,否则一旦节点重启需要回放大量日志,影响可用性。推荐当日志达到某个固定的大小做一次snapshot。

    做一次snapshot可能耗时过长,会影响正常日志同步。可以通过使用copy-on-write技术避免snapshot过程影响正常日志同步。



成员变更

什么是成员变更

    成员变更指的是系统成员变化,即服务器节点的上下线,这和由于宕机故障导致的上下线是不同的。宕机或者重启导致的上下线,是不会影响系统的注册的成员数量的,也就不会影响到一致性判断所依据的“多数派”的生成,众所周知,“多数派”是所有一致性的基础。成员变更时,会修改注册的成员数量,比如在实际应用中,为了提高安全等级,就很可能出现需要把备机数量由三台扩充到五台,在这种情况下,就发生了成员变更。


Raft成员变更实现

    Raft提出了通过一个中间过渡阶段,即联合共识(joint consensus),逐步把数据写入的新的集群中。其具体做法是2阶段提交式的:

第一阶段:先写一条{Cold, Cnew}同步到新旧两个集群的多数派,写入这条日志后,系统中的任何写入请求,都要同步到Cold和Cnew两个集群的多数派才算写入成功

第二阶段:当{Cold, Cnew}同步成功后,再写一条Cnew,同步给新集群,然后就可以完成切换了。

    这个过程看起来比较复杂,其实从宏观来理解,可以把Cnew看作原有集群的一个热备,在联合共识阶段,一个请求要写入原有系统和热备,之后,不论{Cold, Cnew}这条日志是否写成功到多数派,不论热备出现了何种故障,原有系统会一直保证数据是一致的。当写入Cnew到多数派备机成功后,就保证了新集群和热备有了一致且完备的数据,在这种情况下,新集群就可以接替原有集群工作了。



posted on 2022-07-05 16:39  S++  阅读(173)  评论(0编辑  收藏  举报