分布式一致性算法的作用
在一个单机系统中,要维护数据的一致性是相对简单的,但是在分布式系统中,数据一致性维护就会变得特别复杂,要实现分布式系统的数据一致性,至少需要考虑以下情况:
延迟和网络分区:
在分布式系统中,节点之间的通信可能存在延迟和不可靠性。
节点故障:
分布式系统是由多个节点组成的,节点故障及从故障中恢复是必须考虑的情况。
并发操作:
多个客户端可能同时对数据进行读写操作。
扩展性和性能:
随着业务的发展,分布式系统通常需要进行水平扩展以应对不断增长的数据和负载。
不同节点的视图和状态:
在分布式系统中,不同节点可能因为各种原因,导致不同节点之间的视图和状态是不一样的,例如一个由A、B、C、D、E五个节点组成的系统,A节点可能认为A、B、C、D正常,E故障,B节点认为A、B、C、E正常,D、E故障等
所以为了解决分布式系统的一致性问题,需要有合适的分布式一致性算法来,常用的算法有Paxos 算法、Raft算法、Zab 协议、Gossip等等,下文主要讨论Raft算法的实现。
Raft算法的历史:
Raft算法是由 Diego Ongaro 和 John Ousterhout 在2013年提出的,他们在斯坦福大学发布了一篇论文《In Search of an Understandable Consensus Algorithm (Extended Version)》,其中详细介绍了Raft算法的设计和原理。
Raft算法的设计目标是提供一种易于理解和实现的分布式一致性算法,相较于当时已经存在的Paxos算法,Raft算法更注重可读性和可调试性。Ongaro 和 Ousterhout设计Raft的初衷是为了让分布式系统的一致性算法更加容易被理解和广泛应用。
leader及其产生过程
为了简化分布式系统的并发操作及延迟等问题的解决场景,Raft算法不像Basic Paxos算法那样允许任意节点发起写操作,在Raft中,必须选举出一个leader,负责维护全局的数据,其他节点的数据均来自于leader,他们只能被动地接收leader给他们的数据,总的来说,在Raft中,Leader是分布式系统中的一种角色,负责协调其他节点的操作和维护数据的一致性。Leader节点是唯一的,并由Raft算法中的Leader选举过程产生。
下面是Raft算法中Leader的产生过程:
初始状态:
在系统启动时,所有节点都处于初始状态,没有Leader节点。
每个节点都有一个定时器,定期触发选举。
选举超时:
当节点的选举超时定时器触发时,节点会开始一个新的选举过程。
节点将自己转变为候选者(Candidate)状态,增加自己的任期(term)并向其他节点发送选举请求(RequestVote)消息。
请求投票:
候选者节点向其他节点发送请求投票消息,请求其他节点支持自己成为Leader。
请求中包含候选者的任期、候选者的最后日志条目的索引和任期号等信息。
接收选票:
接收到请求投票消息的节点会进行投票决策。
如果接收节点还没有投票给其他候选者,并且候选者的任期大于自己的任期,那么就将投票给候选者,并重置自己的选举超时定时器。
选票计数:
候选者节点等待从其他节点收集选票。
如果候选者获得了大多数节点的选票,那么它将成为新的Leader。
如果候选者在选举过程中收到来自其他节点的心跳消息,表明已经有Leader存在,那么候选者将转变回跟随者(Follower)状态。
成为Leader:
当候选者收到大多数节点的选票后,它成为新的Leader节点。
Leader节点将发送附带空日志的心跳消息,以维持其领导地位。
其他节点在接收到Leader的心跳消息后,将转变为跟随者状态,并更新自己的选举超时定时器。
通过上述过程,Raft算法中的Leader节点经过选举过程产生,并负责协调系统中的操作和维护数据的一致性。Leader的选举过程确保了系统中只有一个Leader存在,并提供了容错和高可用性的机制。
Raft的基础日志结构
如下图,下图的每一列都是模拟raft里的一次数据写入:
其中,各字段含义如下:
term: 代表产生这一列数据的leader的任期
index: 日志内容序号,全局唯一且递增(不要求步长相等,只要保证递增即可)
content:日志内容
每一次leader的记录产生,都必须保证以下几点:
- term[i] >= term[i-1],即产生数据的leader的任期不能回退,这样能够保证在出现网络分区故障恢复后,已经被卸任的旧leader(旧leader此时还不知道自己已经卸任)的脏数据影响全局数据。
- 当term[i] = term[i-1]时,index[i]>index[i-1],这一点不难理解,index代表日志序号,自然是只能递增的,但是需要注意的是,这里有一个前提条件是term[i] = term[i-1],即leader没发生过变更的情况下,index[i]>index[i-1],当leader发生了变更,这个时候可能上一任leader分发下来的值尚未得到过半数的节点的确认,因此是存在index[i]<=index[i-1]的情况的,具体场景我们下面再展开说。
当然了,实际在做工程实现时,除了上述的结构外,还需要根据实际情况添加其他的数据结构内容。
一次从 故障 到 故障恢复的过程例子
我们假设集群有5个节点(记为节点A->节点E,节点A是leader),集群一直运行正常,现在各节点的数据均如下:
term |
1 |
1 |
1 |
1 |
index |
1 |
2 |
3 |
4 |
content |
data1.1 |
data1.2 |
data1.3 |
data1.4 |
此时,节点A收到新的写入请求,但是在往各节点分发数据时宕机,从而使此一时刻集群中的数据不一致,集群中各节点的数据如下:
节点A(已故障)
Term |
1 |
1 |
1 |
1 |
1 |
Index |
1 |
2 |
3 |
4 |
5 |
Content |
data1.1 |
data1.2 |
data1.3 |
data1.4 |
data1.5 |
节点B(节点A故障前,已往节点B成功分发data1.5)
Term |
1 |
1 |
1 |
1 |
1 |
Index |
1 |
2 |
3 |
4 |
5 |
Content |
data1.1 |
data1.2 |
data1.3 |
data1.4 |
data1.5 |
节点C->节点E(节点A故障前,未往这些节点分发数据data1.5)
Term |
1 |
1 |
1 |
1 |
In dex |
1 |
2 |
3 |
4 |
Content |
data1.1 |
data1.2 |
data1.3 |
data1.4 |
此时,进入第二阶段,各节点在超时时间内没有收到leader的心跳数据,判断leader节点发生故障,开始进入leader选举阶段。
假如这时节点D先发起投票请求将自己作为leader,那么我们看下各节点的处理情况,并最终确定节点D是否能成功作为集群的新leader
在展开模拟之前,先给读者们再说明一下raft算法中,其他节点(或称跟随者节点)是怎样决定能否把票投给候选者的:
在Raft算法中,当一个候选者节点请求投票时,其他节点需要根据一定的规则来决定是否将选票投给该候选者。以下是节点确定是否投票给指定节点的条件:
候选者的任期(term)
节点会比较候选者的任期与自己的当前任期。
如果候选者的任期大于等于节点的当前任期,节点才会考虑投票给该候选者。
如果候选者的任期小于节点的当前任期,节点会拒绝投票。
候选者的日志更新情况
节点会检查候选者的日志更新情况,以确定候选者是否具备更完整和更新的日志。
如果候选者的日志长度和最后一条日志的任期号都比节点的对应日志更长或更新,则节点会倾向于投票给该候选者。
如果候选者的日志不如节点的完整或更新,节点会拒绝投票。
投票限制:
节点只能投票给一个候选者,如果节点已经投票给了其他候选者,则不能再投票给新的候选者。
如果节点已经投票给了某个候选者,并且该候选者的任期大于等于节点的当前任期,节点会拒绝投票。
总结起来,节点会根据候选者的任期、日志的更新情况以及自身的投票限制来决定是否将选票投给指定的候选者。这些规则确保了选举过程的安全性和正确性,防止任期冲突和多个领导者的出现。只有满足一定条件的候选者才能获得多数节点的选票,并成为新的领导者。
在了解了投票规则后,那么我们看看对于节点D的提议,各节点的处理逻辑:
节点A:已故障,无法参与处理
节点B: term(节点B) = term(节点D),index(节点B) > index(节点D),节点D的日志不如节点B的新,投反对票
节点C:term(节点C) = term(节点D),index(节点C)= index(节点D),节点D的日志和节点C一样新,投赞成票
节点D:作为候选者,投票给自己,票数+1
节点E:term(节点E) = term(节点D),index(节点E)= index(节点D),节点D的日志和节点E一样新,投赞成票
因此,经过上述的投票,节点D获得3个赞成票,1个反对票,超半数赞成,因此节点D当选为新的leader
在节点D当选后,即进入故障恢复环节,如上文所述,此时集群中数据是不一致的,上一任的leader(节点A)故障时留下了未被大多数节点确认的脏数据,在raft算法中,因为始终会以leader的数据为准,所以在节点D当选后,集群的数据均变为
Term |
1 |
1 |
1 |
1 |
Index |
1 |
2 |
3 |
4 |
Content |
data1.1 |
data1.2 |
data1.3 |
data1.4 |
亦即节点B中已写入的index=5,data1.5的数据会被清理(由于data1.5未经大多数节点确认,对外部请求方来说,这次的写入请求是异常的,因此data1.5存在与否都被视为合理的)。
此时,集群的数据恢复一致,正常对外提供服务。
最后大家也可以尝试思考一下:如果是节点B先发起的将自己作为leader的请求,那么节点B能否当选?当选后data1.5的数据是否有效?