本文大部分内容翻译自MIT 6.824 Lecture 5 note.
本文的核心是如何通过复制状态机(RSM, Replicated State Machine)实现容错服务。
如MapReduce或GFS中的master
目标是存在多个server,但表现在客户端上如同在跟一个server通信,并且允许一定的server宕机。
采取的策略是
- 多个server初始状态相同,每一个server都以相同的顺序执行相同的命令,从而保证所有的server的状态一致(RSM)
- 当一个sever宕机时,由另一个server接替服务。
分布式系统通常采用多机备份的方式来实现容错。但多机系统中,如果有多个服务器能够同时进行决策就可能产生冲突。这个时候就需要分布式一致性算法。人类社会中,人们会采用投票的方式来进行公平决策,通过少数人服从多数人的方式来做出最终的唯一决策。计算机系统借鉴了这种思路。Paxos分布式一致性算法就是通过多数人投票的方式来实现一致性。然而,Paxos算法偏于理论,并且不易理解。而分布式一致性算法的实现过程中会有很多的产生很多工程上的细节问题,不同人对此理解不一就会导致不同的实现,这些细微处的区别很有可能就违背了Paxos一致性的理论证明,从而导致算法的实现并不能保证正确性。而Raft提供了一种更"简单"的一致性算法,并再论文中对实现给出了具体的指导,方便了工程实现,已经被多个系统采用。
分布式系统一个核心的问题是,如果避免脑裂(split brain)现象?即同时存在多个server认为自己是Master并作出决策,从而导致冲突。
在分布式网络环境中,假定有A,B两台服务器。假如出现某个客户端能和副本A通信,但是不能和副本B通信。会有两种原因:一种是B确实crash掉了,此时我们希望副本A能够继续执行命令从而实现容错。然而还可能存在另一种可能的原因,网络可能由于某些原因(比如光缆被挖断了)出现分区现象,从而导致某些客户端只能与某一台服务器联通。此时,B仍然正常工作,但由于网络原因导致客户端和B的通信断开,此时客户端不应当执行命令,因为B可能仍然能与其他的客户端通信并执行命令,因此会出现脑裂现象。
举一个脑裂现象可能造成的问题,在一个分布式的key/value数据库中,C1和C2在不同的网络分区中并且和不同的服务器通信。
假定执行了以下三个命令
C1: put("k1", "v1")
C2: put("k1", "v2")
C1: get("k1") -> ???
如果是一个单机K/V数据库,我们看到的结果会是v2(线性一致性)。但是如果两个server由于网络分区独立为C1和C2服务,C1看到的结果将会是v1。
而在计算机系统中,通信连接的断开并不能区分出是机器crash还是网络分区(network partition)。因为这两者都会导致和一个或者多个服务器的通信失连。
我们希望实现一个复制状态机,能满足以下三个目标。
- 可用性,保持可用性即使由server crash (fail-stop)
- 能够处理网络分区导致的脑裂现象
- 如果由太多的机器fail掉,那么终止命令的执行,等待修复后恢复运行。
应对脑裂问题的一个很有效的思路是使用多数人投票。因为最多只能一个人获得多数人的投票。从而最多只存在一个master。注意,这里的多数人永远都是所有人中的大多数人,而不是存活中的大多数。
Raft
在lab3中,我们会尝试实现一个k/v数据库。整体的架构描述如下:
server使用raft来选举leader
客户端通过RPC与k/v层的leader通信(Put, Get, Append)
k/v层将请求forward到raft层,并不会直接响应客户端
leader的raft层将客户端的请求发送到所有的副本,通过AppendEntries
调用
每个follower都将它append到它自己的本地log中(此时并没有执行commit),并且回应leader
当majority将log记录到本地之后,entry会变成committed状态。大多数能够保证这条log不会被忘记,这条log在下一个leader选举的过程中一定会被看到。
当leader认为一条log状态变成commit,server将会应用client的操作。并且leader会回应k/v层。
使用log的目的是保证状态机的状态。但是使用log并不足以提供这种保证。
我们需要保证一下两点:
一 log是有序的
二 所有的follower必须都有相同的log
Raft协议的设计中有两个主要的部分
- 选出一个leader
- 再出现失败的情况下保证log的一致性
Leader 选举
Leader存在的目的在于保证所有的副本都以相同的顺序执行相同的命令。
Raft通过任期(term)来保证leader的顺序,每一个新的leader都必然会有一个心的term。但一个term不一定对应一个leader,因为可能出现选举失败的情况。通过任期可以保证服务器都能follow最新的leader,而不是之前已经被废弃的leader。
a) Raft 什么时候开始选举
AppendEntries
请求和心跳一起发送,并且leader不定期的发送心跳信息。
如果某个其他的server不能听到当前的leader的心跳,在一定的timeout时间后就会假设当前的server已经挂掉了发起选举。这个timeout时间称为election timeout。
此时,这个follower会将当前的term+1, 状态转换成candidate装填,并发出选举请求信息。这里可能会导致不必要的选举请求,此时原来的leader可能仍在正常工作。在这种情况下,原来的leader会继续正常工作并保持为leader。
b) 当一个server变成candidate的时候会发生什么?
有以下三种可能性
1)获得大多数人的投票,成为leader
2)没有获得大多数人的投票,听到另一个leader传来的AppendEntries
3)没有获得大多数人的投票,也没有听到另一个leader的AppendEntries
,此时会由于timeout而重新发起一次选举。
在第三种情况中,可能会出现term一直增加但不能增加log entry
,原因是此时它并不是leader。一旦网络分区恢复,会发生一次新的选举,因为存在值更大的任期。
但是,可能出现的情况是majority分区中的log更长,从而导致hign-term的选举被拒绝
或者majority分区中的log和minority分区中的log一样长,此时minority的high-term server可能赢得选举,但不会导致错误。
c) 怎么保证一个任期最多只有一个leader
leader必须获得大多数的server的投票,每一个server只能给一个任期投一票
这两条能保证一个term最多只有一个leader,且再少部分server fail时也能选出一个leader
d) 新的leader怎么通知其他server这个信息?
新的leader会立即发送AppendEntries
请求给所有的其他server,从而其他的server不会再发起新的选举
选举失败有两种可能的原因:一,不存在一个大多数的分区;二,两个candidate同时发起投票,从而导致不能达成大多数。
e) 选举失败了怎么办?
因为收不到心跳信息,从而会出现下一个election timeout,发起下一次选举。high-term优先,older term的candidate会终止。
f)如何设置选举的超时时间?
每一个server选择一个随机的选举超时时间,从而能够帮助避免split vote,即两个server同时发起投票。随机化能消除server间的对称。并且希望在下一次timeout时间之前,有足够的时间来完成选举。其他人会看到新leader的AppendEntries
心跳信息,从而不会转变为candidate状态。
Raft log
我们前面已经讨论过了leader怎么将log entries复制到其他的副本中。这里有一个重要的概念的区分:replicated entries 和 committed entries
committed entries能保证永远不会丢失
replicated entries却有可能被覆盖
a) sever的log会永远和其他副本保持一致吗?
答案是否,原因是有些副本会延时,从而导致副本之间可能会出现临时的状态是存在不同的log,好消息是他们最终会converge到相同的状态。commit机制保证了server只会执行稳定的entries。
b)leader可以复制旧的任期log并commit,而不commit最新的term的log吗?
答案是否。
看下面的例子:
- 初始状态中,s1是leader,但是s1还没有将term2的log复制到大多数就fail掉了。
2.此时s5变成了新的leader,term为3,增加了log但没有将log复制出去。
3.S1再次成为leader,此时term为4。S1尝试将旧的term为2的log复制到大多数的机器中。
此时,是否能直接commit term为2的log呢?
答案是否。 考虑以下场景
- term 2复制到了S3中,S1 commit了term2,因为term2已经复制到了大多数机器中。然后S1再一次fail。
- S5成为了新的leader,并且强制将它的log复制到其他的log中。此时会将S1的term2覆盖。从而违背了commit的log不变的原则。
这种情况的解决方案是,S1必须等到最新的log term4复制到大多数的server时才能commit掉term2和term4。这能够保证此时S5不可能被选举。
c) 什么时候Raft可以安全的覆盖掉其他log呢?
这些log必须没有被commit。覆盖log时可能导致其他副本中更长的log。