Raft 选主需要当前配置中投票成员的严格多数。在成员集合固定、每个服务器每任期最多持久记录一票的前提下,两个候选者不能同时赢得同一个任期,因为它们的多数集合必有交集,而交集中的投票者不能支持两个人。这是关于选举结果的保证,不是说所有节点在每个时刻都知道当前主是谁,也不是说任何形式的领导者选择都只能使用这一种算法。

每个任期最多一个赢家

分母是配置中的全部投票者,不是此刻能够联系上的机器。五个投票者需要三票,即使现在只有两台互通,也不能把门槛降成两台中的多数。学习节点和不参与投票的副本不会自动扩大分母。成员变化必须采用安全的重配置协议;若两侧自行改写成员名单,即使各自都声称满足多数,也已经破坏原先的交集依据。

每任期一票与多数算术同样重要。服务器应在回复投票之前,可靠保存当前任期和已投对象,避免重启后忘记旧票又支持另一个候选者。来自同一成员的重复响应不能算成额外选票。这里也假定成员身份正确、消息来自相应节点;这种崩溃故障模型不等于已经解决恶意成员伪造身份与消息的问题。

权限不同于本地相信自己有权限

多数分区可以在新任期选出主,同时孤立旧主仍然以为自己领导着较早任期。基本 Raft 在得知更高任期时退回跟随者,并不是收到不到心跳回复就必定立刻退位。某些实现增加了法定人数检查,但那是需要单独确认的机制,不能把产品的额外行为写成所有 Raft 系统的共同前提。

少数派不能独自提交新的客户端命令。选举限制、日志匹配与提交规则共同保护跨任期的复制状态,单靠票数并不足以保证任意外部写入安全。如果业务工作者凭过去取得的主身份操作另一套存储,还可能需要由资源端执行围栏检查。要求线性一致的读取也应遵守协议的当前权限检查,不能只看到本机主标志就认为结果足够新。

任期与计时器承担不同职责

任期是长度不固定的逻辑时代,不是一个持续固定秒数的租约。跟随者正常收到有效主的心跳时,通常不会发起新选举;收不到则触发怀疑,并可能在超时后竞选。失去消息不能证明旧机器已经停止执行。Raft 的安全性论证并不要求各机器的墙钟完全同步,不能把任期编号解释成全局时间片。

随机化选举超时有助于打破候选者反复同时起跑、瓜分选票的对称。要持续推进,仍需及时通信和足够处理能力。节点过载、网络延迟或过于激进的超时,即使没有永久分区,也可能引起任期快速变化、主位抖动和写延迟上升。应根据延迟与调度观察选择参数,不能认为超时越短就越安全。

坏掉的样子

四个投票者需要三票,分成二对二时两侧都无法推进。三个和四个投票者都只能容忍一个不可用成员,因此增加第四个并没有换来第二次故障的余量;五个投票者可以容忍两个。奇数配置改善的是这一成本与容错关系,而偶数配置仍然具备多数集合相交的安全性质,并非天然会发生脑裂。

少数侧可以收到甚至暂存请求,但不能把新的、尚未提交的命令报告为已经提交成功。客户端超时也会留下结果不确定性:操作可能已经在另一侧提交,只是响应没有到达。因此,安全重试还依赖应用契约,需要时使用请求去重;不能把所有不可用错误都当作没有产生任何效果的证据。

候选者还必须拥有足够新的日志。Raft 比较最后一条日志的任期,再比较其索引,而不是简单选最长日志。即使数出了多数票,删掉新鲜度限制仍可能破坏领导者完整性。下面的票箱实验特意没有实现日志限制,所以不能拿运行通过来宣称已经实现可靠共识,更不能据此替代真实系统的故障测试。

快速验证

保存为 majority.py,运行 python3 majority.py。脚本使用共享的内存票箱、固定成员身份和显式任期,并对重复响应去重。它不启动网络服务、不创建持久化日志,所有输入都由实验明确给出。这样既能观察多数门槛,也能检查同任期重复请求和下一任期之间的区别。

def quorum(size):
    if size < 1:
        raise ValueError("at least one voter is required")
    return size // 2 + 1


class Ballots:
    """Fixed membership, unique votes, and in-memory per-term voting only."""
    def __init__(self, size):
        self.size = size
        quorum(size)
        self.terms = [0] * size
        self.voted_for = [None] * size

    def elect(self, candidate, term, reachable):
        nodes = sorted(set(reachable))
        if not 0 <= candidate < self.size or term < 1:
            raise ValueError("invalid candidate or term")
        if any(not 0 <= node < self.size for node in nodes):
            raise ValueError("unknown voter")
        votes = []
        for node in nodes:
            if term < self.terms[node]:
                continue
            if term > self.terms[node]:
                self.terms[node] = term
                self.voted_for[node] = None
            if self.voted_for[node] in (None, candidate):
                self.voted_for[node] = candidate
                votes.append(node)
        return len(votes) >= quorum(self.size), votes


assert [quorum(n) for n in (3, 4, 5)] == [2, 3, 3]
cluster = Ballots(5)
assert cluster.elect(0, 1, [0, 1, 2]) == (True, [0, 1, 2])
assert cluster.elect(3, 1, [3, 4]) == (False, [3, 4])
assert cluster.elect(3, 1, range(5)) == (False, [3, 4])
assert cluster.elect(0, 1, [0, 1, 2]) == (True, [0, 1, 2])
print("one winner per term; repeated same-candidate requests preserve votes")

assert cluster.elect(3, 2, range(5)) == (True, list(range(5)))
assert cluster.elect(0, 1, range(5)) == (False, [])
print("a newer term can elect another candidate; older requests are rejected")

even = Ballots(4)
assert not even.elect(0, 1, [0, 1])[0]
assert not even.elect(2, 1, [2, 3])[0]
assert not Ballots(5).elect(0, 1, [0, 0, 0])[0]
print("2-2 split stalls; repeating one voter cannot manufacture a majority")
print("PASS: fixed-membership ballot rules only; no replicated log or network")

预期打印四行确认。三节点侧当选,两节点侧落选;即使后来能够联系全部节点,第二个候选者也不能拿走本任期已经投出的票。新任期可以选出另一候选者,之后旧任期请求被拒;四节点二对二无法产生赢家,同一身份出现三次也仍然只有一票。重复向原候选者确认已有票不会制造第二次投票。

修订后的两种语言示例都已在 Linux、Python 3.12.14 下运行通过。这里建立的证据仅限票箱模型,不包括日志复制、崩溃持久化、成员重配置、真实计时器或生产集群从网络分区恢复。使用模型学习时,应把这些缺失的机制保留在检查清单中,而不是把短程序的成功扩大成整个分布式协议的成功。

延伸阅读

来源