Zookeeper 是 CP 的还是 AP 的?
结论先行:ZK 是典型的 CP 系统。写操作必须在过半节点(quorum)确认后才返回,网络分区导致多数派无法形成时,少数派分区会拒绝服务——牺牲可用性(A)来保一致性与分区容忍(C、P)。读请求允许从 Follower 返回,因此可能出现短暂陈旧读,但它仍属于 CP 语义下的“顺序一致”。
一、quorum 的计算
| 集群规模 | 多数派 | 可容忍故障 | 说明 |
|---|---|---|---|
| 1 | 1 | 0 | 单点,仅开发用 |
| 2 | 2 | 0 | 比 1 台更差,绝不要用 |
| 3 | 2 | 1 | 最小生产规模 |
| 5 | 3 | 2 | 常见生产配置 |
| 7 | 4 | 3 | 再往上写性能下降明显 |
公式:quorum = n/2 + 1,可容忍故障 = n - quorum = (n-1)/2。偶数节点不增加容错,只增加同步开销。
二、分区时的两种结果
5 节点集群,网络分区成 3 + 2
分区 A(3 台,含 Leader) 分区 B(2 台)
quorum = 3 ≥ 3 ✔ quorum = 2 < 3 ✘
继续提供读写 无法选主,拒绝写
但 B 侧客户端会话超时 B 侧客户端收到连接失败
这就是“脑裂”不会造成双写的原因:少数派根本凑不齐票数,写提案无法提交。
三、读为什么可能“旧”
ZK 保证的是顺序一致性(Sequential Consistency)与单系统映像,而不是任意时刻的线性一致读。Follower 可能还没应用最新事务就响应读请求:
写 /config=v2 → Leader 提交 → Follower1 已同步,Follower2 未同步
读 /config → 打到 Follower2 → 返回 v1(陈旧读)
需要读到最新值时用 sync() 把当前连接追赶至 Leader 再读:
client.sync().forPath("/config");
byte[] data = client.getData().forPath("/config");
四、配置与选型
# zoo.cfg(5 节点)
tickTime=2000
initLimit=10
syncLimit=5
server.1=10.0.0.1:2888:3888
server.2=10.0.0.2:2888:3888
server.3=10.0.0.3:2888:3888
server.4=10.0.0.4:2888:3888:observer
server.5=10.0.0.5:2888:3888:observer
| 组件 | CAP 取向 | 说明 |
|---|---|---|
| ZooKeeper | CP | 注册中心场景下宁可短暂不可用,也不要错误地址 |
| Eureka | AP | 节点互相同步,分区时仍可读,接受不一致 |
| Nacos | 可配 | 临时实例 AP,持久实例 CP |
四、为什么注册中心宁可选 CP
| 取舍 | 后果 | 业务能否接受 |
|---|---|---|
| 选 CP(ZK) | 分区时少数派不可用,无法注册 | 已运行的调用不受影响,可接受 |
| 选 AP(Eureka) | 分区时仍可读,但可能拿到已下线地址 | 调用直接失败,需重试兜底 |
注册数据的“正确性”比“随时可写”更重要:错一个地址就是一次线上报错,短暂无法注册只影响新实例上线。
常见追问
- 追问:那为什么 Dubbo 用 ZK 做注册中心?注册数据变更不频繁,短暂不可用的代价小于拿到错误地址的代价;且消费端有本地缓存,ZK 抖动不影响已有调用。
- 追问:ZK 是强一致吗?写是线性一致的(经 Leader),读是顺序一致的(可能陈旧),严格说不是全局强一致。
- 追问:会话超时会怎样?临时节点被删除,注册信息消失,这是 ZK 感知实例下线的机制,也是“假死”误摘除的来源。
小结
- ZK 用 quorum 换一致性,分区少数派直接停服,因此是 CP。
- 奇数节点才有意义,5 节点容 2 台。
- 读可能陈旧,需要最新值时先
sync()。 - 注册中心选 CP 的代价是短暂不可用,收益是不返回错误地址。