09

一致性与共识

让多个节点达成一致

阅读量:2 · 预计 20 分钟读完

线性一致性全序广播PaxosRaft
关联层级:L7 应用抽象
阅读进度5%

第九章 一致性与共识 - 让多个节点达成一致

导读

在分布式系统中,让多个节点对某个值达成一致,看似简单,实则是分布式计算中最基本也最困难的问题之一。这就是共识(Consensus)问题。

为什么共识如此重要?因为分布式系统中的很多关键决策都需要共识:

  • 谁是 Leader?(Leader 选举)
  • 一个事务是否可以提交?(分布式事务提交)
  • 消息的传递顺序是什么?(全序广播)

这些问题本质上都是共识问题——多个节点需要对一个值达成一致。而根据 FLP 不可能定理,在异步网络中,即使只有一个节点可能故障,也不可能设计出一个既保证安全性又保证活性的确定性共识算法。

本章将深入探讨一致性的各种模型、共识算法(Paxos、Raft、ZAB)的原理和实现,以及它们在实际系统中的应用。


核心概念详解

9.1 一致性模型

9.1.1 强一致性(Linearizability)

线性一致性(Linearizability),也称为强一致性(Strong Consistency)或外部一致性(External Consistency),是最强的一致性模型。

定义:一旦一个新值被写入(任何节点返回写入成功),任何后续的读取(在任何节点上)都必须返回这个新值或更新的值。

形式化描述:如果操作 A 在操作 B 之前完成(实时顺序),那么操作 A 的效果必须在操作 B 之前被观察到。

线性一致性的直觉理解:系统表现得像只有一个副本,所有操作都是原子的,按照实时顺序执行。

线性一致性的优势:

  • 简单:开发者可以像操作单机系统一样思考,不需要考虑并发和复制。
  • 正确:保证所有节点看到的数据是一致的,避免了陈旧数据和冲突。

线性一致性的代价:

  • 延迟高:每次写入都需要等待多数节点确认,延迟取决于网络往返时间。
  • 可用性低:如果多数节点不可用,系统无法写入。
  • 性能差:无法利用缓存和并行处理。

9.1.2 顺序一致性(Sequential Consistency)

顺序一致性比线性一致性弱。它保证所有操作按照某种全局顺序执行,但这个顺序不一定是实时顺序。

定义:所有节点看到的所有操作都以相同的顺序执行,但这个顺序可以是任意顺序(不一定是实时顺序)。

顺序一致性与线性一致性的区别:

  • 线性一致性要求操作的顺序与实时顺序一致。
  • 顺序一致性只要求存在某种全局顺序,不要求与实时顺序一致。

例如,两个并发写入 A 和 B,线性一致性要求先完成的写入必须先被观察到;顺序一致性允许 B 先被观察到,即使 A 先完成。

9.1.3 因果一致性(Causal Consistency)

因果一致性是一种比线性一致性弱但比最终一致性强的模型。

定义:如果操作 A 因果地影响了操作 B(例如 A 写入数据,B 读取 A 写入的数据并基于此写入),那么所有节点都必须先看到 A,再看到 B。但如果没有因果关系的操作(并发操作),不同节点可以以不同顺序看到它们。

因果一致性的优势:

  • 比线性一致性的延迟低,因为不需要对所有操作进行全局排序。
  • 在网络分区时仍然可以提供服务(与线性一致性不同)。

因果一致性的实现:

  • 使用版本向量(Version Vector)追踪操作之间的因果关系。
  • 每个节点维护一个版本向量,记录每个节点的写入次数。
  • 读取时,比较版本向量,确保因果相关的操作按正确顺序返回。

9.1.4 最终一致性(Eventual Consistency)

最终一致性是最弱的一致性模型。它只保证:如果没有新的写入,经过足够长的时间后,所有副本最终会达到一致。

最终一致性的问题:

  • "足够长的时间"是不确定的——可能是几毫秒,也可能是几分钟。
  • 在达到一致之前,不同节点可能返回不同的值。
  • 开发者需要处理读取到陈旧数据的情况。

