PACELC 定理

某团队在使用 Cassandra 构建实时数据服务时遇到了问题。

团队选择了 Cassandra 的 AP 模式(可用性优先),实现了高吞吐的数据写入。但产品经理反馈:用户写入数据后,查询有时会返回旧数据。

团队开始加长同步等待时间,系统延迟从 10ms 飙升到 200ms,吞吐量大幅下降。

这是一个典型的 "即使没有分区,也需要取舍" 的问题。这正是 PACELC 定理要回答的问题。

【架构权衡】 CAP 定理只考虑了分区(P)情况下一致性(C)和可用性(A)的取舍。但 PACELC 定理告诉我们:即使没有发生分区,系统在正常运行时,也需要在延迟(L)和一致性(E)之间做出取舍。


一、问题背景

1.1 CAP 的盲点

CAP 定理的假设:
├─ 网络分区会发生(P 是必选项)
├─ 分区期间:选择 C 还是 A
└─ 正常运行时:C 和 A 可以兼得

PACELC 的补充:
├─ 即使没有分区(P 不存在 = ELC 情况)
├─ 系统也需要在延迟(L)和一致性(E)之间取舍
└─ 原因:同步复制会增加延迟

CAP 的盲点示例

场景:两节点同步复制

无分区 + 同步复制:
├─ 延迟:高(等待两个节点都响应)
├─ 一致性:强

无分区 + 异步复制:
├─ 延迟:低(只等一个节点响应)
├─ 一致性:弱(主节点宕机会丢数据)

结论:即使没有分区,也需要取舍

1.2 PACELC 的定义

PACELC = PA + ELC

┌─────────────────────────────────────────────────────────────┐
│                    PACELC 定理                               │
│                                                              │
│  If there is a Partition (P),                               │
│      then one must choose between                           │
│          Availability (A) or Consistency (C)                │
│  Else (ELSE),                                               │
│      one must choose between                                 │
│          Latency (L) or Consistency (E)                     │
└─────────────────────────────────────────────────────────────┘

关键洞察:
├─ PA/PC:分区时的选择(与 CAP 一致)
└─ ELC:正常运行时,延迟和一致性不可兼得

【架构权衡】 PACELC 不是推翻 CAP,而是在 CAP 的基础上增加了一个维度:延迟。它告诉我们,分布式系统的取舍是一个二维问题,不是 CAP 的三元悖论。

二、方案对比

2.1 PACELC 的四种组合

┌─────────────────────────────────────────────────────────────┐
│                     分区期间(PA vs PC)                    │
│                                                              │
│  PC 系统:选择一致性,放弃可用性                             │
│  ├─ ZooKeeper、etcd                                         │
│  ├─ 分区时停止服务                                          │
│  └─ 延迟:高(等待多数派确认)                               │
│                                                              │
│  PA 系统:选择可用性,接受不一致                             │
│  ├─ Cassandra、DynamoDB                                     │
│  ├─ 分区时继续服务                                          │
│  └─ 延迟:中(异步复制)                                     │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                     正常运行(ELC)                          │
│                                                              │
│  低延迟 + 弱一致(EL):                                      │
│  ├─ Cassandra(最终一致)                                   │
│  ├─ Redis(弱一致写入)                                     │
│  └─ 适合:实时数据写入                                      │
│                                                              │
│  高延迟 + 强一致(EC):                                      │
│  ├─ ZooKeeper(线性一致)                                   │
│  ├─ etcd(线性一致)                                        │
│  └─ 适合:关键配置写入                                      │
└─────────────────────────────────────────────────────────────┘

2.2 常见系统的 PACELC 定位

系统分区选择正常运行选择描述
ZooKeeperPCEC始终强一致,延迟稳定
etcdPCEC始终强一致,延迟稳定
DynamoDBPAEL 或 EC可配置一致性级别
CassandraPAEL始终低延迟
HBasePCEC强一致,延迟高
KafkaPCEC强一致,延迟高
RiakPAEL最终一致
MongoDB(副本集)PCEC强一致
CockroachDBPCEL/EC 可调可配置

2.3 PACELC 的实际案例

