1 简介
Raft协议是一种共识协议。共识协议允许多个机器以相关关联的方式工作,并能够在部分成员宕机的场景下正常提供服务。
1.1 共识协议需要具备的特性
1)安全性
在非拜占庭场景下总是返回正确的结果,即可以容忍网络延迟、分区、丢包、重复和乱序。
2)可用性
只要多数实例可用,并且可以互相通信、和客户端通信,则服务应该是可用的。服务可能发生过重启,在重启过程中停止服务,并在停止完成后重新加入集群。
3)不依赖时序
不依赖时序来保证日志记录的一致性。错误的时钟和极端的延迟最多只能影响可用性。
4)性能不受少数成员影响
通常来说,只要集群中的大多数实例响应了,一个调用就可以完成。少数的失败不会影响整体的系统性能。
1.2 Paxos协议的两个缺点
1)非常难以理解
2)没有为构建实际的实现提供基础。
Multi-Paxos业界没有达到共识;
日志记录合并增加了复杂度,增加了复杂度;核心是各实例间角色对等,不适合共识协议的多决策场景。
1.3 raft增强可理解性的设计
1)问题分解
raft协议将共识协议分解成几个模块:leader选举、日志复制和安全性和成员变更。
2)简化状态空间
通过减少需要考虑的状态来简化状态空间,尽可能的使系统更加相关,减少不确定性。比如不允许日志空洞的出现,减少日志归于一致的路径(仅可复制leader的日志)。不过有时候引入不确定性也可以简化状态空间,比如raft使用随机的方法简单粗暴地在选举失败时快速放弃并选举。
2 leader选举
2.1 角色定义
raft协议包含三种角色:follower, candidate、leader。
follower:仅响应leader和candidate的请求;
leader:所有客户端的请求由leader处理。如果请求发到了follower,会被重定向到leader。
candidate:follower竞选leader的中间状态。
2.2 任期
raft根据角色状态将时间划分为不同的任期,在一个任期内最多只有一个leader,。
只有获得了大部分投票的candidate才能竞选成功,如果有多个candidate同时发起竞选,则可能出现都不能获得大部分投票的场景(split vote)。此时当前任期内没有选出leader,此时进入下一个任期重新选举。
2.3 选举过程
raft集群的稳定状态中,leader会周期性的向follower会周期的收到leader的心跳包,如果follower在超时时间内收到心跳包,则保持follower状态。
如果超时没有收到leader的心跳包,则增加任期号,投自己一票,并向其他成员发起竞选投票,投票后会有三种最终结果:
1)收到了大多数成员的投票,成为leader,并向其他成员发送leader心跳包;
2)收到了其他成员作为leader的心跳包,且心跳包的任期大于等于当前任期,则退回collower状态。注:任期小于当前任期的心跳包直接忽略;
3)超时未收到大多数成员的投票。此时等待一个随机的时间,然后增加任期号,重新发起新一轮的竞选。
一旦一个实例竞选leader成功,则开始服务客户端。
raft如何避免选出多个leader?
在一个任期内,一个follower最多只能为一个candidate投票,而candidate只有赢得了大多数选票才能竞选成功,所以不会选出多个leader
竞选结果如何快速收敛
为了避免反复出现因为多个candidate竞争出现竞选失败的问题,在一个split vote出现后,每个candidate等待一个随机的时间(如150~300ms之间的任何一个值)后重新发起竞选,以减少竞选竞争的概率,最终快速收敛竞选结果。
选举是否安全
安全指的是总是返回正确的结果,选举安全指的是每个被选出来的leader中均保存了所有已经commit的日志。
原因是follower收到的RequestVote请求中包含candidate的日志信息,如果follower发现自己的日志比candidate更新,就会投反对票。
判断日志更新的办法为比较index和任期:如果任期更大,则LogEntry更新;如果任期相同,则index更大的LogEntry更新。
3 日志复制
3.1 LogEntries
每一个客户端的请求都是一个修改状态机的命令,leader和follower均将这些命令保存起来,称为LogEntry。
以leader的最后一个LogEntry为例,一个LogEntry包含三个信息:当前leader的任期(2);命令(x<-4),LogEntry的index( 8)。
3.2 日志复制过程
leader会将这个命令LogEntry添加到日志记录中,并且向其他成员发送AppendEntries请求来复制这条LogEntry。
当大部分的成员将这条LogEntry添加日志记录中后,leader会执行LogEntry中的命令,并向客户端返回。AppendEntries中除了携带LogEntries信息,还会携带当前已经执行(committed)的LogEntry的index,follower收到请求后,会同时执行index之前的LogEntry。
leader会在后续持续向还没有复制这条LogEntry的成员发送AppendEntryes请求复制这条指令,直到所有的成员都复制了这条LogEntry。
历史任期未commit的LogEntry怎么处理
当leader决定执行一条LogEntry时,日志记录中的前序LogEntry也会被执行。这样的设计是为了解决如下场景中,已经复制到大部分机器上的日志被新的任期内的leader覆盖掉。(这里有个疑问,历史任期未commit的日志没有反馈给客户端,也会被提交,貌似不符合预期)
a)S1被选举为leader,任期为1,收到了一条请求,并发送给其他成员;然后S1挂掉,重启后重新被选举为leader,任期为2,收到了请求,并将请求日志复制到S2,然后S1挂了。
b)重新选举时S5收到S3、S4、S5的投票当选为leader,任期为3,并收到一个请求;然后S5挂了。
c)S1重启后,被S1、S2、S3选为leader,任期为4,收到一条请求
d)首先说明raft协议不是这样实现的。如果此时复制任期2的LogEntry, 复制到S3时,S1挂了;S5会被选为leader,并用自身的日志覆盖其他成员的日志,则2任期的日志就会被覆盖掉,而此时S2可能已经提交了。
e)raft协议的实现是,只向其他成员发送当前任期的的LogEntries(历史任期的LogEntries会因为被follower拒绝而同步到其他成员),即同步任期4的LogEntry,当大部分成员收到任期4的请求时,这条LogEntry被提交,同时2任期的LogEntry也被提交。
冲突日志如何处理
1)follower中不存在leader的部分历史日志
AppendEntries请求中携带了leader中新增LogEntries的前序LogEntry的index和任期,如果follower对比自己的日志发现没有一条LogEntry与该前序LogEntry的index和任期相同,则拒绝该AppendEntries请求。
如果AppendEntries被拒绝,leader会尝试使用前序LogEntry的前一条LogEntry发送AppendEntries,直到返回成功。
如果AppendEntries返回了成功,则leader中的历史LogEntries和当前请求的follower中的日志相同。
2)follower与leader存在日志冲突
follower中可能存在额外的LogEntry,raft的策略是用leader中的LogEntries覆盖follower中的LogEntries。覆盖的范围为最后一个相同的index和任期后的所有LogEntries。
3.3 日志compact
如果无限保存客户端请求日志,会存在可用性问题,也没有必要。raft将已经提交的LogEntries合并成一个快照,其中包含状态机的状态、最后一次提交的index和最后一次提交的任期。
4 客户端交互
4.1 总是向leader发起请求
客户端首次请求时随机向一个server发起请求,如果对方不是leader,则会返回leader的地址,客户端拿到信息后再次向leader发起请求。
如果leader挂了,请求会超时,客户端会再次随机请求一个server,直到请求到leader。
4.2 重试方案
leader可能会在commit和返回给客户端结果之间挂掉,此时客户端会再次请求,这样leader需要多次执行,造成不可预期的结果,如自增两次等等。raft为此制定了一个重试方案。
客户端请求中会带一个请求的序列号,如果是重试的请求,序列号相同,leader判断已经执行过,会忽略掉直接返回成功。
4.3 避免返回过期的数据
leader响应客户端的读请求时,自身的状态可能是过时的,因为可能有一个更新的leader被选中了,而自己还不知道。
raft通过两个措施解决这个问题:1)当一个leader首次当选时,会自动添加一个没有操作的LogEntry给自己和其他成员;2)在响应客户端的只读请求之前,先通过心跳向大部分成员确认自己的leader状态。
5 集群变更
某些机器需要替换,或者需要增加副本数,集群就会发生变更。
简单的处理方式是将所有机器下线,变更后再重新上线,但是这样会导致服务一段时间不可用。另外,这样会引入人工操作,增加了异常的概率。
raft使用了一种两阶段的方案,可以保持不停服更新,且不存在安全性问题。
5.1 两阶段变更方案
两阶段指的是不直接从旧的集群切换到新的集群,而是先切换到联合集群,以防止新旧两个集群选出两个leader。
开始切换时会向leader发送一个切换的请求
1)Cold,new提交前
leader收到切换请求后,会生成一条包含切换前和切换后的集群信息的LogEntry,即Cold,new,并向其他成员分发这条LogEntry。
无论这条LogEntry是否已经提交,所有的成员都会按照最后收到的集群信息进行投票,即leader也会以Cold,new为依据判断是否提交。
此时,如果leader挂了,则新的leader将会以Cold或者Cold,new的配置被选出来,即不会以Cnew的配置选出leader。
2)Cold,new提交后
此时只有包含Cold,new的成员才会当选新的leader(假如原leader没有宕机,原leader也包含Cold,new),这个时候就可以安全的配置新的集群信息Cnew到leader,并分发到Cold,new的其他成员。
3)Cnew提交后
此时仅在老的集群中的机器就可以下掉了。即使发生了leader宕机,新选出来的leader一定包含Cnew,按照Cnew决定是否提交日志。
从上面的流程中可以看出,没有一个时刻是两个集群可以独立选出leader的,即不会出现两个leader。
5.2 一些需要处理的case
1)新加入节点同步状态
新加入的节点需要一段时间来同步leader的数据,这样会导致切换的时间变长。
raft的方案是新加入的节点先以无选举资格的身份加入,状态同步之后,再开始集群变更。
2)旧的leader不在Cnew中,却还是leader
如果旧的leader不在Cnew中,当Cnew被提交后,旧的leader需要做一个额外的操作,就是降级为follower重新从Cnew中选出新的leader。
3)旧集群的节点对集群的影响
旧集群中不在新集群中的节点在Cnew提交之后还没有下线的时候,会退化为follower,由于leader不会向就的节点发送心跳,它们会在随机时间之后发起竞选,导致新的leader降级为follower重新选举,造成可用性问题。
raft的提供的方案是忽略收到心跳包之后最小重选超时时间之内的竞选请求(注:心跳包的间隔是最小重选时间的一半),即确信当前是存在leader,不需要重选的。
6 总结
本文首先介绍共识协议的特点,raft与paxos协议相比的优点,然后介绍了协议实现,包括对一些典型case的处理。