最终一致性的变体:

  • 读己之所写(Read Your Writes):用户写入后,总是能读到自己写入的值。
  • 单调读(Monotonic Read):一旦读取到某个值,后续的读取不会返回更旧的值。
  • 单调写(Monotonic Write):一旦写入成功,后续的写入不会覆盖之前的写入。
  • 一致前缀(Consistent Prefix):所有节点看到写入的顺序相同。

9.2 共识算法

9.2.1 共识问题的定义

共识问题可以形式化为:多个节点需要对一个值达成一致,满足以下性质:

  • 一致性(Agreement):所有正常节点决定相同的值。
  • 完整性(Integrity):没有节点改变主意——一旦决定了一个值,就不能改变。
  • 合法性(Validity):决定的值必须是某个节点提出的(不能凭空产生)。
  • 终止性(Termination):所有正常节点最终都会决定一个值(不会永远等待)。

前三个性质是安全性(Safety),第四个是活性(Liveness)。

9.2.2 Paxos

Paxos 是 Leslie Lamport 在 1989 年提出的共识算法,被公认为最基础的共识协议。

Paxos 的核心角色:

  • Proposer(提议者):提出值。
  • Acceptor(接受者):接受或拒绝值。
  • Learner(学习者):学习最终决定的值。

Paxos 的基本流程(两阶段提交):

Phase 1(Prepare):

Proposer 选择一个提案编号 N,向所有 Acceptor 发送 Prepare(N) 请求。

Acceptor 收到 Prepare(N) 后:

- 如果 N 大于之前收到的所有 Prepare 请求的编号,接受该请求,并返回之前接受的提案(如果有)。

- 否则拒绝该请求。

Phase 2(Accept):

Proposer 收到多数 Acceptor 的响应后,选择一个值 V:

- 如果有 Acceptor 返回了之前接受的提案,选择编号最大的提案的值。

- 否则选择自己提出的值。

Proposer 向所有 Acceptor 发送 Accept(N, V) 请求。

Acceptor 收到 Accept(N, V) 后:

- 如果没有收到更大的 Prepare 请求,接受该提案。

- 否则拒绝该请求。

Proposer 收到多数 Acceptor 的接受后,提案通过。

Paxos 的问题:

  • 难以理解:Lamport 的原始论文以晦涩难懂著称。
  • 难以实现:完整的 Paxos 协议包含很多细节(Leader 选举、成员变更、日志压缩等),实现复杂。
  • 多轮通信:在竞争情况下可能需要多轮通信才能达成共识。

9.2.3 Raft

Raft 是 Diego Ongaro 和 John Ousterhout 在 2014 年提出的共识算法,设计目标是可理解性——与 Paxos 功能等价,但更容易理解和实现。

Raft 的核心设计:

  • 强 Leader:Raft 使用一个固定的 Leader 来处理所有客户端请求,Follower 只负责复制 Leader 的日志。这大大简化了协议。
  • Leader 选举:当 Leader 故障时,Follower 通过选举产生新的 Leader。选举使用随机超时来避免分裂投票。
  • 日志复制:Leader 将客户端请求追加到本地日志,然后复制给所有 Follower。当多数 Follower 确认后,Leader 提交该条目。

Raft 的安全性保证:

  • 选举限制:只有拥有最新日志的节点才能成为 Leader。
  • 日志匹配:如果两个日志在某个索引处有相同的条目和任期号,那么它们在该索引之前的所有条目都相同。

Raft 的优势:

  • 易于理解:将共识问题分解为 Leader 选举、日志复制、安全性三个子问题。
  • 易于实现:协议简单明确,没有 Paxos 的复杂细节。
  • 工程友好:已经有多种语言的实现(如 etcd、Consul、TiKV)。

9.2.4 ZAB(ZooKeeper Atomic Broadcast)

ZAB 是 Apache ZooKeeper 使用的共识协议,与 Paxos 和 Raft 类似但有一些区别。

