如果你曾经和一群朋友商量今晚去哪里吃饭,你就已经亲身体验过"共识"了。每个人都有自己的想法;有人半天不回消息,这叫网络延迟;有人直接睡着了,这叫节点崩溃;两个人同时声称自己是群主,这叫脑裂。分布式系统中的共识(Consensus) 问题,本质上就是让一群可能出错的机器,对某个值达成一致——而且不能反悔,不能分歧,还要在合理时间内搞定。
这听起来简单,但做起来比劝两只抢纸箱的猫握手言和还难。好在过去几十年,理论家和工程师们搞出了一套算法——Paxos、Raft、Zab、Viewstamped Replication——让这件事变得可能。下面我们就来聊聊共识是什么,它有哪些"分身",以及为什么你最好别自己造轮子。
共识到底在"共"什么?
简单说,共识就是让一群节点就某个值达成一致,并且一旦决定,绝不反悔。这听起来像句废话,但在分布式世界里,这玩意儿难到曾有人拿了图灵奖。
为了实现共识,算法必须满足四个条件:
- 统一协议(Uniform Agreement) :所有节点最终决定的值必须相同,不能 A 说"今晚吃火锅",B 说"不,是日料"。
- 完整性(Integrity) :决定了就不能改,不能吃着火锅突然说"其实我想吃日料"。
- 有效性(Validity) :被决定的值必须是某个节点真正提议过的,不能凭空捏造。比如大家都提议吃饭,结果系统决定让你"去月球",这不行。
- 终止(Termination) :必须得有个结果,不能因为有人纠结吃啥就永远卡在那里(除非所有节点都挂了)。
前三个是安全性(Safety) 属性,最后一个是活性(Liveness) 属性。容错的关键就在于:即使部分节点挂了,剩下活着的节点也必须能继续达成一致。
共识的"七十二变":你以为它是锁,其实它是日志
这里有一个非常反直觉的冷知识:CAS(比较并交换)、原子计数器、分布式锁、甚至那个只能往后追加的共享日志,在数学上都是等价的。
逐个说说它们是什么:
- 单值共识(Single-value consensus) :多个节点争抢同一件事,只有一个能赢。比如多个节点同时申请一把分布式锁,谁获得共识,锁就是谁的。
- 比较并设值(Compare-and-set, CAS) :原子地检查"当前值是否等于预期值",如果是则更新,否则拒绝。这本质上就是一种共识——在"要不要更新"这件事上,系统只能有一个答案。
- 共享日志(Shared log)/ 全序广播(Total order broadcast) :多个节点可以向同一个日志追加条目,所有节点以完全相同的顺序读到这些条目。这是状态机复制(State machine replication) 的基础——只要所有节点按同一个顺序执行同一批操作,它们的状态必然一致。
- 原子 fetch-and-add:原子地读取并递增一个计数器。注意,它的共识数(consensus number) 只有 2——意思是它只能在理论上保证两个并发进程之间达成共识,而 CAS 和共享日志没有这个限制,可以支持任意多个节点。
- 原子提交(Atomic commit) :分布式事务中,所有参与者必须一致决定提交还是中止,不能一半提交、一半回滚。
只要有一个可靠的共享日志,单数据中心内的绝大多数共识场景都能从它推导出来:
- 想实现分布式锁? 谁先在日志里写下"这个坑我占了",锁就是谁的。
- 想保证用户名唯一? 谁先在日志里写下"用户名@小明归我了",后来的"小明"只能含泪加个数字后缀。
所以,现代共识算法(Raft、Multi-Paxos)的核心,都是在维护一个仅追加、绝对有序、不可篡改的日志。这就像整个集群共用一本"编年史"——不管谁当班长,史官只按时间顺序写,后来者只能照抄,不能撕页。
Raft 的套路:选班长与抄作业
理解了共享日志的本质,再来看 Raft 就很直观了。Raft 的核心机制只有两件事:选出一个班长(Leader Election) ,以及让班长带着大家把日志条目抄一遍(Log Replication) 。
下面这个时序图展示了一次写入是如何在集群里完成的:
sequenceDiagram
participant 班长(Leader)
participant 同学A(Follower)
participant 同学B(Follower)
Note over 班长(Leader),同学B(Follower): 第一幕:先把作业交上来(复制日志)
班长(Leader)->>同学A(Follower): 作业答案抄一下(AppendEntries RPC)
班长(Leader)->>同学B(Follower): 作业答案抄一下(AppendEntries RPC)
同学A(Follower)-->>班长(Leader): 抄好了!
同学B(Follower)-->>班长(Leader): 我也抄好了!
Note over 班长(Leader): 超过半数人抄完了,这事就算定了(Committed)
班长(Leader)->>同学A(Follower): 行,这题答案正式生效了(Apply)
班长(Leader)->>同学B(Follower): 行,这题答案正式生效了(Apply)
两个关键机制:
1. 任期(Term) :每一届班长的任期都有一个单调递增的编号。如果现任班长长期没有心跳,其他节点超时后发起新一轮选举,任期号 +1。老班长如果从故障中恢复,一看自己的任期号已经落后,立刻退位让贤——这就避免了两个节点同时以为自己是班长(脑裂)的情况。
2. 法定人数(Quorum) :班长发出的日志条目,必须有超过半数的节点确认写入,才算已提交。这意味着:哪怕班长在通知所有人之前就突然宕机,新选出来的班长一定是从"已经抄过这条日志"的节点里选的。数据绝不会悄悄消失。
为什么不能全靠"共识"?
既然共识这么厉害,为什么不所有系统都用上?因为代价太大了。
每一次写入都要等过半节点确认。在同一个机房里,这延迟是几毫秒,还算能接受。但如果你的节点分布在北京和上海,每次写入都要来回一趟,延迟直接上几十毫秒——这就是为什么很少有人把强共识系统部署在跨地域的场景里。这正是 CAP 定理告诉我们的冷酷真相:当网络被切断时,你只能二选一——要么等网络恢复后确保一致(牺牲可用性),要么先响应请求但可能数据不一致(牺牲一致性)。
所以,聪明的做法是:把共识打包成一个专门的服务,让应用层按需调用。这就是协调服务(Coordination Service) ,典型代表是 ZooKeeper、etcd、Consul。它们基于共识算法(Zab、Raft)对外提供:
- 分布式锁与租约(Locks & Leases) :互斥访问某个资源。
- Fencing 令牌(Fencing Token) :单调递增的 ID,用于防止已经认为自己持有锁的"僵尸节点"破坏数据——即使它的租约已经过期,存储层可以凭令牌编号判断并拒绝它的请求。
- 故障检测(Failure Detection) :通过心跳和会话超时判断节点是否存活。
- 变更通知(Change Notification) :Watch 机制,下游不用轮询,有变化了主动推送。
这些服务特别适合做领导者选举、服务发现、分片分配和集群配置管理。它们通常跑在 3 或 5 个节点上,数据量小但要求强一致。千万别拿它们当高性能数据库用——如果你每秒往 etcd 写几千条业务数据,它会哭着求你放过。
总结
共识就是让一群不靠谱的机器,对某个值或操作顺序达成一致。它等价于线性化的 CAS、共享日志和原子提交。Raft、Paxos 这类算法通过领导者 + 法定人数投票,实现了自动故障转移,避免了脑裂和数据丢失。而协调服务把这些能力打包成好用的 API,让你不用从零开始造火箭。
选不选共识,先问自己一个问题:能不能接受数据偶尔乱一下?
- 不能接受 → 拥抱共识,同时准备好迎接延迟和运维挑战。
- 能接受 → 放过自己也放过机器,最终一致性真香。
想深入了解 Raft 的话,原论文 In Search of an Understandable Consensus Algorithm(Ongaro & Ousterhout,2014)出奇地好读,作者把"易懂"作为设计目标之一,是少见的对普通工程师友好的系统论文。想直接动手用,从 etcd 的官方文档入门是个不错的起点。