主节点选举
每个节点有三种角色:
follower: 从节点,被动回复leader和candidate的request。
leader: 主节点,处理client的写request
candidate: leader的候选人
follower 会接受 leader 的心跳包,如果没有收到 leader 的心跳包,将会在随机时间之后变成 candidate 开始竞选 leader。同时选举出来的新主节点也是通过发送心跳来让其余的节点从 candidate 状态转换为 follower 的。
投票前的准备
-
每个节点有一个固定的ID以在多个节点之间进行识别。
-
每个节点有一个任期号 term,从零开始,以后逐渐单向往上递增。服务器重启后需要知道当前的term 才可以正确的跟其它节点交流,所以 term 是必须持久化的.
-
每个节点都可以向其他节点发起投票,要求其他的节点选择自己作为主节点。
-
为了避免所有节点同时发起投票,每个节点会分配一个随机的选举超时时间 electionTimeout,也就是一个等待时间,等待时间过了之后才可以发起投票。所以每个节点发起投票的时间都是不一样的。这样降低了提示投票导致选票被瓜分的情况。等待时间超时之后继续随机一个等待时间并开始下一轮选举。
开始投票
那么当一个节点的等待时间到了之后,该节点会转换为 Candidate,
- 先给自己投一票,并自增terms, 自增 term 表示当前任期内我已经投过票了(投给自己),所以任期结束。重置选举超时计时器 electionTimeout。
- 通知其他节点,要求他们选择自己,通知的时候需要带上自己的 term,ID,最新的日志 Index 。
投票节点收到投票请求的回复
有2种情况
-
成功了。拿到一个节点的选票。
-
失败了。此时要将自己的 term 更新为对方回复的 term。
而每收到一个节点的回复,要判断是不是以下情况:
-
当前是否已经获得了半数以上节点的票,是的话赢得选举,成为leader;向其他节点发送心跳,宣誓主权。
-
没有获得半数以上节点的票。electionTimeout 时间到了,在超时之后自增当前任期 terms,重新随机一个 electionTimeout 并开始下一个任期的选举;
当然在这个过程中有可能发生:收到其他节点的心跳请求,说明leader已经选出了,那结束选举,自己变成 follower,选举结束;
节点收到其他节点的投票请求
-
如果对方的 term 小于自己的,则拒绝投票,并将自己的 term 返回给对方,让投票的人跟上时间(更新自己的term);如果大于,则立刻转为 follower,并返回成功,告诉对方我已经成为你的 follower 了。
-
如果1的情况都不符合,说明2者的任期相同。判断当前任期内是否已经投过票了。是的话也要拒绝投票,因为同一任期内不能多次投票。
-
如果步骤2中还没有结果,此时再比较对方的数据和自己的哪一个更新(根据日志的最新时间),如果对方没有自己新,也要拒绝投票。
-
上述条件都满足,将票投给对方,返回成功。并等待 electionTimeout。
最终一定会有一个节点胜出。
当选出了主节点之后,主节点会跟从节点都保持心跳,从节点每次收到心跳都要重置自己的等待定时器,这样只要主节点跟从节点之间不失联,从节点就永远不会发生选举。
而一旦失联,其余的从节点立刻就根据自己的等待时间 electionTimeout 再次开始选举了。
上述过程中,主节点就是 leader, 从节点就是 follower,从节点失去主节点的心跳开始竞争选举的阶段就是 candidate。
整个过程中有三种RPC:
- RequestVote RPC:投票用的RPC
- AppendEntries RPC:主从保活的心跳RPC(同时同步也用这个RPC)
- InstallSnapshot RPC:主节点给落后太多的从节点发送快照。
整个过程有三个定时器:
- BroadcastTime : 主节点定时发送给从节点的心跳定时时间
- Election Timeout :从节点等待进行选举的超时时间
- MTBT : 指的是单个服务器发生故障的间隔时间的平均数
三个定时器的超时时间应该是:
BroadcastTime << ElectionTimeout << MTBF
心跳时间一定要小于从节点等待的超时时间,从节点每次收到心跳都会重置这个等待时间。
一般BroadcastTime大约为0.5毫秒到20毫秒,ElectionTimeout一般在10ms到500ms之间。大多数服务器的MTBF都在几个月甚至更长。
主从同步,日志复制
leader 选出来之后,如何保证主从之间的同步?
复制状态机通常都是基于复制日志实现的,每个节点都有各自的日志文件,比如在Redis 中是 AOF 文件。
在不同服务器上的不同文件中,每条日志记录都有一个任期号和递增的索引。
如果不同文件的索引和任期号都相同,说明这2个日志一样。也就说明2台服务器上的数据是一致的。
那么会有2种情况:
-
正常运行的情况,在主从数据都一致的情况下:
客户端每发送一个写命令给主节点,主节点会将写命令 append 到主节点的日志文件最后,然后将命令广播给所有的从节点。当所有的从节点都收到这条命令之后,将执行结果返回给客户端。如果这个过程有的从节点因为网络原因没有收到,也会回给客户端,但是对于失败的命令会不断重试。
因为主机节点拥有所有从节点的最新的日志 index 。当从节点回复了之后就更新其日志 index。 -
从节点崩溃很久之后重启,导致的主从数据差异很大:
-
主节点崩溃,导致重新选举。
同时主从之间的心跳会不断的发送主节点的log
有一个问题,主节点和从节点之间的日志不同步,主节点领先从节点N条数据,然后主节点挂掉,新的主节点被选中,此时新主的数据其实不是全局最新的。
安全性和成员变化
如果重新选举之后的主节点落后于从节点,从节点多余的数据将会全部抹掉。
如果从节点的日志比主节点少,主节点会减少日志索引,直到找到最终的一致的地方
如果一个跟随者的日志和领导人不一致,那么在下一次的附加日志 RPC 时的一致性检查就会失败。在被跟随者拒绝之后,领导人就会减小 nextIndex 值并进行重试。最终 nextIndex 会在某个位置使得领导人和跟随者的日志达成一致。当这种情况发生,附加日志 RPC 就会成功,这时就会把跟随者冲突的日志条目全部删除并且加上领导人的日志。一旦附加日志 RPC 成功,那么跟随者的日志就会和领导人保持一致,并且在接下来的任期里一直继续保持
https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md