浅谈分布式协调
单机系统里,互斥、顺序、失效这些问题可以依靠本地内存、数据库事务或进程锁解决。到了分布式系统里,这些办法不再够用。
分布式系统多了一层网络,不确定性也随之而来。每个节点只能看到自己的本地状态,以及已经收到的消息。某个节点没有响应,可能是真的宕机,也可能是网络抖动、GC 暂停、磁盘 I/O 卡住、线程池打满。调用方看到的超时,只能说明自己在指定时间内没有收到结果,不能证明对方一定没有执行。
协调(Coordination),就是多个节点在无法确知彼此状态的前提下,通过交换信息达成一致、共同完成任务的过程。
无论什么业务,分布式系统都需要保证三个性质:
- 顺序性:确保多个事件发生的先后次序与客观事实一致;
- 存活检测:确保一个节点卡住了,其他节点能接替它工作;
- 互斥性:一个任务有且仅有一个实例负责;一个数据有且仅有一个实例写入。
由 CAP 定理(详见《浅谈CAP定理》)可知,系统必须在 CP 与 AP 之间抉择:选 CP(一致性优先),意味着在无法确定数据是否正确时宁可拒绝服务;选 AP(可用性优先),意味着无论数据是否正确都先响应请求。这个抉择决定了两条实现路径各自的手段与代价。下文分别从两条路径出发,推导它们遇到的场景与解决方案。
CP 系统
CP 的出发点是一致性:状态在时间线上必须是单线,任何时刻只能有一个正确的当前状态。这要求所有写操作在唯一的“点”上排队——全局序列化点(Serialization Point):系统必须能为每个写请求提供全局唯一递增的序号。序列化点是逻辑层面的概念,其中 Leader(领导者,当前决策者)是它最常见的一种物理实现。
此处的要求,实际指的是构建自动、健壮、无人值守运行的工业级 CP 系统,这都建立在共识算法之上。因为理论上完全可以手动配置一个“主从强同步数据库”(如 MySQL 半同步复制,并设置 wait_for_slave_count = 1)。只要网络分区发生时,人工介入确保“不切主”或“只切给日志最新的从库”,这个系统在数学行为上就是 CP(停机保一致)。但这并没有意义。
顺序性
异步网络中没有全局时钟,任何节点都无法证明“这个写请求比那个写请求先发生”。两个客户端同时向不同节点写同一个数据,两个节点都会认为自己的写在前。CP 的解法是让 Leader 作为唯一写入口:所有写请求必须经过 Leader 排队,由 Leader 分配全局严格递增的日志序号(Log Index)。
写入流程:
- Leader 收到写请求,将其封装为日志条目并分配序号;
- Leader 将日志条目并发复制给所有 Follower(跟随者);
- Leader 等待超过半数节点确认落盘——多数派(Quorum)策略:N 个投票节点中超过 floor(N/2) + 1 个同意;
- 达成多数派后提交(Commit)该条目,应用到状态机(按日志顺序执行操作,产生最新状态),返回客户端成功。
值得一提的是,一个写操作应当分为存日志和业务写,此处讨论的是存日志。业务写往往会在日志标记为 committed 后,同步执行业务写,并异步在下一次心跳或 RPC 中通知从节点执行。
由于序号全局严格递增,集群对“谁先谁后”达成唯一共识。等待多数派确认属于同步复制:写入必须等待其他副本确认后才返回成功,即使网络正常也付出额外延迟。
存活检测
Leader 需要知道 Follower 是否存活,Follower 也需要知道 Leader 是否存活。
异步网络中没有可靠的死亡证明,只能在时间上划定边界。
- Leader 定期向所有 Follower 发送心跳(不带数据的空日志请求),Follower 在选举超时内没有收到心跳,就认为 Leader 失联。
选举超时(Election Timeout):收到上一次心跳后开始计时,超时未收到新心跳则发起选举
- Leader 向 Follower 发放租约(Lease,带有效期的授权),Follower 需在租约内回应或完成续约(keepAlive),否则 Leader 判定其故障,并通过配置变更日志将其剔除出集群;该日志同样需要多数派确认,防止 Leader 单方面误判。
Leader 选举
Leader 也是普通节点,同样会宕机或与多数派失联。此时集群必须选出新 Leader,但存在脑裂(Split-Brain)风险。
脑裂:网络分区后,两个节点集合各自选出自己的 Leader,两边都认为有权接受写入,分区恢复后出现两条互相冲突的状态线。
防止脑裂的机制是任期号(Raft 中叫 Term,ZAB 中叫 Epoch,含义为“第几任 Leader”):
- 每次选举任期加一,候选者发起投票,必须获得多数派选票才能当选;
- 节点在同一任期内最多投一票;
- 任何节点看到更高任期后,拒绝旧任期的请求,旧 Leader 降级为普通节点;
- 候选人的日志必须至少和投票者一样新(Raft 称之为 Leader Completeness),保证已提交的日志不会在后续选举中丢失。
多数派有一个关键性质:任意两个多数派集合一定有交集。因此任意时刻最多只有一个节点能获得多数派支持当选;结合任期与日志新旧规则,已经提交的决定不会被推翻。
多数派的前提是:成员集相对固定、每个投票节点权重相同、处理的是崩溃故障和网络分区(而非恶意节点)。3 个节点容忍 1 个投票节点故障,5 个节点容忍 2 个;4 个节点仍然只能容忍 1 个,所以协调系统通常推荐奇数个投票节点。
残留写
任期只保护协调协议内部状态,不会自动传播到外部业务资源。旧 Leader 如果绕过复制协议直接写数据库或对象存储,资源端并不知道任期已经变化,迟到的写入仍然可能成功。
Fencing Token(栅栏令牌):每次成功选举新 Leader 时,协调系统发放一个单调递增的 token;写入共享资源必须携带 token,资源端记录见过的最大 token,拒绝更小 token 的写入。校验必须是原子的:比较 token、更新数据、记录最新 token 应在同一原子操作中完成,否则中间仍可能被并发请求插入。
AP 系统
AP 的出发点相反:写路径不能被任何节点故障或网络抖动阻塞。为此 AP 采用与 CP 相反的手段:
- 无主架构(Leaderless):任意节点都可以接受写请求,不存在单点;
- 异步复制:节点收到写请求后,本地落盘即返回成功,不等待其他节点确认。
由于没有排队与等待,写入始终畅通;也正因此,两个节点可能同时接受对同一数据的写请求——历史必然分叉。AP 的问题在于,系统必须内置一套无损合并算法(如向量时钟、CRDT),确保分叉最终能收敛回统一状态。AP 的协调机制,全部是为了处理这个副作用。
顺序性
AP 系统往往通过向量时钟 + 流言协议来裁定顺序。
向量时钟(Vector Clock)
向量时钟的核心思想是每个节点不仅维护自己的时钟,还维护它已知的其他所有节点的时钟信息。在一个有 N 个进程的系统中,每个进程 Pi 都维护一个长度为 N 的向量 VCi。VCi[j] 代表进程 Pi 已知的进程 Pj 的当前逻辑时钟值。
更新规则:
- 初始化:所有向量时钟值初始为 0;
- 内部事件:进程 Pi 发生新事件时,VCi[i] 自增 1;
- 发送消息:进程 Pi 发送消息时,将自己的整个向量时钟 VCi 附在消息中;
- 接收消息:进程 Pj 收到来自 Pi 的消息(含向量时钟 VCi)后,先更新自己的向量时钟:对于每个维度 k,VCj[k] = max(VCj[k], VCi[k]),然后,VCj[j] 自增 1。
判断因果关系
向量时钟采用偏序关系进行比较。假设事件 a 发生在进程 Pi,时间戳为 VC(a);事件 b 发生在进程 Pj,时间戳为 VC(b)。
因果关系 (A → B):当且仅当 VC(a) 中的每一个元素都小于等于 VC(b) 中的对应元素,并且至少有一个元素严格小于。这表示事件 a 在因果上先于事件 b。
并发关系 (A ‖ B):如果两个向量时钟无法比较大小,即某个维度上前者大、另一个维度上后者大。这表示两个事件是并发的,不存在因果关系。
流言协议(Gossip)
Gossip 协议(又称流言协议、流行病协议)是一种去中心化的点对点通信协议,旨在分布式系统中实现信息的高效传播与节点状态的最终一致。
Gossip 协议主要通过以下三种方式来传播信息和实现最终一致性:
直接邮寄(Direct Mail):当一个节点状态发生重要更新时,它主动将这个更新“推送”给随机选择的几个邻居节点。这种方式简单直接,传播速度快,但并不保证消息一定能送达,无法单独实现最终一致性。
反熵(Anti-entropy):这是实现最终一致性的核心手段,旨在降低节点间的数据差异。集群中的节点会定期随机选择一个其他节点,通过互相交换各自的所有数据(或数据摘要)来消除两者之间的差异。它主要有三种模式:
推(Push):节点A将自己的数据推送给B,让B修复差异。
拉(Pull):节点A从B那里拉取数据,来修复自身的差异。
推拉(Push-Pull):双方交换数据,各自修复差异。这是最常见和健壮的方式。
反熵非常可靠,但因为需要交换大量数据,通信成本较高。
- 谣言传播(Rumor Mongering):当一个节点有了“新信息”(谣言)时,它会成为一个“活跃”的传播者,积极地向其他节点传播。收到新谣言的节点也会变为活跃传播者,继续向外散播。为了避免信息无限传播,节点在发现大多数邻居都已经知道这个谣言后,会停止主动传播。
特性与权衡
Gossip 协议因其独特的机制,带来了一系列鲜明的特性:
核心优势:
高可扩展性(Scalability):每个节点只与少量固定节点通信,通信成本不随集群规模线性增长,非常适合大规模系统。
高容错性(Fault Tolerance):没有中心节点,部分节点故障不影响整体。消息传播路径众多,可通过其他节点绕开故障。
去中心化(Decentralization):所有节点都是对等的,避免了单点故障(SPOF)。
主要代价:
最终一致性(Eventual Consistency):Gossip 协议不提供强一致性保证。它只能保证在系统持续运行、通信不间断的情况下,所有节点的状态最终会达成一致。这个“最终”所需的时间,在理论上无法精确定义。
消息延迟(Latency):因为信息传播需要多轮随机交换,从信息产生到所有节点都收到,会存在一定的延迟。
带宽消耗(Bandwidth):为了确保可靠性,同一条信息可能会被重复发送给不同节点,造成一定的网络带宽开销。
网络分区无感知(Network Partition Agnostic):发生网络分区时,不同分区的节点会继续在各自分区内传播 Gossip,导致分区间的信息同步被严重延迟。
借助流言协议传播的向量时钟,系统可以精确判断:
如果两个更新是因果关系,则后一个更新可以安全地覆盖前一个。
如果两个更新是并发关系,则产生了数据冲突,必须由系统(如通过特定策略)或客户端/应用层来解决(如合并)。
存活检测
AP 没有 Leader 来统一判定节点死活。故障判定采用柔性检测(如 Phi-Accrual 累积故障检测):根据历史延迟计算“怀疑度”,只要没有请求发往疑似故障节点,就不主动判死。对于疑似故障节点,写入方还可以先将数据暂存(Hinted Handoff,暗示移交),待其恢复后补发,进一步保证写入不被阻塞。
收敛
AP 允许脑裂。分区恢复后,系统通过 CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型:数学上保证合并操作满足交换律等性质,无论以什么顺序合并都得到相同结果)或业务自定义的合并函数,把两侧的分叉数据合并回统一状态。
如果业务数据没有定义良好的合并规则,合并时就会丢数据——这是 AP 系统最常见的资损根源。
机制汇总与选型
上述推导覆盖的机制,最终可以归为两类问题:
- 决策:谁有资格、什么已生效。交给 Leader、Quorum、任期和资源端校验;
- 传播:状态如何扩散。交给 Gossip 与版本收敛。
真实系统很少二选一,而是按场景组合:比如 Gossip 传播成员状态与故障观察,多数派投票与任期决定主节点晋升。
| 机制 | 主要解决什么 | 一致性特点 | 典型场景 |
|---|---|---|---|
| Leader + 日志复制 | 决定写入顺序与元数据状态 | 可提供强一致 | 配置发布、主从切换、元数据变更 |
| Quorum 投票 | 判断某个决定是否有效 | 依赖集合交集与协议规则 | 日志提交、Leader 选举、故障确认 |
| Lease | 临时授予执行资格 | 资格可能过期 | 定时任务、资源 owner、短临界区 |
| Fencing Token | 拒绝旧 owner 的迟到写 | 依赖资源端校验 | 数据库、对象存储、外部资源写入 |
| Gossip | 扩散成员与状态信息 | 通常最终收敛 | 服务发现、健康状态、缓存元数据 |
| 队列 / 分片领取 | 把工作分配给多个 Worker | 通常至少一次 | 批处理、消费任务、分片扫描 |
选型的判断从两个问题开始:
- 错误决定的后果是什么?重复执行只是多消耗资源、短时间内看到旧状态可以接受,那状态传播与最终收敛通常足够;错误决定会造成资金错误、库存错误、元数据损坏、两个主节点同时写同一份数据,就要优先考虑 Leader/Quorum、共识算法、多数派提交,必要时加 Fencing Token。
- 系统规模与写入路径如何?小规模控制面用 Leader 管理更容易维护;节点数量大、状态变化频繁、每个节点只需要近似视图时,对等传播更合适。Gossip 的消息有冗余,但避免了所有状态更新汇聚到单个节点,代价是冲突处理、消息去重和状态排查的成本。