ZAB 的核心特点:

  • 基于广播:ZAB 实现的是全序广播(Atomic Broadcast),而非一般的共识。
  • 发现阶段:在开始广播之前,先选举 Leader 并同步状态。
  • 广播阶段:Leader 接收客户端请求,按顺序广播给所有 Follower。

ZAB 与 Raft 的区别:

  • ZAB 使用纪元号(Epoch)而非任期号(Term)。
  • ZAB 的 Leader 选举和日志同步是分开进行的。
  • ZAB 支持顺序读和最终一致读两种读模式。

9.3 全序广播

9.3.1 全序广播的定义

全序广播(Total Order Broadcast / Atomic Broadcast)是一种通信原语,保证:

  • 可靠性(Reliability):如果消息被一个正常节点投递,它最终会被所有正常节点投递。
  • 全序(Total Order):所有节点以相同的顺序投递所有消息。

全序广播等价于状态机复制(State Machine Replication):

  • 每个节点维护一个状态机(如数据库的状态)。
  • 所有节点以相同的顺序执行相同的消息(状态转换)。
  • 由于状态机是确定性的,所有节点最终达到相同的状态。

9.3.2 全序广播与共识的关系

全序广播和共识是等价的问题:

  • 如果解决了共识,就可以实现全序广播(通过共识决定每条消息的顺序)。
  • 如果解决了全序广播,就可以实现共识(通过全序广播提议一个值,第一个被投递的提议就是共识结果)。

这意味着 FLP 不可能定理同样适用于全序广播——在异步网络中,不可能同时保证安全性和活性。

9.3.3 全序广播的应用

全序广播是构建可靠分布式系统的基础:

  • 数据库复制:所有副本以相同顺序执行写入操作,保证数据一致。
  • 分布式事务:通过全序广播决定事务的提交顺序。
  • 区块链:区块链本质上是一个全序广播系统——所有节点以相同顺序处理交易,形成一致的账本。
  • 消息队列:Kafka 的每个 Partition 实现了全序广播(在单 Leader 的情况下)。

重要知识点

知识点 1:线性一致性的实现

实现线性一致性需要解决两个问题:

确定操作的顺序:所有节点需要就操作的顺序达成一致。

确保读取最新值:读取操作需要能看到最新的写入。

实现方式:

单 Leader 复制:

  • 所有读写都通过 Leader 处理。
  • Leader 按顺序处理请求,保证线性一致性。
  • 如果 Leader 故障,需要等待新 Leader 选出后才能继续服务。

多数 Quorum:

  • 写入需要多数节点确认,读取需要从多数节点读取。
  • 通过时间戳或序列号确定最新版本。
  • 不能保证严格的线性一致性(可能读到旧版本),但可以实现接近线性一致性的效果。

共识协议:

  • 使用 Paxos、Raft 等共识协议实现全序广播。
  • 所有操作通过全序广播排序,保证线性一致性。

知识点 2:Leader 选举

Leader 选举是共识算法的核心组件。当 Leader 故障时,Follower 需要选举新的 Leader。

Raft 的 Leader 选举流程:

Follower 在超时时间内没有收到 Leader 的心跳,转为 Candidate。

Candidate 增加任期号,向所有节点发送 VoteRequest。

节点在每个任期只能投一票,投给第一个请求投票的 Candidate。

获得多数票的 Candidate 成为新 Leader。

如果多个 Candidate 同时竞选,可能分裂投票,无人获得多数票。通过随机超时避免重复分裂。

Leader 选举的关键约束:

  • 只有拥有最新日志的节点才能成为 Leader(选举限制)。
  • 每个任期最多一个 Leader(避免脑裂)。

知识点 3:成员变更

分布式集群的节点可能动态变化——新节点加入、旧节点离开、节点故障。共识协议需要处理成员变更。

单步变更(Single-Server Change):

  • 一次只变更一个节点。
  • 新配置必须与旧配置有重叠(多数节点重叠),保证安全性。
  • 变更过程:先添加新节点(作为非投票成员),同步日志,然后将其加入投票集合。

