Appearance
共识、Raft 与分布式协调
共识(Consensus)解决的是:多个节点即使经历延迟、丢包、重复消息和部分节点故障,也要对某个值或一串操作达成一致。它常用于复制状态机、Leader 选举、配置管理和元数据服务。
共识需要保证什么
经典共识通常关注:
- Agreement:正确节点不能决定不同结果;
- Validity:决定值来自合法提议,不能凭空产生;
- Integrity:一个节点不会对同一轮决定两次;
- Termination:满足故障和网络假设时,正确节点最终作出决定。
安全性表示“不能发生错误决定”,活性表示“最终能够继续推进”。网络不确定时,协议通常宁可暂停少数派,也不能让两个分区同时认为自己拥有合法写权限。
FLP 应该怎样理解
FLP 结论是:在完全异步网络中,即使只有一个节点可能崩溃,也不存在既确定性、又能保证所有执行都终止的共识算法。它不是说工程上无法实现共识,而是说明不能同时依赖无限期未知的消息延迟并保证确定时限内完成。
Raft、Paxos 等工程协议通常基于部分同步假设:安全性不依赖固定延迟;当网络最终恢复到足够稳定、超时参数能够区分正常与异常后,系统恢复活性。随机选举超时也用于减少候选人反复平票。
多数派、任期与脑裂
多数派集合必然相交。若集群有 2f+1 个投票节点,可容忍 f 个节点不可用,并仍形成 f+1 的多数派:
text
3 节点容忍 1 个故障
5 节点容忍 2 个故障节点数量增加并不会线性提高写吞吐,因为每次提交仍需多数派复制;偶数投票节点通常不会比前一个奇数配置多容忍故障。例如 4 节点仍只能容忍 1 个故障,却需要 3 票多数。
脑裂是多个节点或分区同时对外充当唯一主节点。多数派只允许包含多数票的分区继续提交,但外部资源还需要识别旧 Leader:任期号、配置版本或 fencing token 可以让旧持有者的迟到请求失效。
Raft 的角色与任期
Raft 把节点分为:
- Follower:被动接收 Leader 日志和候选人投票请求;
- Candidate:选举超时后发起竞选;
- Leader:处理写请求并复制日志。
时间被划分为单调递增的 term。每个任期最多有一个合法 Leader;节点看到更高 term 的消息必须更新任期并退回 Follower。term 类似逻辑时代编号,用于识别旧 Leader 和旧消息。
Leader 选举
Follower 在选举超时内未收到合法心跳,就:
- 增加
currentTerm; - 转为 Candidate 并给自己投票;
- 向其他节点发送
RequestVote; - 获得多数票后成为 Leader;若收到更高 term,则退回 Follower。
同一节点在一个 term 内最多投一票。除了任期,投票者还会比较候选人的最后日志 term 和 index,只给日志“至少和自己一样新”的候选人投票。这条限制防止缺少已提交日志的节点当选。
随机化选举超时用于降低多个 Follower 同时竞选造成平票的概率。心跳本质是没有新日志项的 AppendEntries,同时也维持 Leader 权威。
日志复制与提交
Leader 将客户端命令追加到本地日志,再通过 AppendEntries 复制给 Follower。每条日志至少包含 index、term 和状态机命令。Follower 会校验前一条日志的 index 与 term:若不匹配,就拒绝请求,Leader 回退复制位置,直到找到共同前缀,并用 Leader 的日志覆盖冲突后缀。
text
Client command
↓
Leader append log
↓ AppendEntries
Followers persist log
↓ majority acknowledged
Leader advance commitIndex
↓
All nodes apply committed entries in order日志写到多数派不等于任何情况下都能立刻按 index 提交。Raft Leader 通常只通过“多数副本已保存”直接推进当前 term 的日志项;旧 term 项会随着当前 term 项提交而间接提交。这条规则避免新旧 Leader 交替时错误提交历史日志。
Follower 按 commitIndex 顺序把日志应用到确定性状态机。同一日志序列必须得到相同结果,因此状态机命令不应直接依赖本地随机数或未纳入日志的墙上时钟。
Raft 的关键安全性
- Election Safety:一个 term 最多一个 Leader;
- Log Matching:若两条日志在相同 index、term 处相同,则此前前缀相同;
- Leader Completeness:已提交日志一定出现在以后任期的 Leader 中;
- State Machine Safety:不同节点不会在同一 index 应用不同命令。
“Leader 收到请求”或“Leader 写入本地磁盘”都不等于已提交。客户端只有在协议定义的提交条件满足后收到成功,才能依赖该写不会因正常选主被覆盖。
线性一致读
Follower 本地读可能陈旧。Leader 读也要先确认自己仍是当前合法 Leader,否则旧 Leader 在网络分区中可能返回过期结果。常见办法包括:
- 通过多数派确认当前任期权威,再读取已应用状态(ReadIndex 类方案);
- 使用满足严格时钟漂移假设的 Leader lease;
- 把读作为日志操作提交,但成本更高。
租约依赖时钟和暂停上界,不能在没有假设的情况下把“近期发过心跳”直接当作永远安全的线性读证明。
快照与成员变更
日志无限增长时,需要将已应用状态做快照并截断旧日志;落后太多的 Follower 可安装快照后继续追赶。
集群成员不能直接从旧配置一步切到新配置,否则两个配置可能各自形成不相交多数派。Raft 常用 joint consensus,让过渡阶段同时满足新旧配置的多数要求,再切换到新配置。
Raft 与 Paxos
Raft 和 Paxos 都基于多数派相交保证共识安全。Paxos 先描述对单个值达成共识,工程系统常使用 Multi-Paxos 持续复制日志;Raft 则显式引入 Leader、term,并把选举、日志复制和成员变更拆成更容易解释的模块。
不能简单说 Raft “比 Paxos 更强”或“性能一定更好”:它们解决相近的共识问题,具体性能取决于实现、批处理、持久化、网络和读协议。Raft 的主要优势是协议结构和可理解性。
共识、复制和事务不是同一概念
- 复制:把数据保存到多个节点;异步复制不一定需要共识;
- 共识:让节点对值或日志顺序达成一致;
- 事务:让一组读写满足原子性、隔离性等语义;
- 分布式锁:协调某段时间内谁可以执行操作。
共识日志可以实现强一致复制状态机,但不自动等于跨多个业务资源的事务。分布式事务也可能使用 2PC 协调多个已经各自通过共识复制的参与者。
分布式锁与租约
一个可用的分布式锁至少要讨论:
- 互斥:同一时刻最多一个合法持有者;
- 所有权:只有获取锁的客户端能释放它;
- 活性:持有者崩溃后锁最终可重新获取;
- 租约续期:长任务如何安全延长有效期;
- 故障后的旧持有者:暂停或断网的客户端恢复后,不能继续修改受保护资源。
Redis 风格的租约锁
单实例常见获取方式:
text
SET lock_key random_token NX PX ttl释放时必须原子地比较 token 后删除,不能先 GET 再 DEL:
lua
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
end
return 0随机 token 防止客户端 A 的锁过期后,A 误删客户端 B 新获得的锁。但仍要说明安全边界:客户端可能发生长时间 GC / 调度暂停,租约过期后继续执行;主从异步复制和故障切换也可能让刚写入的锁丢失。因此它适合能够容忍极少量并发执行、且操作本身幂等的协调场景,不能仅凭 SET NX PX 宣称获得了严格共识级互斥。
Fencing token
更可靠的办法是每次成功获取锁时分配单调递增 token:
text
客户端 A 获取 token=41
A 暂停,租约过期
客户端 B 获取 token=42,并成功写资源
A 恢复后携带 token=41 写资源 → 资源拒绝旧 token真正保护数据的是资源端拒绝较旧 token。只有锁服务检查 token,而数据库、对象存储或下游服务不检查,旧持有者仍可能造成破坏。
基于 etcd、ZooKeeper 等共识系统的锁通常可利用 revision、lease 或临时顺序节点提供所有权与 fencing 信息,但业务仍需处理租约失效、会话断开和操作幂等。
Leader 选举和锁的区别
选出 Leader 后,Leader 通常持续处理一类请求并复制日志;锁则保护一段临界区。两者都需要防止旧持有者,但不要用一个普通短 TTL 锁草率代替完整的 Leader 任期、日志提交和故障恢复协议。
若只有单机多线程竞争,应优先使用进程内 mutex;若能通过玩家、房间或订单 ID 把同一实体固定路由到唯一工作线程,通常也比每次业务操作获取分布式锁更简单。
常见错误
- 把心跳超时当作对端一定宕机;超时只能触发怀疑和选举;
- 认为节点多于一半存活就一定可用;它们还必须彼此连通并完成协议;
- Raft 日志复制到一个 Follower 就算提交;提交需要满足多数派和当前 term 规则;
- 从任意 Follower 读取却宣称线性一致;
- 用系统时间直接判断 Leader 或锁所有权,却不考虑漂移和进程暂停;
- 锁释放不校验 token,误删其他客户端的新锁;
- 只有租约没有 fencing token,旧持有者恢复后仍能写外部资源;
- 一次修改集群成员配置,产生两个不相交多数派;
- 把共识、复制、事务和锁当作同一个概念。
面试中的简洁回答
Raft 通过递增 term、随机超时选举和多数派日志复制实现共识。候选人的日志必须至少和投票者一样新,保证已提交日志不会被缺失它的新 Leader 覆盖。Leader 只有在满足多数派提交规则后才向客户端确认成功;少数派在网络分区时不能继续提交,从而避免两个合法 Leader。分布式锁还要处理租约过期后的旧持有者,因此仅有 TTL 不够,关键资源最好校验单调 fencing token。