ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CAP定理速查:分布式存储选型与CP/AP决策指南

CAP定理速查:分布式存储选型与CP/AP决策指南 这问题我被人问过无数次新系统评审架构师指着架构图问“你这个存储到底是CP还是AP”会议室里一片安静有人小声接了句“都支持吧”。其实这个回答也不算错但“支持”和“你在分区时保哪个”是两件完全不同的事。CAP定理的麻烦之处在于它听起来只是三个字母的排列组合好像背下来就行可真到选型时90%的纠结都不在这三个字母本身而在你愿不愿意接受分区发生时的那个代价。这篇速查手册想做的事很简单帮你把CAP这层窗户纸捅破先讲清楚C、A、P三个字母到底在说什么再给出一张主流中间件的归属速查表最后给一套能直接拿去用的选型决策步骤。适合正在做分布式系统设计、准备系统设计面试、或者在为订单、库存、用户画像这类核心业务选存储的人收藏下次被问到CAP不用再支支吾吾。1. 先搞清楚CAP到底在说什么三选二其实是个伪命题CAP定理的原始表述来自Eric Brewer在2000年的分布式系统大会上的一个猜想两年后被证明。整个定理严格说起来只有一句话在一个分布式系统中当网络分区发生时你必须在一致性和可用性之间做一个选择不可能两者兼得。网络分区这个词听着抽象实际翻译过来就是分布式系统里的节点之间是靠网络通信的而网络这个基础设施根本不保证永远通。机房光缆被挖断、交换机故障、云厂商某个可用区瘫痪都可能导致一部分节点联系不上另一部分节点。只要系统分布在两台机器以上分区就是客观存在的物理现实它不是你可以在设计时忽略的东西。1.1 C、A、P分别是什么别被教科书绕晕一致性Consistency指的是所有节点在同一时刻看到的数据是相同的。用户写入了一个新值之后无论从哪个节点读都必须是这个新值不能出现有的节点还是旧值的情况。注意这里说的是“同一时刻”这是强一致不是“过一会儿就一致了”。可用性Availability指的是系统在接到请求时必须保证能够在合理时间内给出响应而且这个响应不能是“系统出错请稍后重试”这种错误。注意CAP里的可用性和平常说的“高可用”不是一回事。高可用通常指的是系统整体不宕机而CAP里的A侧重的是“每个请求都能收到一个非错误的回复”。分区容错性Partition tolerance指的是系统在网络分区发生的情况下依然能够继续运行、继续对外提供服务。1.2 为什么说“三选二”是伪命题绝大多数文章为了好记把CAP理解成“三者只能选两个”于是有了“CP系统”“AP系统”的说法。这个理解不算全错但不准确的地方在于它没有强调一个前提分区是必须容忍的。你先别管C和AP这个东西不是你要不要选而是它一直存在你必须接受它不接受也得接受。所以CAP真正的逻辑是网络必然会发生分区。分区一旦发生系统就分裂成了两个孤岛此时你只有两条路可以走一条路是保C放弃A。比如客户端向某个节点写数据这个节点发现没法把数据同步给其他节点于是拒绝写入请求告诉客户端“我现在写不了”。系统保持着一致但有一部分请求得不到有效响应这就是CP。另一条路是保A放弃C。节点收到写入请求后直接存下来哪怕知道数据暂时同步不到其他节点也先返回成功再说。系统始终能响应但此时你在不同节点上读到的数据可能不一致这就是AP。分区不发生的时候呢说实话无分区时所有节点通信正常C和A可以同时满足这也是为什么很多人平时根本察觉不到问题直到某次机房故障才暴露出系统的真实取舍。1.3 最常见的一个误区拿“最终一致性”往C上套我见过不少人在设计文档里写“我们选择最终一致性方案所以严格来说是CP系统”这个表述对最终一致性体系的整体目标是有误解的。实际上最终一致性是AP系统在分区之后为了修复数据分裂而采用的一种补偿机制。选AP意味着你承认分区的瞬间数据可能不一致之后想尽办法把它织补回一致的状态这个“织补过程”是最终一致性的价值。它不能反过来证明你是个CP系统。打个比方CAP讨论的是“两个人异地记账时电话突然断了怎么办”。CP方案是电话断了我就拒绝记账直到恢复通话并核对清楚AP方案是电话断了我也照样先记着回头再拿各自记的账本碰一次合并冲突。最终一致性是“回头碰账本”的那套流程不是第三种记账理论。2. 一张表看懂主流中间件哪些天生CP哪些天生AP到了选型这一步真正有用的不是再背一遍定理而是把心里那几个常用组件一张表摊开看清楚它们在网络分区时到底默认保哪个。我按自己这些年生产环境的经验把主流中间件的归属、一致性语义、分区时的表现整理成了一张速查表先说结论再逐个解释。中间件默认倾向分区时的行为典型场景ZooKeeper / etcdCP少数派拒绝读写多数派继续服务分布式锁、选主、元数据存储HBaseCPRegionServer分区后少数侧不可用海量结构化数据、需强一致的在线服务Cassandra / DynamoDBAP所有节点继续接受读写冲突后合并订单历史、消息、IoT、用户画像Redis Cluster偏AP多数侧可用少数侧拒绝写入缓存、会话、高并发读多写少Kafka特殊CPISR机制分区后少数侧不可用消息队列、事件流、日志收集Elasticsearch偏AP可用性优先副本同步放宽搜索、日志检索、OLAP分析2.1 ZooKeeper和etcdCP的正统代表ZooKeeper和etcd的底层都基于Raft共识算法核心思路是多数派写成功才算写成功。只要多数节点还活着系统就能继续对外服务少数派节点如果断网了它会拒绝客户端请求因为此时接受写入必然导致数据分叉后面很难收场。使用这两者的场景基本都是一旦数据错了就全盘错的元数据节点分布式锁的状态、选主结果、配置中心里的配置项、服务注册发现列表。这些数据平时读多写少但对一致性要求极高宁可让个别客户端请求失败重试也不能让两个节点抢到同一把锁。经验之谈etcd因为API更现代、运维更简单已经逐渐成为Kubernetes等云原生体系的事实标准ZooKeeper在很多老系统里仍然是刚需。选它们之前先想清楚你的业务能不能接受“分区时有一部分请求报错”如果能CP这条路就是通的。2.2 Cassandra和DynamoDBAP阵营的代表Cassandra采用Dynamo风格的gossip协议和最终一致性模型设计哲学是“任何时候都能写”。它的每个节点都是对等的没有绝对的leader客户端可以往任意节点写入节点的数据通过异步机制相互传播。分区发生时孤立的节点照样接受请求等分区恢复后再通过版本号、时间戳等手段合并冲突。这类系统的典型场景是那些即使数据暂时不一致也不会造成大问题的业务订单历史多一笔少一笔可以后补、消息记录重复消费可以幂等处理、用户画像标签晚几秒更新无所谓。Cassandra在写入上的水平扩展能力非常猛适合海量数据写入压力极大的场景。使用AP系统最怕的不是分区而是业务侧把“最终一致”错当成“反正不用管”。事实上AP系统需要你在应用层设计好冲突解决策略、幂等机制和补偿流程数据脏了得有办法清理这是选AP的真正门槛。2.3 Redis Cluster你以为它保C其实它保的是ARedis Cluster在CAP里是个很有意思的存在。从Redis 3.0引入Cluster模式开始它的设计目标是高可用和水平扩展。节点之间使用异步复制主节点写入后返回客户端成功同时异步把数据复制给从节点。当主从节点间发生网络分区时Redis Cluster的策略是如果某个分片的主节点和它的从节点失去联系从节点所在的多数派会选举新的主节点而旧主节点如果发现自己已经在少数派一侧会停止接受写入请求。这样既保证了多数派侧始终可用又避免了两个主节点同时写导致的数据分叉。严格说在主从切换的那一瞬间旧主节点还没有来得及同步的写入数据会丢失所以它算不上强一致更接近CP与AP之间折中但整体上偏AP。用Redis做缓存时你根本不用纠结这些问题缓存的核心就是允许过期、允许回源。但如果你打算把Redis当作数据库存订单、存余额就一定要意识到它主从切换丢数据的可能性务必设计好补偿机制或者直接用RedLock之类额外处理锁安全问题。2.4 Kafka用ISR机制在CP和AP之间走钢丝Kafka默认给每个分区配置多个副本但只有ISR同步副本集合里的副本才被允许参与leader选举。生产者发送消息时如果设置了acksall消息要写入所有副本才算成功这时Kafka的CP性质非常强分区leader和ISR中的剩余副本失联时这个分区不再接受写入。但实际生产环境里很少有人真的每条消息都等到所有副本同步完通常你会设置acks1或者acksall但开启min.insync.replicas。这样Kafka出现了延迟接受写入的空间leader接收消息后返回成功但部分副本还没同步完此时如果leader宕机消息就有丢失风险。所以Kafka的定位更像“在一致性和可用性之间可调”你需要根据消息的重要程度去调整副本配置。用Kafka最典型的实践是核心支付消息要求严格不丢acksall、min.insync.replicas2宁可吞吐低一点而日志流、监控指标这类数据acks1甚至0都行丢一点无伤大雅。2.5 Elasticsearch可用性优先但会给你视觉上的“一致”Elasticsearch在分区时的表现偏向AP。它有副本机制但写入时默认只要主分片写成功就返回副本写入是异步的。当主节点和副本节点之间发生网络分区时为了保证搜索和写入可用Elasticsearch倾向于让主分片继续接受写入副本分片的数据滞后并不会阻止请求处理。这导致一个搜索结果可能看起来永远读得到最新数据——因为你只访问到了主分片。但从副本分片路由过去的请求就会读到旧数据在搜索这种场景下几乎没人较真可如果你用Elasticsearch存订单做对账这个“看起来一致”的假象就会坑人。经验教训是Elasticsearch适合做检索、分析、日志这类对实时一致性不敏感的业务不适合做主存储源它更应该是业务数据库的下游索引数据由数据库异步同步过来。3. 从理论到决策分区前怎么设计分区后怎么办很多人在设计系统时默认“网络分区是小概率事件”于是干脆不去设置降级策略直到某天真的出现分区才发现系统在故障时的行为根本不在掌控之中。我从事故复盘的角度给各位提个醒网络分区并没有你想象中那么罕见跨可用区部署之后每一条链路抖动都可能是分区的诱因。云厂商的可用区之间走的是独立机房看似可靠实际上带宽拥塞、光缆被挖断、DNS故障甚至发布变更误伤网络配置都可能导致跨区通信中断。设计系统时你必须默认“分区一定会来只是时间问题”才能在它来的时候不手忙脚乱。3.1 同步复制与异步复制CP和AP的技术底座CP系统的底层逻辑几乎都是同步复制。以Raft算法为例leader节点接收到写入请求后会将日志复制给大多数follower等follower们确认落盘才向客户端返回成功。这个过程保证了分区发生时只有拥有最新日志的多数派节点可以继续选举出新的leader少数派无论写什么都无济于事。代价非常直观延迟变高。每次写入都要经过一轮或几轮网络往返跨可用区时尤其明显。AP系统的底层逻辑则是异步复制加冲突检测。节点之间同步数据是“后台任务”用户写入先落本地异步地向其他节点传播。分区发生时孤岛节点照常接受写入等网络恢复后再把对方缺失的数据同步过来。代价是数据可能出现冲突比如同一个用户的余额在两个节点上各被修改了一次。理解这一点后你会发现选CP还是选AP本质上是在选择一种“复制模型”要同步复制就别怕延迟和不可用要异步复制就必须构建一套冲突处理机制。很多组件号称“高可用、强一致”你往底层一看真正能扛住分区考验的还是Raft这类共识算法或者Paxos其他那些靠异步同步再号称强一致的都是在偷换概念。3.2 分区恢复之后才是真正考验系统的时候CP系统在分区恢复后比较简单少数派节点重新连回集群自动从leader同步缺失的数据追上进度后重新加入。棘手的是恢复期间你要确保丢失的写入请求得到了客户端重试否则用户虽然在分区时收到了错误但那个写入可能已经丢了。设计CP系统时客户端侧的自动重试和幂等操作是必不可少的配套工程。AP系统恢复后的工作量大得多。两个分区的数据都要合并Cassandra会用Last Write Wins之类的时间戳策略来自动覆盖但相同主键的并发写入可能丢失其中之一DynamoDB的冲突合并也是类似逻辑需要你提前想清楚这个业务是否允许丢旧保新。真正确保不丢数据的做法还是应用层的幂等设计和最终对账把消息队列里的记录、操作日志里的原始事件都保留下来恢复后用对账任务把差异补上。没有这套机制就盲目上AP系统迟早会在某个深夜被对账报表上的缺口吓得睡不着。3.3 故障半径架构设计比单纯选CP或AP更重要CAP的选型经常被当成一个非此即彼的决定仿佛整个系统要么全CP要么全AP。但真实业务的架构里不同数据、不同链路完全可以用不同策略关键是划分好故障半径。我最常用的设计方法是“分模块定级”核心资金链路支付、余额、订单状态用CP语义宁可故障拒绝也不能给用户一个错误数字非核心链路浏览记录、推荐位、操作日志用AP语义允许短时间不一致、允许丢一小部分数据至于那些本来就无所谓的统计数据用最终一致性就够。进一步的架构优化是“分区范围内的CP化”不要试图让全球所有节点保持强一致而是把强一致的范围收缩到一个小的选举组跨组之间用异步同步。比如把某个用户的订单数据限定在同一可用区组内做Raft同步跨可用区通过异步复制备份这样既控制了延迟又保持了核心数据在单区域内的强一致。故障半径越小系统的稳定性和性能越好这是我做架构设计时最核心的一条原则。4. CAP之外一致性等级、延迟预算与运维成本才是真正的胜负手我在评审系统设计的时候经常遇到一种情况候选人把“我们用了etcd所以是CP”写在方案里然后觉得这个问题就算回答完了。但CAP只是给定了一个约束范围选CP还是AP之后系统设计仍然有一大堆决定要下。换句话说CAP是起点不是终点。4.1 一致性本身是个光谱不是只有“一致”和“不一致”线性一致性是最强的一致性意思是所有操作看起来像在一台单机上按某个顺序执行任意时刻读到的都是最新写入的数据。顺序一致性和因果一致性稍弱一些它们关注的不是实时性而是操作顺序的合理性。再往下还有单调读一致同一会话内不会读到更旧的数据、会话一致性局限于连接会话内、最终一致性早晚会一致但不保证时间。你在绝大多数业务里根本不需要线性一致比如微博热度排序、推荐流、消息未读数会话一致性甚至最终一致性就已经足够。所以每次选型前先问业务自己这个数据被读错了会怎样如果答案是“用户刷新一下就好”那完全可以走AP链路不必为了一个不必要的线性一致把可用性和性能都搭进去。如果答案是“涉及金额、涉及权利归属、涉及流程状态”才需要严格考虑CP方案。4.2 延迟预算你愿意拿多少毫秒换一个“绝对正确”CP系统的强一致不是免费的。Raft类的多数派提交在最坏情况下每写一次都要经过leader到多数follower的网络往返跨可用区场景下延迟轻松超过几十毫秒如果follower分布在较远区域上百毫秒也很常见。有些业务对延迟极其敏感比如用户登录态、商品详情页缓存多50毫秒都可能导致转化率下降这种业务几乎不可能全量采用CP方案。AP系统在延迟上有天然优势写入本地即可返回代价是后续的同步和补偿。现实中大多数公司采用的其实是“分层混合”方案用CP系统保护最核心的状态机订单状态、支付状态、库存扣减用AP系统加速边缘数据用户行为、日志、搜索索引最后通过异步同步机制把AP数据回流到核心系统让整体逻辑不至于乱套。这个“冷热分层”的设计模式比在一张技术选型表里勾选CP还是AP重要得多。4.3 运维成本CP要防选举风暴AP要防数据腐烂选CP意味着你要和一个极其敏感的leader选举机制长期共处。网络抖动可能导致follower发起leader选举选举期间系统短暂不可用频繁的网络抖动甚至会让系统陷入长时间的选举风暴整个集群颤抖不止。运维CP系统需要精心配置心跳超时、选举超时、拉长毛刺容忍窗口还要做好客户端重试退避这些工作非常考验团队的运维能力。选AP运维压力不会消失只是换了个方向。你要面对的是持续不断的读后写冲突、Cassandra里的墓碑机制、DynamoDB中的版本向量、数据修复工具的定期巡检。没有这些配套运维随着时间推移AP系统的数据会越来越脏最终变成一团无法解释的烂账。所以不管是CP还是AP都需要运维投入。当年我在考量一个系统该不该换存储时最先算的不是它宣称的QPS和P99延迟而是“现在团队有没有人长期盯数据链路”如果没有哪怕这个系统理论上再优秀也要慎重。4.4 BASE理论最终一致性系统的落地配方讲CAP很难绕开BASE——Basically Available基本可用、Soft state软状态、Eventually consistent最终一致。BASE是对AP系统落地方式的一种概括系统整体保持可用数据状态允许是软性的、会变化的在时间窗口内趋于一致。落地BASE方案时一套标准的配方是事件驱动架构业务操作先记事件再通过消息队列异步更新各个下游避免直接跨系统调用。幂等消费消息可能重复投递消费者必须根据业务主键做幂等保证重复执行结果相同。对账任务定期扫描核心表和对账表找出差异并自动修复。补偿事务对于跨系统的长流程操作在某个环节失败后通过反向操作把之前已经生效的动作回滚。这套配方设计好了AP系统才算是真正有兜底而不只是“先记下来再说”。5. 速查决策框架六步判断你的系统该选CP还是AP最后把前面的内容落成一套可以照着执行的方法论。我给自己总结了一个“六步选型框架”这些年做系统审查都是先跑完这六步再动技术选型基本不会走偏。第一步问业务能不能接受旧数据。拿笔在纸上列一下当前这个数据的读请求如果偶尔读到一分钟前的快照用户会发现吗会造成资损吗会影响流程状态判断吗如果答案是“完全无感”直接进入AP赛道不用犹豫。第二步明确一致性等级。不要笼统地说“要强一致”把一致性等级细化是全局线性一致还是仅要求不出现环形依赖是会话内单调读还是最终一致即可这一步决定你对具体组件的选择标准。第三步评估故障场景和故障预算。列出你的系统可能遭遇的分区场景单机房宕机、跨区断网、交换机故障。估算每种场景的期望概率和影响时长。再问一个问题如果某类请求在故障高峰期拿不到正确数据业务能不能扛住十分钟这个时间就是你能接受的故障恢复目标。第四步反馈到延迟和吞吐约束。先算好当前业务的读写比例、峰值QPS、可接受的P99延迟。然后对照候选组件在CP/AP模式下的基准性能看看是不是有哪一个直接出局。记住理论上再完美的方案如果延迟达标不了也是纸上谈兵。第五步选择组件并做故障演练。把候选组件在测试环境里人为制造分区观察它的实际表现哪个节点还在响应数据读出来是否一致恢复后能不能自动追平这一步一定要做不能只信文档我见过太多组件在文档里写“strong consistency”真到故障演练时发现丢数据的丢数据、拒绝服务的拒绝服务。第六步设计降级与恢复流程。无论选了哪条路都必须配套设计好分区期间的行为客户端重试策略、降级开关、恢复后的数据校验、对账任务。把这一项写进系统的运行手册确保故障发生时值班人员能照着操作。为了让你在方案评审时能快速自检我整理了一份经验清单建议直接贴在你的设计文档末尾是否明确写出本系统在网络分区时优先保证C还是A是否定义了“可用”的含义是“所有请求返回合理响应”还是“核心链路返回合理响应”是否明确了一致性等级线性、因果、会话、最终是否定义了数据不一致的最大容忍时间是否设计了由AP数据引发的冲突解决策略是否已制定分区故障演练预案是否考虑了客户端侧的重试与幂等是否规划了对账与补偿任务我自己在这套框架上吃过不少亏。早年间做过一个用户钱包系统当时觉得“反正量不大用MySQL主从就够了”从来没有真正模拟过主库宕机后的行为。后来真的发生了一次机房级别故障从库顶上来之后一批流水对不上账凌晨四点爬起来用脚本逐条比对花了整整一天才把数据修干净。那次之后我给自己立了个规矩凡是涉及钱和状态的数据一律先按CP设计并在上线前完成分区演练凡是允许短期不一致的数据一律按AP设计并且把对账流程设计得比业务逻辑更严格。这个习惯让我后来的几次大故障都从容了不少。对你也是一样CAP不是一个面试背完就忘的知识点它是你每次做架构决策时都要面对的一张考卷。下一次有人问你这个系统是CP还是AP回答“我们是CP”或者“我们是AP”之前先想清楚分区那一刻会发生什么恢复那一刻你又要做什么。想明白了这两件事选型就已经成功了一大半。
返回列表