联合共识(Joint Consensus):

  • Raft 的成员变更机制。
  • 变更期间同时使用旧配置和新配置,需要两个配置的多数节点都同意。
  • 变更完成后切换到新配置。

知识点 4:共识的性能

共识协议的性能瓶颈:

  • 网络往返:每次写入需要至少一次网络往返(Leader 到 Follower 再回来)。在跨数据中心场景中,延迟可能达到数十毫秒。
  • 磁盘写入:Leader 和 Follower 都需要将日志写入磁盘(fsync),磁盘 I/O 是瓶颈。
  • 多数 Quorum:需要等待多数节点确认,延迟取决于第 N/2+1 快的节点。

优化策略:

  • 批量提交:将多个请求打包成一批,减少网络往返和磁盘写入次数。
  • 并行复制:Leader 同时向所有 Follower 发送日志,不等待一个完成再发下一个。
  • 预写日志优化:使用组提交(Group Commit)将多个 fsync 合并为一次。

常见误区

误区 1:"最终一致性足够好"

纠正:最终一致性在很多场景下是不够的。例如:

  • 用户刚修改了个人资料,刷新页面后看到旧数据——用户体验差。
  • 两个用户同时修改同一数据,最终一致性可能导致一个用户的修改被覆盖——数据丢失。
  • 金融系统中,余额的读写需要强一致性,最终一致性可能导致透支。

选择一致性模型应该根据业务需求,而不是一味追求弱一致性。

误区 2:"Paxos 太复杂,不应该使用"

纠正:Paxos 确实难以理解和实现,但它是共识算法的理论基础。Raft 和 ZAB 都是 Paxos 的变体,理解了 Paxos 的思想,更容易理解其他共识算法。此外,大多数应用不需要自己实现共识算法——使用 etcd、Consul、ZooKeeper 等成熟的共识服务即可。

误区 3:"共识算法可以保证 100% 的可用性"

纠正:根据 FLP 不可能定理,共识算法在网络分区时可能无法达成共识(牺牲活性)。此外,共识算法需要多数节点可用——如果多数节点故障,系统无法继续服务。对于 N=3 的集群,最多容忍 1 个节点故障;对于 N=5 的集群,最多容忍 2 个节点故障。

误区 4:"线性一致性的代价太高,应该避免"

纠正:线性一致性的代价确实高,但在某些场景下是必要的。例如:

  • 分布式锁:需要保证同一时刻只有一个持有者。
  • Leader 选举:需要保证只有一个 Leader。
  • 余额扣减:需要保证不超扣。

对于这些场景,线性一致性的代价是值得的。对于其他场景(如社交动态、内容推荐),最终一致性可能更合适。

误区 5:"Raft 比 Paxos 更好"

纠正:Raft 和 Paxos 在功能上是等价的,都实现了共识。Raft 的优势在于可理解性和易实现性,但 Paxos 在理论上更通用(Multi-Paxos 可以优化为类似 Raft 的性能)。选择 Raft 还是 Paxos 主要取决于实现质量和生态系统,而非算法本身。


实践应用

实践 1:使用共识服务

大多数应用不需要自己实现共识算法,而是使用成熟的共识服务:

  • etcd:基于 Raft 的分布式 KV 存储,用于服务发现、配置管理、分布式锁。Kubernetes 使用 etcd 作为后端存储。
  • Consul:基于 Raft 的服务发现和配置管理工具,支持多数据中心。
  • ZooKeeper:基于 ZAB 的协调服务,用于分布式锁、Leader 选举、配置管理。Hadoop、Kafka 等系统使用 ZooKeeper。

使用共识服务的最佳实践:

  • 使用 Watch 机制:监听键的变化,而不是轮询。
  • 设置合理的超时:共识操作可能需要较长时间,设置合理的超时避免长时间阻塞。
  • 处理 Leader 变更:Leader 可能变更,客户端需要能够自动重定向到新 Leader。
  • 限制数据量:共识服务的数据存储在内存中,不适合存储大量数据。

