Eric Brewer 的 CAP 思维:为什么分布式系统要权衡取舍
以实用视角理解 Eric Brewer 的 CAP 定理:一致性、可用性与网络分区如何塑造分布式系统的设计决策。

为什么 CAP 成为常用的思维模型
当你在多台机器上保存相同的数据时,你得到的是更快的响应和更强的容错能力——但同时也带来了一个新问题:不一致。两台服务器可能收到不同的更新,消息可能迟到或根本丢失,用户可能根据命中的是哪个副本看到不同的结果。CAP 之所以流行,是因为它给工程师提供了一个清晰的方式来描述这种混乱的现实,而不是含糊其辞。
Eric Brewer(一位计算机科学家、Inktomi 的联合创始人)在 2000 年提出了这一核心想法,作为在故障情况下对复制系统的实用陈述。它迅速传播,因为它吻合了团队在生产环境中已经在经历的事情:分布式系统的失败并不只是“宕机”;它们会通过分裂而失败。
CAP 是一种故障视角,而不是功能清单
CAP 在事情出错时最有用——尤其是当网络不按预期工作时。在健康的日子里,许多系统看起来既一致又可用。真正的考验是当机器不能可靠通信时:你必须决定在系统分裂期间如何处理读写请求。
这个框架正是 CAP 成为常用思维模型的原因:它不讨论最佳实践;它逼你回答一个具体问题——在分区时我们会牺牲什么?
读完本文你将能做出的决定
到本文结尾,你应该能够:
- 识别何时你面对的是一个真正的 CAP 情况(复制 + 可能的通信中断)。
- 有意地选择在副本不能达成一致时,你的系统应优先保证 一致性(所有人看到相同的真相)还是 可用性(系统继续响应)。
- 将该选择与产品影响联系起来:用户的体验、你展示的错误、以及分区恢复后需要的修复措施。
CAP 持久不衰,因为它把模糊的“分布式很难”讨论,变成了一个你可以做出并捍卫的决定。
场景设定:复制与不一致的问题
分布式系统,用通俗的话说,就是 许多计算机试图表现得像一台。你可能在不同的机架、区域或云可用区中有几台服务器,但对用户来说就是“这个应用”或“这个数据库”。
我们为什么要复制数据
为了让共享系统在真实世界规模下工作,我们通常会复制:在不同机器上保存同一数据的多个副本。
复制流行有三个实用原因:
- 扩展性:更多机器可以承载更多流量。
- 性能:用户可以被附近的副本服务,减少延迟。
- 可靠性:一台机器死了,另一个副本可以继续提供服务。
到这里看起来复制像是直接的收益。问题在于,复制带来了一个新任务:让所有副本保持一致。
核心张力:副本会出现分歧
如果每个副本都能即时与其他副本通信,它们就能协调更新并保持一致。但真实网络并不完美。消息可能延迟、丢失或因故障而绕开路由。
当通信正常时,副本通常可以交换更新并趋于一致。但当通信中断(即便是暂时的)时,你可能会得到两个看似有效的“真相”版本。
例如,用户更改了送货地址。副本 A 收到了更新,副本 B 没有。现在系统必须回答一个看似简单的问题:当前地址是什么?
正常运行与故障运行的区别
这就是以下两种状态的不同:
- 正常运行: 副本可以协调;分歧主要是时间问题。
- 故障运行: 部分副本无法通信;分歧变得不可避免。
CAP 思维正是从这里开始:既然存在复制,在通信失败时产生分歧不是边缘情况——而是核心设计问题。
用通俗话说的 CAP:C、A 和 P
CAP 是一个用来描述当系统分布在多台机器(通常在多个地点)时用户实际感受的思维模型。它不是在评价“好”或“坏”的系统——而是你必须管理的张力。
一致性(C):我能看到最新写入吗?
一致性关乎达成一致。如果你更新了某项内容,下一次读取(无论从哪个副本)是否会反映该更新?
从用户角度看,这区别在于“我刚改了,大家都看到新值”与“有人在一段时间内仍看到旧值”。
可用性(A):我能得到任何回答吗?
可用性意味着系统对请求——读和写——返回成功结果。不是“尽可能快”,而是“不拒绝为你服务”。
在出问题时(服务器宕机、网络抖动),可用的系统会继续接受请求,即便返回的数据可能稍有过时。
分区容忍(P):当节点无法通信时怎么办?
分区就是网络分裂:机器在运行,但它们之间的消息无法通过(或到达太晚以至于无用)。在分布式系统中,你不能把这当作不可能事件——你必须定义在这种情况下的行为。
一个简单故事:两家店,一个库存
想象两家零售店都卖同一件商品并共享“1 件库存”。顾客在店 A 买走了最后一件,店 A 写入 inventory = 0。与此同时,网络分区使店 B 听不到这个消息。
如果店 B 选择保持可用性,它可能会售出一件它实际上没有的商品(在分区时接受销售)。如果店 B 强制一致性,它可能会拒绝售出直到确认最新库存(在分区期间拒绝服务)。
分区究竟是什么(以及为何不能忽略)
“分区”不仅仅是“互联网断了”。它是指系统的部分不能可靠地互相通信——即便每一部分仍可能正常运行。
在复制系统中,节点不断交换消息:写入、确认、心跳、领导者选举、读取请求。分区就是这些消息停止到达(或到达得太晚),从而导致对现实的分歧:“这个写入发生了吗?”“谁是领导者?”“节点 B 还活着吗?”
分区是通信故障
通信可以以混乱、部分化的方式失败:
- 丢包 触发重试和超时
- 路由问题 导致流量绕行或黑洞
- 链路过载(或网卡饱和)导致长延迟
- 防火墙/安全组配置错误 只阻断某些端口或方向
- DNS 或服务发现故障 导致节点找不到彼此
重要的一点:分区通常是降级而非干净的开/关。从应用角度看,“足够慢”与“宕机”往往无法区分。
在大规模下分区是不可避免的
随着你增加更多机器、更多网络、更多地域和更多移动部件,通信暂时中断的机会就更多。即便单个组件可靠,整体系统也会因为依赖更多东西而出现故障。
你不需要假设具体的故障率来接受现实:只要系统运行足够久并跨足够多的基础设施,分区就会发生。
实践中“容忍分区”意味着什么
容忍分区意味着你的系统被设计成在分裂期间仍然运行——即便节点无法达成一致或无法确认对方看到的内容。这就迫使你做出选择:要么继续服务(冒不一致的风险),要么停止/拒绝某些请求(保留一致性)。
关键时刻:在分区时选择一致性还是可用性
一旦你有了复制,分区就只是通信中断:系统的两个部分在一段时间内无法可靠通信。副本仍在运行,用户仍在点击,服务仍在接收请求——但副本不能就最新真相达成一致。
这就是 CAP 张力的一句话表述:在分区期间,你必须选择优先保证一致性(C)还是可用性(A)。你不能同时获得两者。
如果你选择一致性(C)
你的意思是:“我宁可正确也不愿响应。”当系统无法确认某个请求会让所有副本保持同步时,它必须失败或等待。
实际效果:一些用户会看到错误、超时或“请重试”的提示——尤其是对会更改数据的操作。这在你宁可拒绝一次支付也不愿重复扣款,或宁可阻止座位预订也不愿超卖时很常见。
如果你选择可用性(A)
你的意思是:“我宁可响应也不愿阻塞。”分区的各方会继续接受请求,即便无法协调。
实际效果:用户得到成功的响应,但读取的数据可能是旧的,并且并发更新可能产生冲突。你需要在事后依靠调和(合并规则、最后写入胜出、人工审核等)。
选择可以按操作变化
这不一定是一个全局设置。很多产品会混合策略:
- 读与写分开: 保持读取可用,但对写入更严格。
- 关键与非关键操作: 对金钱、身份和库存强制一致性;对 feed、分析、“点赞”或缓存的个人资料允许可用性。
关键时刻是按操作决定:现在阻塞用户更糟,还是以后修复冲突更糟?
常见误解:超越“选两个”的口号
“选两个”这句口号容易记,但常常误导人们以为 CAP 是个三项特征的菜单,你只能永远保留两项。CAP 讨论的是网络不配合时会发生什么:在分区(或任何类似分区的场景)期间,分布式系统必须在返回一致答案和对每个请求保持可用之间做出选择。
误解 1:“我会选择 C 和 A 并避免分区”
在真实系统中,分区不是可以关闭的设置。如果你的系统跨机器、机架、可用区或地域运行,消息可能会延迟、丢失、重排序或路由异常。从软件的角度看,这就是分区:节点无法可靠地就发生了什么达成一致。
即便物理网络没问题,其他故障也会产生相同效果——过载节点、GC 暂停、嘈杂的邻居、DNS 障碍、负载均衡器不稳定。结果相同:系统的部分无法足够好地协调。
误解 2:“分区是罕见的边缘情况”
应用不会把“分区”当成一个整洁的二元事件来体验。它们体验到的是延迟突增与超时。如果一个请求在 200 ms 后超时,不管数据包是否在 201 ms 到达,应用都必须决定下一步怎么做:从应用角度看,慢往往与坏并无二致。
误解 3:“系统要么是 CP,要么是 AP”
很多真实系统在不同配置和运行条件下会表现为大多数时间一致或大多数时间可用。超时、重试策略、仲裁大小和“读到自己的写”选项都能影响表现。
在正常条件下,数据库可能看起来强一致;在跨区故障或压力下,它可能开始拒绝请求(倾向一致性)或返回旧数据(倾向可用性)。
CAP 更多是帮助理解当分歧发生时你所作的权衡——尤其是当分歧由普通的“变慢”引起时。
你实际上可以选择的那些一致性选项
CAP 讨论常把一致性描述为二元:要么“完美”,要么“什么都行”。真实系统提供一系列保证,每种在副本分歧或网络断链时都带来不同的用户体验。
强一致性(以及它在故障时的代价)
强一致性(通常指“线性化”行为)意味着一旦写入被确认,之后的任何读取——无论命中哪个副本——都会返回该写入。
代价:在分区或有少数副本不可达时,系统可能延迟或拒绝读写以避免冲突。用户会感知到超时、“请重试”或临时只读行为。
最终一致性(用户可能注意到什么)
最终一致性 保证如果没有新的更新,所有副本最终会收敛。它并不保证两个同时读取的用户会看到相同的结果。
用户可能注意到:刚更新的头像“回退”,计数器滞后,或刚发出的消息在另一台设备上短时间内不可见。
有用的中间保证
你常常可以在不要求完全强一致性的前提下,获得更好的用户体验:
- 读到自己的写:在你更新后,你不会读到旧版本的自己数据。
- 单调读取:一旦你看到版本 N,就不会再看到 N-1。
- 因果一致性:如果事件 B 依赖于 A(例如在读完一条消息后回复),每个人都会先看到 A 再看到 B。
这些保证更贴近人的直觉(“别让我看不到自己的改动”),并且在部分故障下更容易维护。
根据期望挑选一致性等级
从用户承诺开始,而不是行话:
- 如果错误的读取会造成不可逆的伤害(资金流动、库存保留、权限更改),倾向于 更强的一致性,并接受临时不可用。
- 如果功能能容忍短暂分歧(点赞、浏览量、Feed 排序),通常选择 最终 或 因果一致性 更合适。
- 如果核心痛点是个人混淆(“我保存了——为什么看不到?”),优先考虑 读到自己的写 和 单调读取。
一致性是产品选择:先定义用户眼中什么算“错误”,然后选择能防止这种错误的最弱保证。
可用性是产品决策,不只是可用性指标
CAP 中的可用性并不是用来吹嘘的“可用率”指标(如五个九)——而是你对用户在系统不确定时会发生什么所作的承诺。
快速的成功 vs. 准确的成功
当副本无法达成一致时,你常常要在下列之间选择:
- 快速成功: 快速返回某个结果(即便可能是旧的)。
- 准确成功: 仅在能证明答案是最新时才返回。
用户会感受到“应用可用”与“应用正确”之间的权衡。两者都不是放之四海皆准的优劣;正确选择取决于“错误”的含义。稍旧的社交 Feed 令人恼火;过时的账户余额可能造成伤害。
“关闭失败” vs. “开放失败”
在不确定时,两种常见行为会出现:
- 关闭失败(fail closed): 拒绝请求(错误、超时、只读模式)。你保护了正确性,但用户可能被阻塞。
- 开放失败(fail open): 提供尽力而为的响应(缓存数据、局部副本、队列化写入)。你保护了流程,但可能显示不一致的结果。
这不是纯粹的技术决策,而是策略决策。产品需要定义什么可以接受,什么绝不能猜测。
部分可用仍然是可用
可用性很少是全有或全无的。在分区期间,你可能看到部分可用:某些区域、网络或用户组成功,而其他失败。这可以是刻意的设计(在本地副本健康的地方继续服务)或意外情况(路由不均、仲裁可达性不均)。
降级模式:保留核心,限制风险
一个实用的折中方案是 降级模式:继续提供安全的操作,同时限制高风险操作。例如,允许浏览和搜索,但暂时禁用“转账”、“修改密码”或其他对正确性和唯一性要求高的操作。
具体例子:将 CAP 选择映射到用例
CAP 在抽象上容易迷糊,直到你把它映射到分裂期间用户的体验:你是更愿意系统继续响应,还是更愿意停止以避免返回(或接受)冲突数据?
库存与订单:超卖风险 vs. 结账中断
想象两个数据中心在无法通信时都接受订单。
如果你保持结账流程 可用,每一端可能都会卖出“最后一件”,导致超卖。对于低价值商品,这可能可接受(补货或致歉),但对于限量发售会很痛苦。
如果你选择 一致性优先,在无法全局确认库存时你可能会阻止新订单。用户会看到“请稍后再试”,但你避免了无法履行的销售。
支付与余额:优先正确的模式(以及原因)
金钱是经典的“错了代价高”领域。如果两个副本在分裂期间各自接受取款,账户可能出现透支。
系统通常在关键写入上优先考虑 一致性:如果不能确认最新余额,就拒绝或延迟操作。你会以部分可用性(临时支付失败)换取正确性、可审计性和信任。
聊天、Feed、分析:带有轻微延迟的数据可用性
在聊天和社交 Feed 中,用户通常能容忍小范围不一致:消息延迟几秒到达,点赞数不准确,查看量稍后更新。
在这些场景下,设计为可用通常是好的产品选择,只要你明确哪些元素是“最终一致”的,并且能把更新合并好。
要点:你的权衡是商业决策
“正确”的 CAP 选择取决于错误的代价:退款、法律风险、用户信任或运维混乱。决定哪些可以接受暂时的陈旧,哪些必须在分区时关闭。
将你的权衡付诸实现的设计模式
一旦决定了在网络分裂时要做什么,需要一些机制把这个决定落到实处。这些模式在数据库、消息系统和 API 中频繁出现——即便产品方从不提“CAP”。
仲裁(Quorums):多数同意
仲裁就是“多数副本同意”。如果你有 5 份数据,法定多数是 3。
通过要求读/写联系多数副本,你减少了返回过时或冲突数据的概率。例如,如果写入必须被 3 个副本确认,就更难出现两个隔离组各自接受不同“真相”的情况。
代价是速度与可达性:如果你无法联系多数副本(因分区或故障),系统可能拒绝操作——在这种情况下选择了一致性而非可用性。
超时、重试与退避塑造感知可用性
许多“可用性”问题不是硬失败而是响应缓慢。设置较短的超时会让系统显得更灵敏,但也增加了把慢成功视为失败的概率。
重试可以从瞬时故障中恢复,但激进的重试会在服务本已吃紧时造成更大负载。退避(重试间隔逐步增长)和抖动(随机化)能防止重试引起流量峰值。
关键是把这些设置与承诺对齐:承诺“始终响应”通常意味着更多的重试与降级;承诺“绝不撒谎”通常意味着更严格的限制与清晰的错误。
当允许分歧时的冲突处理
如果你选择在分区期间保持可用,副本可能接受不同更新,事后必须合并。常见方法包括:
- 最后写入胜出(LWW):选时间戳最新的更新。简单,但若时钟不同步可能丢掉有效改动。
- 版本向量:附带一个小的“历史”以检测更新是否并发或谁覆盖谁。
- 合并规则:定义如何合并更改(例如购物车取并集;计数器相加;资料字段优先非空字段)。这通常在数据模型设计阶段就需要考虑。
幂等性:让重试变得安全
重试可能产生重复效果:重复收费或重复提交订单。幂等性可以防止这种情况。
常见模式是使用 幂等密钥(请求 ID)随请求一起发送。服务器保存第一次结果并对重复请求返回相同结果——这样重试能提高可用性而不破坏数据。
如何在真实环境中验证 CAP 假设
多数团队在白板上“选择”了 CAP 策略——然后在生产压力下发现系统行为不一致。验证意味着主动制造分歧场景并检查系统是否按设计反应。
有目的地测试分区(安全地)
你不需要真的砍断光缆就能学到东西。在预生产(并在生产中谨慎)使用故障注入来模拟分区:
- 黑洞流量 在特定服务或节点间丢弃包以模拟静默分裂。
- 阻断链路 通过在副本/地域间屏蔽端口或安全组规则来切断连接。
- 加入极端延迟和丢包 使超时和重试表现得像分区。
- 强制领导者隔离(例如把主节点与法定多数隔离)来观察系统是选择“一致”还是“可用”。
目标是回答具体问题:写入会被拒绝还是接受?读取会返回旧数据吗?系统会自动恢复吗,合并需要多长时间?
如果你想尽早验证这些行为(在还没把周边服务接好之前),快速搭建一个现实的原型很有帮助。例如,团队使用 Koder.ai 时,通常会快速生成一个小服务(常见为 Go 后端 + PostgreSQL 加上 React 前端),并在沙箱环境中迭代重试、幂等密钥和“降级模式”流程以验证行为。
监控能揭示 CAP 痛点的信号
传统的可用性检查捕捉不到“可用但错误”的行为。要跟踪:
- 按操作类型的错误率(读 vs 写 vs 条件更新)
- 过时读取指示器(读到自己的写被破坏、版本/ETag 不匹配、滞后指标)
- 副本分歧(复制滞后、应用失败计数、冲突率)
- 超时与重试(通常是即将发生分裂的第一个信号)
运行手册与用户沟通
运维需要预先决定分区发生时的操作:何时冻结写入、何时切换、何时降级功能、以及如何验证重新合并的安全性。
同时也要计划面向用户的行为说明。如果你选择一致性,提示可能是“我们目前无法确认你的更新—请重试。”如果你选择可用性,要明确告知:“你的更新可能需要几分钟才能在所有地方生效。”清晰的文案能减少支持负担并维护信任。
日常系统决策的实用 CAP 清单
在做系统决策时,CAP 最有用的就是一个快速的“分区发生时会坏什么?”审计工具——而不是理论辩论。在选数据库特性、缓存策略或复制模式前,用下面的清单快速自查。
1) 简短 CAP 清单
按顺序问自己:
- 什么必须是正确的?(例如“银行余额绝不能为负”、“库存不能超卖”、“权限必须准确”)
- 什么必须保持可用?(例如结账端点、登录、只读目录)
- 什么可以临时降级?(例如分析、推荐、头像、“最后在线”)
如果发生网络分区,你就在决定先保护哪一项。
2) 按数据类型和端点做出决定
避免使用诸如“我们是 AP 系统”的全局声明。改为按:
- 数据类型: 金钱 vs 点赞 vs 日志
- 端点: “下单” vs “查看订单” vs “跟踪发货”
示例:在分区期间,你可以阻止 payments 的写入(优先一致性),但让 product_catalog 的读取通过缓存继续可用。
3) 以具体术语定义“可接受的不一致”
用例子写下你能容忍的情况:
- 时间范围: “计数可能落后 5–10 分钟”
- 幅度: “低需求商品的库存可能误差 ±1”
- 字段级别: “运送 ETA 可过时;但订单总额不可过时”
- 用户可见文案: “显示‘待定’而不是确定状态”
如果你不能用简单例子描述不一致,就很难测试并解释事故。
4) 要点 + 后续阅读
- 分区会把“可选”保证变成必须做出的选择。
- 针对每个端点明确这些选择,并记录可接受的不一致。
配套的进一步主题包括:一致性共识机制(/blog/consensus-vs-cap)、一致性模型详解(/blog/consistency-models-explained)以及 SLO/错误预算(/blog/sre-slos-error-budgets)。
常见问题
CAP 帮助工程师思考什么问题?
CAP 是用于 复制系统在通信故障下 的思维模型。当网络变慢、丢包或分裂时,副本之间无法可靠达成一致,你必须在下面两者之间做出选择:
- 一致性(Consistency): 所有人看到相同的最新值
- 可用性(Availability): 系统继续返回成功响应
它把“分布式系统很难”这种模糊问题,转化为具体的产品与工程决策。
什么时候我真正处于 CAP 情况?
真正的 CAP 场景要求同时存在:
- 复制(同一数据可以由多个节点提供/接受操作)
- 存在现实的通信失败可能性(分区、超时、长延迟)
如果你的系统是单节点或不复制状态,CAP 的权衡就不是核心问题。
在真实系统中,什么算作网络分区?
分区是指系统的部分不能在可靠或在要求的时间范围内进行通信——即便每台机器仍在运行。
实际上,“分区”通常以以下形式出现:
- 触发超时的延迟突增
- 丢包或流量被黑洞丢弃
- 防火墙或路由配置错误
- 无法及时响应的过载节点
从应用的角度看,“太慢”往往等同于“宕机”。
从用户角度看,一致性和可用性有什么区别?
一致性(C) 意味着读取会反映任何已被确认的最新写入。用户体验是“我改了,大家都能看到”。
可用性(A) 意味着每个请求都会得到成功响应(不一定是最新数据)。用户体验是“应用继续工作”,但可能看到过时的结果。
在分区期间,通常无法同时对所有操作保证两者。
为什么我不能只选择一致性和可用性然后忽略分区?
因为在跨机器、跨机架、跨可用区或跨地域运行的分布式系统中,分区不是可选项。只要存在复制,就必须定义在节点无法协调时系统的行为。
因此,“容忍分区”通常意味着:当通信中断时,系统仍会有明确的运行方式——要么拒绝/暂停某些操作(偏向一致性),要么提供尽力而为的结果(偏向可用性)。
在分区期间,偏向一致性(CP)的表现是什么?
如果你偏向 一致性,通常会:
- 在无法确认达成一致时拒绝或延迟操作
- 对读写要求多数派/仲裁
- 展示超时、“请重试”或只读模式等错误信息
这类策略常见于资金划转、库存保留与权限变更等场景——在这些地方出错比短暂不可用更糟糕。
在分区期间,偏向可用性(AP)的表现是什么?
如果你偏向 可用性,通常会:
- 在分区的各方继续接受读写
- 允许副本暂时分歧
- 在事后进行合并(合并规则、冲突解决、人工审查)
用户会少见到硬错误,但可能看到过时数据、重复效果(如果没有幂等性)或需要清理的冲突。
我可以按操作混合一致性和可用性的选择吗?
可以对不同端点或数据类型做不同选择。常见的混合策略包括:
- 读取保持可用,写入更严格(浏览可用;危险更新可能失败)
- 关键操作关闭(fail closed)(支付、库存、认证),低风险功能打开(feeds、分析)
- 降级模式:保留安全操作,暂时禁用高风险操作
这避免了用单一“我们是 AP/CP”标签来错误地描述产品行为。
除了“强”和“最终”,我还能选择哪些一致性保证?
有用的保证选项包括:
- 强一致性(线性化):最高的正确性;在分区期间可能阻塞/失败
- 最终一致性:持续提供服务;副本会在没有新写入时收敛,用户可能短期内看到过时数据
- 中间级别保证:
- 读到自己的写入(read-your-writes):你看不到自己刚做的改动消失
- 单调读取(monotonic reads):一旦看到版本 N,就不会再看到 N-1
- 因果一致性(causal consistency):若事件 B 依赖 A,则大家会先看到 A 再看到 B
挑选能防止用户可见“错误”的最弱保证即可。
我如何在实践中测试与监控系统的 CAP 行为?
通过制造分歧可见的条件来验证:
- 在预生产(谨慎地在生产中)模拟分区/延迟:黑洞流量、阻断端口、加入极高延迟/丢包
- 验证行为:写入会被拒绝还是接受?读取会变旧吗?恢复与合并需要多长时间?
- 监控超出可用性的信号:
- 按操作类型的错误率(读 vs 写)
- 副本滞后/分歧/冲突率
- 超时与重试
还要准备运行手册与对用户的沟通文案,使之与选定行为(fail closed vs fail open)一致。