跳到正文

CAP 与分布式共识 ​

一句话:网络分区是物理现实,CAP 说的是分区发生时在一致性和可用性之间选一个;PACELC 补充了「没有分区时,还要在延迟和一致性之间选」。

适用:etcd 3.5+、ZooKeeper 3.8+、Kafka 3.x、Nacos 2.x;组件行为随版本和配置变化,以官方文档为准。

为什么需要 ​

单机数据库只有一个副本,不存在「两个节点看到不同数据」的问题。一旦复制到多台机器,网络就可能把集群切成两半:一边以为对方挂了,另一边还在接受写入。此时系统必须决定:拒绝写入保住一致,还是继续写入保住可用。

共识算法要解决的是更具体的问题:多个节点如何对「同一份日志的顺序」达成一致,并且在部分节点故障时仍能继续。

核心机制 ​

CAP 与 PACELC ​

  • C(Consistency):这里指线性一致,读到的总是最新已提交值。
  • A(Availability):每个到达非故障节点的请求都能得到非错误响应。
  • P(Partition Tolerance):网络分区时系统仍能工作。

P 不是可选项。分区发生后,少数派一侧要么拒绝请求(选 C),要么继续服务但可能读到旧值/产生冲突(选 A)。CAP 不是「平时三选二」,而是「分区时 C 和 A 二选一」。

PACELC 把平时的情况也补上:If Partition, then A or C; Else, then Latency or Consistency。即使没有分区,同步复制也会增加延迟,异步复制会带来旧读;系统仍要在延迟和一致性之间做选择。

组件定位要带前提 ​

组件分区时平时前提与例外
ZooKeeper偏 C偏 C写要多数派;Follower 读可能读到旧值,需要 sync 才读到最新
etcd偏 C偏 C写要多数派;线性一致读要走 ReadIndex/lease
Consul偏 C偏 CCatalog 走 Raft;DNS/HTTP 缓存可能返回旧实例
Nacos可切换可切换临时实例走 Distro(AP),持久配置走 Raft(CP)
Eureka偏 A偏 A自我保护会保留过期实例,消费者要能容忍
Redis 主从偏 A偏 A(异步复制)WAIT 能减少丢失,但不等于线性一致;故障切换有窗口
Kafka可调可调acks=all + min.insync.replicas + 禁止 unclean 选举才偏 C
RocketMQ可调可调同步复制/DLedger 偏 C,异步复制偏 A

这张表只能当起点。同一个组件换个配置就可能换一侧,判断时要问:写入要等几个副本、读能不能落到落后副本、故障切换丢不丢未同步数据。

Raft:leader、term、log 与多数派 ​

Raft 把共识拆成三件事:

  • Leader:同一 term 最多一个,所有写请求先到 Leader。
  • Term(任期):单调递增的逻辑时钟。节点收到更大的 term 就更新自己;旧 Leader 发现更大 term 会退回 Follower。
  • Log:每条日志有 (term, index, command)。Leader 把日志复制到 Follower,多数派确认后才提交。

选举:Follower 在随机超时内没收到心跳,变成 Candidate,term +1,向其他节点拉票。拿到多数派选票才成为 Leader。随机超时是为了避免所有节点同时竞选。

复制:Leader 收到写请求,追加本地日志,发 AppendEntries 给 Follower;多数派持久化后,Leader 提交并返回成功。提交索引之前的日志不能被应用状态机执行。

多数派边界:

  • 3 节点容忍 1 个故障,5 节点容忍 2 个;4 节点也只能容忍 1 个,6 节点只能容忍 2 个。偶数节点不提升容错,只增加成本。
  • 少数派分区既选不出 Leader,也提交不了新日志;多数派一侧继续服务。
  • 旧 Leader 在被隔离的一侧可能还能响应读请求但不知道新 Leader 已提交的日志,所以线性一致读要走 ReadIndex/lease,不能直接读本地。
  • 只有当前 term 的日志能通过「多数派复制」直接提交;旧 term 的日志要等当前 term 的日志提交后间接提交。
  • 成员变更要一次只改一个节点,或用 joint consensus;不能同时替换多个节点。

BASE ​

BASE 是 CAP 的工程化表达:基本可用(Basically Available)、软状态(Soft state)、最终一致(Eventually consistent)。它承认中间状态和不一致窗口,用补偿、对账、幂等把系统收敛回正确状态。

最小上手 ​

用 etcd 观察集群和读写:

bash
export ETCDCTL_API=3
etcdctl endpoint status --write-out=table
etcdctl member list --write-out=table
etcdctl put /demo/key hello
etcdctl get /demo/key

endpoint status 的 IS LEADER、RAFT TERM、RAFT INDEX 能看出当前 Leader 和日志进度;member list 能确认节点数和健康状态。

Kafka 查看 ISR:

bash
kafka-topics.sh --bootstrap-server localhost:9092 \
  --describe --topic orders
# 关注 Replicas 和 Isr;Isr 少于 min.insync.replicas 时 acks=all 会失败

生产动作 ​

  • 节点数用奇数:etcd/ZooKeeper 生产至少 3 节点,跨机房用 5 节点;不要用 2 节点,容错没有提升。
  • etcd 参数:--heartbeat-interval=100(毫秒)、--election-timeout=1000(毫秒)是默认起点;跨机房要按 RTT 调大,否则会频繁重选。
  • etcd 容量:--quota-backend-bytes 默认约 2GB;接近上限会触发 NOSPACE 告警,必须做历史版本压缩和碎片整理。
  • Kafka:acks=all、min.insync.replicas=2、unclean.leader.election.enable=false 是偏一致的常见组合;可用性优先时再逐项放宽,并明确丢数据风险。
  • Redis:min-replicas-to-write 和 min-replicas-max-lag 可以降低写入未同步就故障的概率,但仍然不是线性一致。
  • 排障:频繁 Leader 切换先看网络 RTT、磁盘 fsync 延迟和 CPU;提交变慢看 WAL/日志盘是否饱和。

边界 ​

  • CAP 只讨论分区期间的一致性/可用性,不讨论延迟、吞吐和运维成本;PACELC 补了延迟,但仍不是性能模型。
  • CP 不等于「任何读都最新」:ZooKeeper Follower 读、etcd 普通读都可能落后,要看读模式。
  • AP 不等于「永远可用」:多数派丢失、磁盘满、进程崩溃仍然会不可用。
  • Raft 解决的是日志复制,不解决业务幂等、跨服务事务和重复消费。
  • 不要为了「看起来高可用」堆偶数节点;多数派成本上升,容错没有增加。

讲清楚 ​

CAP 的前提是网络分区一定会发生。分区时,少数派要么拒绝写入保住一致,要么继续服务但可能读到旧值,这就是 C 和 A 的选择。PACELC 补了一句:没有分区时,同步复制换一致性但增加延迟,异步复制换延迟但可能旧读。判断组件不能只看标签:ZooKeeper 的 Follower 读可能旧,Nacos 的临时实例和持久配置走不同协议,Kafka 要看 acks 和 ISR。Raft 靠 Leader、term 和日志复制工作,写要多数派确认才能提交,3 节点容忍 1 个故障,偶数节点不提升容错。共识只保证日志顺序,业务幂等和对账仍要自己做。

把一门技术讲透到既懂也会用