课程主页: https://pdos.csail.mit.edu/6.824/schedule.html
本节课是介绍Raft共识算法的第一部分, 建议阅读论文, 如果要做Lab的话, 论文是一定要看的, 尤其是要吃透论文中的图2。
1 引入: 脑裂(split brain)
脑裂的定义:
当系统中的节点因为网络故障而无法相互通信时,可能会发生脑裂现象,即系统被分割成两个或多个互不连通的子集,每个子集都可能认为自己是完整的系统。
在介绍raft算法之前, 课程首先总结了之前课程中的几个分布式应用程序的共性和潜在的问题:
MapReduce: 主节点管理Map和Reduce任务的派发和信息收集, 自始至终没有发生变化GFS: 主节点管理Chunk信息、租约管理等, 主节点通过日志的形式避免宕机后信息的丢失VMware FT: 通过一个原子测试完成Primary和Backup之间的切换
以上讨论的问题都是主节点失效后系统能否正常工作的场景, 看起来VMware FT可以满足在一个服务器宕机后剩下的服务器也能正常运行的需求, 但这里仍然存在问题: 其原子测试本身也是一个Test-and-Set服务
下图为课程中展示的案例:

因为Test-and-Set服务本身也算是一个分布式应用, 其也存在主从备份的概念, 因此会出现上图中描述 的场景:
Primary和Backup之间的网络出现问题, 都认为自己是唯一的服务节点Primary与Test-and-Set Server1之间网络正常, 与Test-and-Set Server2之间网络不正常;Backup与Test-and-Set Server1之间网络不正常, 与Test-and-Set Server2之间网络正常Primary和Backup的Test-and-Set测试都成功了, 此时存在2个上线的服务器, 这就是一种脑裂现象
以上的问题是一种脑裂现象, 因为Primary和Backup之间出现了网络分区, 每个区域之间不能彼此访问。
补充:另外二者对脑裂现象的处理:
MapReduce中没有提及解决方案GFS的主节点会将关键的元数据信息存储在持久化的操作日志中,这个日志会被复制到多个地方,确保其可用性。如果发生网络分区,那么分布在不同分区的节点可能无法与主节点通信,但GFS的设计允许系统在某种程度的网络分区后仍然能够继续操作,只要分区中有足够的副本存在。
2 多数投票(Majority Vote)
以上的问题在于不同的网络分区并不知道自己是否在别的网络分区工作的同时也在工作, 为了解决这个问题, raft算法提出了Majority Vote: 任何决策必须要求过半的节点的投票支持。这是Raft最基本的一个概念, 而且这个概念还有一个重要的前提: 节点数量必须是奇数
因为如果是奇数的节点构成的分布式应用, 至多只有一个数量过半的网络分区, 因此至多只有一个网络分区能够完成正常的决策工作, 避免了脑裂。同时, 这个机制还表达了raft的容错能力: 至多允许一半的节点异常。
3 raft的运行案例
课程中为了展示raft在实际应用中如何工作, 给出了lab3中将实现的kv存储的例子:

- 客户端请请求(
Get或Put)发送给Leader的服务层 Leader的服务层将这个Get或Put命令向下发送到Raft层Raft层将操作封装成日志发送给多个Follower的Raft层Follower的Raft层复制日志后回复Leader的Raft层收到过半节点回复后, 向上发送一个通知到应用程序表示真正的执行这个操作了。Leader的服务执行命令改变自身Table的状态, 并回复给客户端执行结果Follower的应用层也应用Raft层的日志改变自身Table的状态(这里需要Leader告知哪些log是commited, 通常是通过心跳告知, 图中未画出, 而且这个操作通常有延迟, 客户端不需要等待这个操作完成)
接口函数说明
Start函数
应用程序通过Start函数将一个操作传递给Raft层,返回值包括,这个请求将会存放在Log中的索引(如果成功commit的话)applyCh channel
当之前请求的操作commit之后, 通过applyCh channel向应用程序发出消息通知, 应用程序读取这个消息。
具体而言, 客户端调用应用层的RPC,应用层收到RPC之后,会调用Start函数,Start函数会立即返回,但是这时,应用层不会返回消息给客户端,因为它还没有执行客户端请求,它也不知道这个请求是否会被Raft层commit。只有在某一时刻,对应于这个客户端请求的消息在applyCh channel中出现, 应用层才会执行这个请求,并返回响应给客户端。
4 Log
Raft中的Log会被添加到节点的Log数组中, 每一个Log包含了以下内容:
- 具体的命令, 取决于应用
- 任期
Term, 用于判断这个Log是否有效
Leader节点需要维护每个Follower的Log状态:
- 目前已经复制的
Log在数组中的索引 - 下一个追加的
Log应该从哪个索引开始
同时Leader也需要维护全局的日志状态:
- 目前已经提交的
Log在日志数组中的索引
Log有3大作用:
- 用于
Leader和Follower之间的同步 - 包含命令用以改变应用程序的状态
- 每个
Raft节点都需要将Log写入到它的磁盘中, 因此Log还可以被Raft节点用来从头执行其中的操作进而重建故障前的状态
重启后Leader如何利用Log进行恢复?
- 首先重启后进行
Leader选举,其中一个节点被选为Leader Leader通过心跳来确定目前已经提交的日志的索引位置- 在这个索引位置以前, 所有的节点的日志是相同的, 可以应用到自身的状态机
- 在这个索引位置以后,
Leader迫使其他所有副本的Log与自己保持一致
5 Leader选举
5.1 选举机制
每一个运行的raft节点都包含一个任期号(Term), 每个任期只能有一个Leader。
如何保证每个任期只能有一个Leader?
首先, 每一个运行的raft节点只能是如下几个角色之一:
LeaderFollowerCandidate
每个Raft节点都有一个选举定时器, 每次收到心跳RPC或追加log的RPC(都通过AppendEntries)时, 都将重置这个选举定时器, 在选举定时器没有到期时, 发出AppendEntries的节点是Leader, 接收AppendEntries的节点是Follower, 一旦因为没有接收到消息导致选举定时器到期, 节点转化为Candidate并进行投票选举。
每一次选举会增加Term,通过RequestVote向其他节点寻求投票, 由于节点数量是奇数, 而投票选举成功要求过半的投票, 因此只有一个Leader将在一个特定的Term内选举成功。
拿到过半投票的Leader将通过心跳告知新Leader的诞生, 如果没有这样的节点, 则再次自增Term进行新一轮的选举。
5.2 避免选票分裂
因为每个Candidate都会给自己投票, 因此如果所有的Follower同时转化为Candidate并发起选举, 结果一定是各自一张投票, 形成了选票分裂(Split Vote), 并且恶心循环地再次进行新一轮选举。
为了避免这种情况,不同的服务器都应该选取随机的超时时间。先超时的节点有更大的概率获得更多的选票,因此避免了多次选票分裂的情况。
PS: 选举超时的设定要求: 至少大于
Leader的心跳间隔, 否则该节点会在收到正常的心跳之前触发选举
5.3 Raft未考虑的情形: 单向网络故障
以下的情形会导致Raft不能正常工作:
Leader可以发出心跳,但是又不能收到任何客户端请求Leader的心跳会抑制其他服务器开始一次新的选举
以上情形将导致客户端请求永远不被执行, 教授提出的解决方案是:通过一个双向的心跳解决:
Follower需要回复心跳- 如果
Leader一段时间没有收到自己发出心跳的回复,将让出Leader