Raft协议
在分布式系统中一致性是很重要的。1990年Leslie Lamport提出基于消息传递的一致性算法 Paxos
算法,解决分布式系统中就某个值或决议达成一致的问题。Paxos
算法流程繁杂实现起来也比较复杂。
2013年斯坦福的 Diego Ongaro、John Ousterhout 两个人以易懂为目标设计一致性算法Raft。Raft一致性算法保障在任何时候一旦处于leader 服务器当掉,可以在剩余正常工作的服务器中选举出新的Leader服务器更新新Leader服务器,修改从服务器的复制目标。
概念
复制状态机 (Replicated state machines)
将集群中的每个服务器看做一个状态机,它们接收外部的指令,进行状态的改变,例如机器的存储器就可以看做是一个状态机。不考虑硬件错误,这样的状态机是一个确定型状态机,即只要初始状态和接收到的指令是确定的,那么它任意时刻的状态也是确定的, 这样以来,所谓保持分布式一致性即是保证集群中所有状态机的状态一致性。
角色:领导人 (leader)、跟随者 (follower) 和候选人 (candidate)
所有服务器结点划分为 3 种角色:
leader、follower、candidate
。一旦follower
在给定的时间内没有收到来自leader
的心跳包,它便认为leader
出了故障, 于是转变自己的身份为candidate
进行领导人的竞选
任期 (term)
raft
将系统时间划分为一个个任期term
,每一个任期都设置一个整型编号,称为任期号。每一个任期可以进一步划分为两个子段,其中第一个子段是选举期,第二个子段是任职期。选举期将竞选产生集群的领导人,若领导人选举成功,则进入了任职期,在任职期内只要领导人持续保持健康状态(即持续不间断地向其他跟随者发送心跳包),则这个任期可以无限期地持续。当然选举不一定都是成功的,可能存在没有任何候选人胜出的情况,这样会进行下一个任期,重新进行选举。raft
采用了特别的机制来尽可能地避免一个term
中没有任何候选人竞选成功的情形出现
日志 (log) 与日志复制 (log replication)
在
raft
协议中,指令是以日志的形式的传递的,虽然集群中有N
个结点, 但只有一个leader
结点接收客户端的请求(注意raft
本身是不涉及主从分离的,主从分离实现是业务代码实现的事)。其它所有结点接收来自leader
的复制日志Replicated log
,从而进行状态的变更,所有服务器结点按序执行相同的日志,从而保持状态一致性
日志提交 (commit)
leader
在接收到客户端请求之后,会产生一个相应的日志项log entry
,leader
不会立即执行这个指令,而是先会进行日志项复制,当日志项被成功地复制到集群中 半数以上 的结点后,领导人才会提交这个日志项,并执行其中的指令
Raft一致性算法
Raft
将一致性问题分解为了3个子问题:
leader
选举:leader
挂掉时,选一个新leader
,选举算法日志同步:
leader
在时,由leader
向follower
同步日志安全性:确保每一个状态机都以相同的顺序执行相同的指令
1. 主节点选取 Leader Election
当
follower
超过特定的时间没有收到leader
的心跳之后,follower
将变为candidate
,发起新一轮选举选举投票依据三个变量:任期(选主轮数)、数据同步进度(日志索引偏移量)、随机超时时间
每个结点拥有一个
currentTerm
变量,表示了当前的任期号,或者说选举轮数。发起选举时follower
会将自己的currentTerm
变量加一,然后向中其它结点发送投票RPC
请求。同时它也会将自己的选票投给自己,当候选人收到了过半选票时便选举成功为了避免多个节点都发起选举、大家票数一样的情况,每轮选举都有超时时间,且每个点的超时时间都是随机的
这样先超时的点将先进入下一轮选举,
currentTerm
就更大。其他candidate
碰见更大的任期将放弃参选
follower
会拒绝term
比自己小的master
的数据,避免脑裂
2. 日志复制 Log Replication
raft
将日志组织成线性结构,日志是由日志项串联而成的,同时会有一个索引来标识每一个日志项的相对位置主节点接收到命令后,写入日志,同步给从节点。收到半数以上
ACK
时,才提交每一个日志项可以用一个二元组来表征:
领导人对所有的跟随者维护了一个
nextIndex
列表,记录下一个同步给follower
的进度如果因为选主导致从节点同步进度滞后,从节点将会拒绝新数据,主节点会不断补发旧数据,直到追上进度为止
如果主节点落后于其他从节点呢?参见下面
3. 安全性约束 Safety
1. 领导者追加日志
领导者永远不会覆盖已经存在的日志条目
日志永远只有一个流向:从领导者到追随者
2. 选举限制:投票阻止没有全部日志条目的服务器赢得选举
为了保证主节点进度不落后于其他从节点的情况,
raft
限制那些进度落后的候选人,不允许成为新的领导人节点发起投票请求时,会带上自己最新的日志的索引和任期,收到该请求的结点会检查自身的日志是不是比候选人的更新
3. 永远不提交任期之前的日志条目(只提交任期内的日志条目)
领导人不允许直接提交任期号为之前任期的日志
4. 候选者和追随者崩溃
主向从同步数据时,可能因为节点挂了、网路波动收到不到从的回复
raft
的应对方案是简单的无限重试,为此需要保证结点之间信息传递的幂等性。比如直接忽略已处理的日志
4. 集群成员变更 Cluster Membership Changes
直接从一种配置转到新的配置是十分不安全的。可能会有的节点转新失败。
raft
采用了两阶段提交的方案首先当集群要进行扩容或缩容时,需要向领导人发送配置变更请求
领导人收到请求后会将旧配置和新配置取并集
C
,形成一个过渡配置,然后将该配置作为一个日志项广播给集群中的所有其它结点只有收到
C
的节点才有资格参与选主
领导人会再生成一个新的日志项, 该日志项中只含新配置,作为第二阶段的提交
上面的论述仅仅解决了更新配置过程中的安全性问题,但还有一些性能问题需要进一步分析。
首先第一个问题是当集群中有新结点加入的时候,它的历史日志是空的,因为可能需要耗费很久的时间来追日志
因此新节点加入后只同步主节点,不参与竞选。直到追平后才变为正常节点
领导人可能被下线,即领导人不在新配置中
raft
会把它变为从节点。由于系统没有领导人,所以一段时间之后会自发启动领导人选举,进而产生新配置下的领导人
更新配置之后,某个结点可能不在新配置中了,但如果它没关机,收不到心跳会触发选举,扰乱集群
raft
规定如果从节点能正常收到主的心跳,则无视其他点的选举请求被下线的节点收到新配置、发现自己被下线后,应停止发送数据
日志压缩
日志会随着系统的不断运行会无限制的增长,这会给存储带来压力,几乎所有的分布式系统(Chubby、ZooKeeper)都采用快照的方式进行日志压缩,做完快照之后快照会在稳定持久存储中保存,而快照之前的日志和快照就可以丢弃掉。
Client交互
Client
只向 leader 发送请求;如果向非 leader 发送请求,会被拒绝并重定向到 leader 节点Client
请求失败,会超时重新发送请求;
Raft
算法要求 Client
的请求线性化,防止请求被多次执行。有两个解决方案:
每个请求有个唯一标识;
Raft
的请求保持幂等性;
Raft 协议应用
redis 选主(采用了类似的做法,但不是直接应用
raft
redis
哨兵/集群模式下半数节点将主标记为客观下线才会触发选主,这和raft
/ZAB
一个从节点连不上就触发选主不同
kafka 2.8
抛弃了zk
,改用raft
,实现了个叫kraft
的zk
选举时整个集群不可用。一般选举很快,但也有慢的情况,例如碰上FullGC
分布式数据库:TiDB
服务发现:Consul、etcd
和 Paxos 区别
Paxos
侧重于大方向上理论,实现方案各家都不同。Raft
作者包含了很多细节,都有伪代码了。Raft
中,leader
必须存有全部commited
的日志。Paxos
则不一定,节点即使不包含所有日志也可能选上leader
,选上后再靠别的机制补日志。
参考
Last updated