实践 2:设计一致性策略

根据业务需求设计一致性策略:

识别关键数据:哪些数据需要强一致性?(如余额、库存、锁)

识别非关键数据:哪些数据可以容忍最终一致性?(如用户资料、社交动态)

混合策略:对关键数据使用强一致性(通过共识协议),对非关键数据使用最终一致性(通过异步复制)。

提供一致性选项:允许客户端在读取时指定一致性级别(如强读、最终读)。

实践 3:监控共识集群

共识集群的健康状态对整个系统至关重要。监控指标包括:

Leader 状态:当前 Leader 是谁?Leader 是否稳定(频繁切换说明有问题)?

日志复制延迟:Follower 的日志是否落后于 Leader?延迟多大?

心跳延迟:Leader 到 Follower 的心跳延迟是多少?

选举次数:选举是否频繁发生?(频繁选举说明网络不稳定或超时设置不合理)

节点状态:各节点是否正常运行?是否有节点频繁重启?

实践 4:处理网络分区

网络分区是共识系统面临的最严峻挑战。处理策略:

检测分区:通过心跳和投票超时检测网络分区。

降级服务:在分区期间,少数派分区无法选出 Leader,应该降级服务(只读或拒绝写入)。

多数派继续服务:多数派分区可以正常选出 Leader 并继续服务。

分区恢复:分区恢复后,少数派分区的节点重新加入集群,从 Leader 同步日志。

事后审计:记录分区期间的事件,分析是否有数据丢失或不一致。


本章小结

本章深入探讨了一致性与共识的核心概念:

一致性模型:

- 线性一致性:最强的一致性,保证所有节点看到相同的数据顺序。代价是延迟高、可用性低。

- 顺序一致性:保证存在全局顺序,但不要求与实时顺序一致。

- 因果一致性:保证因果相关的操作按正确顺序执行,并发操作可以乱序。

- 最终一致性:只保证最终达到一致,不保证何时一致。

共识算法:

- Paxos:理论基础,但难以理解和实现。

- Raft:与 Paxos 等价,但更易理解和实现。使用强 Leader 简化协议。

- ZAB:ZooKeeper 使用的协议,实现全序广播。

全序广播:

- 保证所有节点以相同顺序投递消息。

- 等价于共识和状态机复制。

- 是构建可靠分布式系统的基础。

关键组件:

- Leader 选举:当 Leader 故障时,Follower 选举新 Leader。

- 日志复制:Leader 将日志复制给 Follower,多数确认后提交。

- 成员变更:处理节点的动态加入和离开。

共识是分布式系统中最基本也最困难的问题。理解一致性模型和共识算法,是设计可靠分布式系统的关键。在实践中,大多数应用使用成熟的共识服务(etcd、Consul、ZooKeeper)即可,不需要自己实现共识算法。但对于系统架构师来说,理解共识的原理和限制,是做出正确架构决策的基础。

从工程实践的角度来看,共识算法的选择和部署需要特别注意以下几个问题。首先是集群规模的选择:3 节点集群可以容忍 1 个节点故障,5 节点集群可以容忍 2 个节点故障。更多的节点虽然提高了容错能力,但也增加了写入延迟(因为需要更多节点确认)和选举时间。对于大多数场景,3 或 5 节点的集群是最佳选择。

其次是跨数据中心部署的考量。如果将共识集群的节点分布在不同数据中心,需要仔细评估跨数据中心的网络延迟对共识性能的影响。通常建议将多数节点部署在同一个数据中心,以保证正常的写入性能;同时在不同数据中心部署少量节点,提供灾难恢复能力。这种"2+1"或"3+2"的部署模式在保证高可用的同时,也兼顾了性能。

最后,需要特别关注共识集群的监控和运维。共识集群的健康状态直接影响整个系统的可用性。建议建立完善的监控体系,包括 Leader 稳定性、日志复制延迟、选举频率、节点存活状态等关键指标。当检测到异常时,应该及时告警并启动应急预案,避免小问题演变为大规模故障。