案例一:Cassandra 的 EL 路径

Cassandra 的设计选择:

写入路径:
├─ 写入 CommitLog(持久化)
├─ 写入 MemTable(内存)
└─ 返回客户端(延迟 < 5ms)

读取路径:
├─ 查询 MemTable
├─ 查询 BloomFilter → 定位 SSTable
├─ 查询 SSTable
└─ 合并返回(延迟 < 10ms)

结论:Cassandra 始终选择低延迟,接受最终一致

案例二:ZooKeeper 的 EC 路径

ZooKeeper 的设计选择:

写入路径:
├─ Leader 接收请求
├─ Leader 广播到所有 Follower
├─ 等待多数派 ACK
└─ 返回客户端

读取路径:
├─ 可以在任意节点读取
├─ 节点保证读取到已提交的操作
└─ 延迟:1-5ms

结论:ZooKeeper 始终选择强一致,接受高延迟

【架构权衡】 PACELC 告诉我们:没有"又快又一致"的系统。如果你的业务既要求低延迟又要求强一致,你需要在架构上做额外的设计(如:读写分离、分层缓存)。

三、工程实践

3.1 根据业务选择 PACELC 路径

选择指南:

业务场景:低延迟优先
├─ 选择:PA + EL(AP 系统)
├─ 代表:Cassandra、DynamoDB
└─ 适用:实时数据、日志、物联网

业务场景:一致性优先
├─ 选择:PC + EC(CP 系统)
├─ 代表:ZooKeeper、etcd
└─ 适用:配置中心、分布式锁、事务

业务场景:可配置
├─ 选择:PA/PC 可切换 + EL/EC 可配置
├─ 代表:DynamoDB、MongoDB
└─ 适用:混合场景

3.2 延迟和一致性的实际权衡

// DynamoDB 的延迟-一致配置
public class DynamoDBService {

    // 强一致性读取:高延迟,但保证读到最新数据
    public Item getStrongConsistent(String tableName, String key) {
        GetItemSpec spec = new GetItemSpec()
            .withPrimaryKey("id", key)
            .withConsistentRead(true); // 强一致
        return dynamoDB.getTable(tableName).getItem(spec);
    }

    // 最终一致性读取:低延迟,可能读到稍旧数据
    public Item getEventuallyConsistent(String tableName, String key) {
        GetItemSpec spec = new GetItemSpec()
            .withPrimaryKey("id", key)
            .withConsistentRead(false); // 最终一致
        return dynamoDB.getTable(tableName).getItem(spec);
    }

    // 牺牲一把一致性读取:延迟和一致性的折中
    public Item getBoundedStaleness(String tableName, String key) {
        GetItemSpec spec = new GetItemSpec()
            .withPrimaryKey("id", key)
            .withConsistentRead(true)
            .withProjectionExpression("...")
            .withMaxNullChecks(10);
        return dynamoDB.getTable(tableName).getItem(spec);
    }
}

3.3 读写分离的 PACELC 分析

传统主从架构的 PACELC:

写操作(EC):
├─ 必须写入主库
├─ 同步到从库
└─ 延迟:高

读操作(EL):
├─ 可以从从库读取
├─ 异步复制
└─ 延迟:低

结论:主从架构天然是"写 EC + 读 EL"的组合

四、工程代价评估

维度PA + EL 系统PC + EC 系统
写入延迟
读取延迟
一致性保证最终一致强一致
可用性中(分区时低)
冲突处理需要不需要
运维复杂度高(需要冲突解决)

五、落地 Checklist

  • 业务分析:识别业务的延迟要求和一致性要求
  • 路径选择:根据业务选择 PA 还是 PC
  • 延迟监控:监控读写延迟,发现问题及时调整
  • 一致性监控:监控不一致率,设置告警
  • 降级设计:设计从高一致到低一致的降级路径
  • 测试验证:在压测中验证不同负载下的延迟和一致性行为

六、面试总结

PACELC 是 CAP 的扩展,它告诉我们:

  • 分区时:选择 PA 或 PC
  • 正常时:选择 EL 或 EC

理解 PACELC,才能理解为什么"没有又快又一致的分布式系统"。