选主必须有大多数
多数票正是用来禁止脑裂的:集群被切开后,人数不足一半的那一侧无法凑够法定多数,也就不可能继续自封为主。 一个主、一帮多数 一个不能各执一词的复制系统,要为这组节点选出一个「主」。候选者向同伴征票,要成为主,它需要一个法定多数——在容错方案里,超过一半的多数在一届任期内点头。这步正是让唯一赢家敢放手的保证:没有少数派还能与它平起平坐,因为被分成两半的集群里,最多只有一半凑得齐多数。所以多数严格规定为大于一半,而不是刚刚一半——正是因为这一点,两个被切开的半边才不会同时都以为自己说了算。 法定人数挡掉脑裂 分区是领导者选举必须活过的经典险境。如果集群被切成两半,最多只有一半握得住多数;缺多数的那一侧必须等待,因为凑不齐法定人数,它就不能硬把写请求当成交付。这正是多数票的作用:把旧主从少数派一侧安全地换下,让两个断开的节点不会各拿一份因果上相互矛盾的写。多数派把「该谁主政」从一场互相指摘,变成一个结果唯一且安全确定的操作。 任期与心跳维持秩序 领导权要不断地被续,而不是一支永续。任期的时长由任期与心跳来度量:主靠心跳续租,当 peers 因它失联而越过任期,就会重启一次选举、开启一个新的任期。这个不断延续的节奏,让集群能在成员进出时依然有序,因为没有任何一位主的信任会依赖它自己永生。这一套「选主、竞选、失联检测、重选」的循环,让整群无需一台共同认可的时钟,也能一直保持诚实。 坏掉的样子 偶数个节点最先吃亏。四个节点被切成二对二,选主需要三票,两边谁也凑不够,整个集群只能静默,直到分区愈合。六个节点三对三僵持,道理完全一样。这正是生产环境常用三个或五个投票者的原因:任何一次切分,最多只有一侧还握着严格多数。 没有多数的一侧必须停写,这停顿正是安全的代价。被困在少数派一侧的旧主续不上法定人数,只能让位,于是粘在那一侧的客户端看到的是报错而不是进展。若把这些报错当成缺陷,强行在本地落写,就等于把多数派规则花钱买来的安全又亲手退掉,脑裂立刻回来。 即使网络完好,选举也可能活锁。候选者若在同一瞬间起跑,选票被瓜分,谁都到不了多数,任期空转,主位抖动,写延迟随之飙升,而每一轮投票都以失败告终。随机化选举超时正是为打破这种对称而设;若把超时配成完全相同的值,或节点过载拖延投票,就等于亲手复刻这场风暴。 最后一坑是任期回退。长期分区后归队的节点拿着过期任期拉票,或日志落后的候选者索要它本不配得的选票。投票者必须拒绝更低任期,并要求候选者日志足够新;一旦跳过这道新鲜度检查,新主上台就会覆盖上届多数派已经提交的条目,共识的根基就此动摇。 快速验证 把下面脚本保存为 majority.py,直接运行 python3 majority.py 即可。它只用标准库,模拟五个投票者、每个节点每任期一票,再用分区决定候选者实际能收到谁的票。脚本覆盖四组断言:五节点下多数侧当选、少数侧落选、四节点偶分两侧皆败、过期任期零票。若任一步语义走样,断言即抛错,不会悄悄打印通过: def quorum(n): """严格多数:超过全体投票者的一半,而不是某一侧的一半。""" return n // 2 + 1 class Cluster: """最小票箱:N 个投票者,每个节点每任期只能投一票。""" def __init__(self, size): self.size = size self.terms = [0] * size self.voted_for = [None] * size def request_votes(self, candidate, term, reachable): if term <= self.terms[candidate]: return [] # 过期任期:候选者自己都见过更新的任期 votes = [] for node in reachable: if term > self.terms[node]: self.terms[node] = term self.voted_for[node] = candidate votes.append(node) elif term == self.terms[node] and self.voted_for[node] in (None, candidate): votes.append(node) return votes def elect(self, candidate, term, reachable): votes = self.request_votes(candidate, term, reachable) return len(votes) >= quorum(self.size), votes assert quorum(3) == 2 and quorum(4) == 3 and quorum(5) == 3 majority = Cluster(5) won, votes = majority.elect(candidate=0, term=1, reachable=[0, 1, 2]) assert won and sorted(votes) == [0, 1, 2], votes print(f"5 节点中 3 节点侧:得票 {sorted(votes)}(需 {quorum(5)} 票),当选") minority = Cluster(5) won, votes = minority.elect(candidate=3, term=1, reachable=[3, 4]) assert not won and sorted(votes) == [3, 4], votes print(f"5 节点中 2 节点侧:得票 {sorted(votes)}(需 {quorum(5)} 票),无人当选") even = Cluster(4) for candidate, reachable in ((0, [0, 1]), (2, [2, 3])): won, votes = even.elect(candidate, term=1, reachable=reachable) assert not won, (candidate, votes) print(f"4 节点 2-2 分裂:两侧均到不了 {quorum(4)} 票,集群停摆") fresh = Cluster(5) won, _ = fresh.elect(candidate=0, term=2, reachable=[0, 1, 2, 3, 4]) assert won won, votes = fresh.elect(candidate=4, term=1, reachable=[0, 1, 2, 3, 4]) assert not won and votes == [], votes print("过期任期 1 在任期 2 之后:零票拒绝") print("PASS:多数侧当选,少数侧与偶分停摆,过期任期落败") 脚本逐条断言选举语义。三节点一侧拿到五票中的三票,顺利当选本任期主;两节点一侧只拿到两票,选不出任何人,只能等待。四节点被切成二对二时,两侧都到不了三票,于是没有主诞生,集群原地停摆。过期任期的选票纵使算术上够数,也会被零票拒绝。预期输出如下,五行缺一不可: ...