首先呢 我们想来看一下什么是共识算法,他能做到什么?
共识算法
说到共识算法,就是一定会想到Paxos,说到Paxos,就一定会想到zookeeper(...)。
但是,Paxos实在是太难了
在大家都是单机应用的情况下,数据都是一个人说的算,完全不存在什么分歧,所以,也不需要什么共识算法。
但是,现在情况不一样了,为在发生网络抖动,服务器宕机,外星人入侵的情况下系统能持续提供服务,就不得不使用更多的节点进行冗余备份。
那这样,问题来了,只要数据存在多份,就一定会存在数据不一致的情况,而共识算法,就是为了能使分布式系统,在数据不一致的情况下仍能达成共识的算法。
三种特性
也是与其他共识算法不同的地方。
-
Strong leader
数据只能单向流动,由leader流向其他节点。
-
Leader election
Raft使用随机定时器去选取leader。
-
Membership changes
在集群配置变更时持续可用。
基本介绍
在raft集群包含有多个服务节点,每个节点在同一个时刻,只能是下面三种状态内的一个: 领导者(leader),跟随者(follower),候选者(candidate)。
最常见情况为:集群中只有一个leader,其他全是follower。而follower不会主动去发送request,都是被动接收来自leader和candidate的请求。
同时,客户端在于raft集群交互时,都是与集群leader建立连接。
Raft保证以下5个属性在任何时候都成立:
- Election Safety:在一个任期内,只会有一个节点被选举为leader。
- Leader Append-Only:leader永远不会删除或覆盖自己的log。仅支持追加。
- Log Matching:如果两个log列表中有一对log有相同的index和term值,那么在这两个log之前的所有log都相同。
- Leader Completeness:如果一个log在某个term内被提交了,那么他将出现在所有term大于该term的leader中。
- State Machine Safety:如果一个节点已经将一个log以当前index加入到自己的状态机中,那么其他机器不会再使用相同的index去添加其他log。
任期(term)
Raft把时间分成了一个个term,并且用连续的整数去标识每一个term。
当选举开始时,就标记着一个term的开始,如果有一个候选者(candidate)赢得的选举,那么他将成为一个任期内的leader。
选举(leader election)
Raft使用心跳机制来触发选举。
当服务启动成功后,默认为follower状态。随后,只要他可以持续的收到来自leader或candidate节点心跳,那么他就可以保持在follower中。
如果节点在election timeout内没有收到心跳包,那么他就认为当前集群内无可用的leader,并发起选举。
下面先看一个简略的选举过程:
- 发起节点将自己的term+1,并切换状态为candidate。
- 先给自己投一票。
- 给集群内其他节点发送请求,拉取选票。
- 如果获得集群内大多数节点的投票,则赢得选举。
- 切换为leader,向集群内其他节点发送心跳声明。
哈哈哈 这样看这个选举的过程是不是存在很大漏洞?
问题 1
当集群内leader挂掉后,所有节点都收不到心跳,那么,他们不是都会变成candidate,并为自己拉票?
答:很简单,我们让每个节点的`election timeout`都不一样(e.g., 150–300ms)。
这样,在leader挂掉后,可能只有少数几个节点先超时并发起选举,他们只要在其他节点超时前选出新的leader就可以了。
问题 2
如果多个节点都去发起选举,那么剩下的节点如何去决定投票呢?
答:首先,每个节点,在一次选举中,只能投出一票。
另外,至于投票给谁,基于first-come-first-served原则。
简单的说,就是谁先向我拉票,我就投给谁!
问题 3
投票这么草率,能保证选出来的leader拥有所有已提交的数据吗?
答:哈哈哈,其实上面的过程给简化了,投票一点也不草率。
首先,想要成为Leader,必须拥有集群内大多数节点的投票。
其次,参与投票的节点,是有权拒绝给某个候选者投票的!
在候选者去发起拉票请求时,其请求内会带有自己拥有的数据信息,如果参与投票的节点发现自己的信息比候选者还要up-to-date,那么,他就会拒绝本次投票。
日志回放(log replication)
当leader产生后,随后所有客户端与raft交互都是通过该leader节点。
client发请求给leader,并被leader的回放状态机(replicated state machaines)执行,将该请求加入到自己的log entiy中。
随后leader会并行向集群内的其他节点同步该请求。
当集群leader确认当前请求以被大多数节点确认后,会将执行结果返回给client。
来自客户端的请求会被raft包装为一个个带有序号的日志实体(log entries)。其中,每个log entry中还记录有他被创建时的任期编号。
用来保证log在不同节点间的一执行:Raft设计的log机制提供了一个更高级别的抽象。其log机制有如下特征:
- 如果两个在不同log列表的中log具有同样的index和term,那么其中存储的command一定是相同的。
- 如果两个在不同log列表的中log具有同样的index和term,在其之前的所有log都是相同的。
在通常情况下,集群中leader和followers的log数据都应该是一致的。但是,如果发生了leader宕机,就可能发生不一致的情况(还没有将所有的log entry完成回放)。
因为这种不稳定的情况,follower中可能会出现leader中没有的log entry,也可能follower中缺少了一些log entry。
在raft中,为了处理这种可能发生的不一致的情况,leader会强制follwer复制自己的log entry。
- 首先,leader会找出与两者间log匹配的最大index。并删除follower上所有大于该index的log。
- leader向follower发生该log index之后的所有log entry。
- leader会储存所有follower上的nextIndex值,以记录同步进度。
集群信息变更(Cluster membership changes)
在实际使用中,我们偶尔需要去变更集群的一些信息,例如替换集群中的节点或修改集群冗余因数。 尽管我们可以先将集群停止,变更配置然后再重新将集群启动。但是,如果采用停机变更的话,在变更期间整个集群都是不可用的!
首先,在变更期间,我们需要保证不会在同一个任期内被选举出两个leader。不幸的是,集群变更不可能在一瞬间就同步到所有节点上。所以,集群在变更期间会被分成两个独立的群组。
配置变更将分两步进行:
- raft集群会先将要配置变更达成一个中间状态(joint consensus)。当joint consensus提交后,系统将会去过度到新的配置。joint consensus会将新老配置进行合并。
- log entry会同步给新老配置分组内的所有节点。
- 无论是新老配置的节点,他们都有可能成为leader。
- 共识(不论是选取节点还是提交log)分别都需要双方新老配置组的大多数节点投票。
配置变更信息使用特殊的log entry存储。
- 当leader收到请求要将配置从Cold转换为Cnew后,leader会生成一个中间状态(joint consensus)Cold,new。
- leader将这个中间状态Cold,new同步给集群中的其他节点。
- 当节点收到并储存这个中间状态Cold,new后,后续的日志处理都将采用该配置(配置永远都使用最新的,不管是否提交)。
- Leader生成一条新的log entry,其内容是新成员配置Cnew,将该log entry写入本地日志,并同步给Follower。
- Follower收到新成员配置Cnew后,将其写入日志,并且从此刻起,就以该配置作为自己的成员配置,并且如果发现自己不在Cnew这个成员配置中会自动退出(上图红色箭头处)。
- Leader收到Cnew的多数派确认后,表示成员变更成功,后续的日志只要得到Cnew多数派确认即可。
- Leader给客户端回复成员变更执行成功。
当中间状态Cold,new被提交后,不管是Cold还是Cnew都无法独自做出决定。
并且,当前leader挂掉后,只有持有Cold,new的节点才能被选举为新的leader。