分布式共识算法
为什么需要分布式共识算法?
集群很容易就能保证可用性,我们只要保证健康节点立即就能响应即可,但是如何保证最终一致性?或者说抛弃可用性保证一致性?如何保证分区容错?这就是分布式共识算法解决的问题。
保证一致性无非就是:同步,半同步,异步复制。但是网络的不稳定性以及机器故障等问题给一致性带来其他的挑战。
Paxos算法
由兰伯特提出,所有共识算法的“祖宗”。随着发展,主要分为两大类:
- Basic-Paxos:拉伯特提到的算法
- Multi-Paxos
拜占庭将军问题:多个将军攻打拜占庭,需要多个同时攻打才可成功,在有内奸,不可靠的情况下如何保证单个将军下发的决策是正确的。(答案是根据大多数将军的共识采取决策)
Paxos中的三种角色(实际过程中一个节点可以有3种角色的功能):
- Proposer:提案者,发起提案 提案=编号(版本号)+Value(值)
- Acceptor:接受者,批准提案 P提出提案,A负责批准提案,L学习已经接受的提案
- Learner:学习者,学习以及批准通过的提案
如集群内,两个请求分别对不同节点分别同时向不同的节点写入x=1和x=2,这时候的集群应该接受谁的请求?这两个节点接收到请求后就成了P,向集群中其他节点(看成A)发起提案。
Paxos推导流程:
- 单个A:能够很轻易达成共识,多个提案都由A审批,不需要考虑共识问题。但是中心化的缺点很明显。
- 多个A:同一轮决策内,一个A接受和批准多个方案,多数(半数以上)A对某个方案都进行批准才算通过。==>显然同一轮决策应该只能批准一个方案才行。
- 多个A:但是同一轮只能批准一个方案。但是没有考虑宕机恢复的情况,某个机器宕机恢复导致延迟的提案同意应答回复慢,可能出现多提案同时推行
- 多个A:某个提案被拒绝时返回成功批准的提案,让提出提案的知晓本轮通过的提案,从而撤销他提出的提案,下轮再提交。
Paxos算法流程
分为Prepare,Accept,Learn三大阶段
Prepare:提出提案
根据当前节点提案编号,生成新提案编号,并通知其他节点,寻求接受该提案编号。
发送给大多数节点,且接受到同意也是需要大多数节点。
接受提案编号的节点:
- 如果已经接受当前编号,且已经执行了提案,就返回提案值和编号。
- 如果只是接受过编号,就返回错误,表示拒绝。
- 如果新编号小于当前节点最新编号,可以忽视。
- 如果新编号大于当前最新编号则接受该编号,响应为同意。
Accept:接受阶段
某个节点的提出的提案收到大多数节点的同意后,进入Accept阶段。
提出提案的节点:
- 如果有一个节点返回了最新提案值(value)和编号,就将返回的提案值(value)发给大多数节点,自己的提案值放到下一个决策回合。===>这个时候可能出现的情况是多数的宕机节点恢复,某个宕机节点收到了新请求,这个时候在帮助其他恢复节点完成同步数据。
- 如果多数节点同意,就将自己的新提案+编号同步给其他节点,只要有一个节点在这个阶段拒绝后,就停止后续发送,等待下一个决策阶段重写提出提案。
接受提案的节点:
- 如果当前提案是第一个提案,必须接受。
- 不是第一个提案,需要比对是否大于等于当前最新经过prepare阶段的编号,不是就拒绝即可。 **为什么是大于等于?**某些节点可能因为网络原因没有prepare阶段。 **为什么不是已经经过prepare阶段的编号,而是最新的?**这种情况会使得系统变得非常复杂,如果我们考虑接受已经经过prepare阶段的编号:如已经接受有编号1,编号2,这个时候因为网络原因某个或某些节点始终没有接受到编号1的数据,但是接收到了编号2的数据,我们还需要考虑编号1的数据该怎么办解决?同时要还需要考虑一系列故障可能性下带来的问题,考虑这些问题后我们还要考虑这样设计是否会违背我们系统设计的目标。
Learn:学习阶段
这个阶段主要是节点将自己已经接受的提案发送给其他的学习者,类似于主从。
- 发送给所有学习者
- 发送给一个学习者,他负责通知其他学习者。
- 发送给部分学习者,他们负责通知其他学习者。
Paxos的缺陷
B节点收到节点A的prepare阶段的请求,他自己的编号+1,接着收到了新请求,它也进入到prepare阶段,B节点的提案编号和A的提案编号都被接受,同时进入到了接受阶段(同步数据数据),A发现所有同步数据请求都被拒绝了,立即开启新的prepare阶段,B也因此被拒绝,B也开启了新的prepare阶段,如此往复,两个节点始终无法完成accept阶段。
解决方案就是只允许一个提出提案的角色存在即可,于是就变成了中心化的主从架构。
Raft算法
Paxos算法存在缺陷,后续发展中诞生了变种算法:Multi-Paxos,Multi-Paxos算法只允许一个提案者(leader)存在,但是如果leader宕机,会退化到Basic-Paxos算法选出leader,极端情况下还是会出现问题。现在共识算法实现最多的是:Raft。Raft可以看作是Multi-Paxos的变种版本,集群内部只有一个leader,leader负责处理所有的请求。
复制状态机:只要节点一样起始状态一样,经过一样的操作后,所得的状态也一定一样。
每个节点只要实现复制状态机,保证初始状态一样,然后在不同节点间进行日志同步,就能保证各个节点数据一致。
Raft算法简化Paxos的状态,Paxos中每个节点既可以是Proposer,也可以是Acceptor和Learner。在Raft中每个节点身份只有一种:
- leader:处理客户端请求,封装日志同步给follower
- follower:接收日志,并同步数据 不复制处理请求,收到请求重定向到leader节点。
- candidate:leader宕机时,follower会成为candidate,经过选举后成为新的leader。
身份状态的变化也只有leader宕机时才会出现切换,其他时候都固定不变。
首次启动时,都是follower,经过一轮选举后,会选出新的leader。当leader宕机后,follower检测到leader出现宕机,再次进行选举就会得到新的leader,在新的任期里面,leader会将自己的最新日志同步给所有的follower,最终达到一致。
Raft算法要解决的问题:
- 领导选举:选举出leader
- 日志复制:将主节点的操作日志复制到从节点,保证数据一致性。
- 安全性:保证集群内只有leader,以及一些选举leader的安全事项。
领导选举
心跳机制:只能由leader发出,follower节点需要返回响应。
- 维持领导地位,当超过一定时机follow没有收到leader的心跳检测请求,说明follow下线,可以开启新的选举。
- 确定其他节点状态,确定故障或者存活。
任期(term):leader下线后,候选者会根据前一个leader任期,生成下一个任期编号,并进行拉票。
- 心跳包会携带任期,每个节点会维护一个任期编号,会检查心跳包任期是否大于或等于当前任期 大于:大于就更新之间任期,重写认主。 小于:抛弃心跳包或返回最新任期都可以。
leader下线,其他节点检测到没有心跳后,节点立即投自己一票,然后向其他节点发送拉票,若其他节点没有投过票,就会同意本次拉票,否则拒绝投票。
**随机计时器:**每个节点在一定时间内没有收到心跳包后,不是立即成为候选者,而是有一个倒计时,倒计时结束还没有收到其他节点的拉票,才会立即成为候选者,投自己一票,然后拉票。
- 计时器一般在150ms-300ms之间
只有拉到大多数票,才会成新的leader,然后向其他节点发送心跳包,这个时候选举就结束了,如果都没有拉到大多数票,意味本轮没有选出新的leader,意味着会继续超时,进行新一轮选举。
- 没有选出新leader:会开启新选举。
- 选举成果后新主宕机:没有心跳包,会超时,也会开启新选举
- 旧主复活:会收到新主心跳包,成为追随者
- 网络原因,某个节点超时开始选举:该节点也许会成为leader,即使这样不影响运行,但是因为网络问题其数据大概率不是最新的,raft安全性会尽量保证最新节点成为leader。
日志复制
新主上线后,负责处理来自客户端的请求,还要负责将日志同步给其他follower。
客户端如何找到新leader:
- 轮询集群中所有节点
- 重定向
- 健康检查:向所有节点发起探测,找到leader
**日志复制:**每个操作会封装成日志记录,日志记录中还会包含任期编号,以及一个日志号。
当日志成功同步到多个follower后,leader才会真正执行操作,leader执行完操作会通知其他follower也执行对于日志,总共两个请求:一个是写日志,一个是提交数据,这个提交的通知封装在心跳包内,心跳包内会包含以及应用的最新日志编号。
不稳定网络下的,多条日志的顺序性如何保证?(一致性检查机制)
leader在发送写入日志的请求时,会包含上一条成功执行的日志的任期+日志号,节点会根据这个信息确定顺序。
安全性
日志复制会因为网络不稳定以及节点的宕机恢复等存在着诸多问题等待解决,这就是安全性的目标。
- 节点宕机一段时间后,再次上线,表现为缺少数据
- leader在收到大多数日志同步成功后,立即写入新数据,但是还没有通知同步就宕机了,然后一段时间后又恢复了,表现为某个不是最新任期内多出了数据。
- leader只通知了一个节点写入最新数据就立马宕机,这个节点任期内多出数据。
- 某个节点写入最新数据后,主节点和该节点立马宕机,一段时间后该节点又恢复,可能表现为多出一些莫名其妙的任期数据。
leader上线后,会同步自己的日志到所有节点,因此尽量保证选举出来的leader的数据一定要最新,否则写入成功的新数据,客户端会出现读取不到的情况。防止新上线节点成为leader,出现丢失数据的情况。
- 选举拉票时会携带自己以及成功写入的日志的最新编号和任期,收到拉票请求节点会根据当前节点数据的新颖程度决定是否拒绝,如果还没有自己的新就直接拒绝。
- 比对规则:先比对任期,任期越新数据越新,任期一样比对日志编号,日志编号越新数据越新
考虑这样的情况:
- 首先Leader1,写入数据到一部分少数节点,然后宕机
- 然后L2上线,由于大部分节点没有同步到数据,即使L2不是最新数据也能上任,L2接收到数据,写入数据到L2节点后立马宕机。
- L1恢复,再次竞选成为新Leader,继续同步此前写的数据,然后又宕机。
- L2恢复,由于自己最新数据任期比较新,成功成为leader,同步数据就出现了覆盖L1成功写入的数据。
为了解决上述情况:raft要求新上任不允许以历史任期提交数据,而是以当前最新任期进行一个提交。历史没有完全同步的就用当前任期进行提交。
其他机制
用日志表示操作,每次宕机重启恢复数据速度太慢,新节点到达同步的时间也会比较久。日志压缩基于当前内存建立快照,然后舍弃快照之前的日志。
- 需要保留最后一条日志
集群成员变更带来的脑裂开问题。
- 集群允许动态的新增成员或减少成员
- 脑裂:集群内出现了多个leader
主要照成脑裂的原因是新增节点后,原来的follower没有即时感知到新增节点,leader就掉线,在新的选举轮次中有的节点由于不知道新的总节点数量变化了,选举过程出现了多个leader。
联合共识变更机制
- 阶段1:通知所有节点要发生变更 将变更后的新集群节点信息(新旧去并集)封装发给所有follower 大部分节点收到后,leader提交阶段1对于日志
- 阶段2:多数节点收到变更消息后再正式变更 将正式新集群节点发给所有follower,follower发现自己没有被包含在内就主动退出,大多数都收到这个新集群节点后,变更就结束了。
为什么这样就能解决闹裂问题?
- 阶段1没有成功就发生掉线:这里会在旧的集群内进行选举
- 阶段1提交完毕:大多数节点都提交了阶段1,选举得到的leader只可能出现在提交了阶段1的节点