CAP与分布式系统
参考:
https://ieeexplore.ieee.org/abstract/document/6133253
https://www.infoq.cn/article/cap-twelve-years-later-how-the-rules-have-changed/
CAP理论三个特性:
一致性(Consistency,C):所有节点在同一时间看到相同的数据。
高可用(High Availability,A):所有请求(读/写)都能在合理时间内得到响应,即使部分节点出现故障。系统始终对外提供服务,不拒绝请求,但响应结果可能是旧数据或临时状态。
分区容忍性(Network Partition tolerance,P):网络分区(Network Partition)发生时,导致部分节点之间通信中断,系统也不会停止服务,仍能继续运行。
“三选二”的表述过于简化,忽略了分区(P)的必然性
P 是 Network Partition tolerance,指的是要求分布式系统中存在部分节点、子网路无法访问时,系统依然能正常提供服务。
CAP“三选二”的说法本身具有一定误导性。CAP被广泛描述为“一致性、可用性、分区容忍性三者只能选其二”,但这种表述容易让人误以为分区(P)是一个可选项,甚至可以完全避免。
如果 P 可以舍弃,就有两种可能:
- 网路绝对可靠、系统中没有节点会出错(现实世界绝对不可能,除非单个节点)
- 当分区事件出现,系统不再正常提供服务(集群单个节点出现问题,整个系统停止)
实际上,分区(P)是分布式系统中必须接受的现实。网络故障、节点宕机、跨数据中心通信等问题在分布式系统中不可避免。因此,真正的选择是在一致性(C)和可用性(A)之间权衡,而不是“三选二”。
两个节点发生分区时且有具有分区容忍(P)的情况下,要么阻止数据写入(只读状态)以保证数据的一致性(CP系统);要么允许部分节点写入而导致产生数据的不一致性(AP系统),由此网络分区(P)下的CA几乎不能完成。
忽略了分区发生的频率和业务场景的灵活性
CAP理论被解读为“所有场景下只能选C或A”,但实际系统设计中,分区发生的频率远低于正常运行状态。
在大多数时间,系统可以同时满足CA:
当网络分区未发生时,系统可以追求强一致性和高可用性(CA)。例如,单数据中心内的数据库集群在正常运行时,通常可以同时满足CA。
分区时的动态权衡:
CAP理论的核心矛盾仅在分区发生时才显现。此时,系统需要根据业务需求选择CP或AP,而不是全局静态选择。
e.g.
Google Spanner通过强一致性(CP)设计,但在全球范围内极少发生分区(依赖专网和原子钟同步),因此在实际中可以“假装”是CA系统。
此外,当客户端和服务端发生分区时,高可用必是不存在的,服务无法对外提供访问。
忽视了C和A的可量化性
CAP理论被简化为“非此即彼”的二元对立(C vs. A),但实际上一致性和可用性都可以以不同粒度实现。
一致性(C)的层级:
从强一致性(线性一致性)到最终一致性,存在多种中间状态。例如,NoSQL数据库支持“最终一致性”,而区块链使用“拜占庭容错”算法。
可用性(A)的层级:
可用性并非100%的绝对值,而是可以量化为“99.99%可用”或“部分可用”。例如,12306订票系统在极端情况下允许“基本可用”,而非完全不可用。
e.g.
在电商系统中,购物车服务可以容忍短时间的数据不一致(AP),而支付服务必须保证强一致性(CP)。
忽略了网络延迟和分布式系统的复杂性
CAP理论的原始表述忽略了网络延迟和数据复制的时间开销,导致对一致性的理解过于理想化。
网络延迟的必然性:
即使在无分区的情况下,数据从节点A复制到节点B也需要时间。CAP理论中的“一致性”要求数据瞬间同步,这在现实中是不可能的。我们无法判断是网络延迟导致节点间无法通讯还是网络分区导致的。
此时有两个选择,一个是继续等待,另一个是忽略风险继续操作。如:Paxos将会无限地将决策进行延期(CP)。
PACELC理论(Partition-tolerance, Availability, Consistency Else Latency, Consistency)扩展了CAP,指出:
- 当发生分区时(P),在A和C之间权衡;
- 当无分区时(E),在延迟(L)和一致性(C)之间权衡。例如,MySQL在无分区时通过主从复制实现高一致性,但需要容忍一定的延迟。
忽视了工程实践中的折中方案
CAP理论被解读为“必须严格选择CP或AP”,但实际系统设计中存在大量折中策略。
混合架构:
系统可以针对不同模块选择不同的策略。例如,金融交易模块选择CP(强一致性),而推荐系统模块选择AP(高可用性)。
算法优化:
通过Paxos、Raft等共识算法,在分区发生时实现“最终一致性”,既保障可用性,又逐步恢复一致性。
管理网络分区
系统设计者的挑战在于缓和系统分区对一致性和可用性带来的影响。核心思路是显式地管理网络分区,不仅包括探测,还需要一个特定的恢复程序,以及一个计划来应对分区过程中系统不变性被打破的情况。这个管理过程有三个步骤:
- 特测到分区的发生
- 进入显式的分区状态,并限制部分操作
- 当通信恢复时,启动分区恢复过程
最后一步的作用是重建一致性,并为系统分区时程序造成的错误进行补偿。
分区的演化过程:正常的操作由一系列原子操作构成,因而分区总是出现在操作之间。一旦系统发现超时,便检测到了分区,而发现分区的这一侧进入分区状态。如果分区确实存在,那么分区的两侧都会进入分区模式,不过单侧分区也不是不可能。在这种情况下,对侧根据需要发起通信,本侧可能正常回复,也可能不需要回复,这两种情况都能保持一致性。不过,由于感知到分区的一侧有可能会发生不一致的操作,所以其必须进入分区模式。使用“众数”机制的系统就是单侧分区的例子。一侧拥有众数,可以正常工作,而另一侧不行。支持离线操作的系统显然有分区模式的概念,有些原子广播的系统,比如Java的JGroups也是一样。
设计系统中进行分区管理需要考虑的问题:
问题1:哪些操作可以继续,哪些操作在分区合并之后仍然保证其准确性?
问题2:分区恢复时如何合并分区?
问题3:如何补偿在分区阶段造成的错误?
e.g.
ATM机上的补偿问题
在ATM(自动柜员机)的设计中,强一致性看似符合逻辑的选择,但现实情况是可用性远比一致性重要。理由很简单:高可用性意味着高收入。不管怎么样,讨论如何补偿分区期间被破坏的不变性约束,ATM 的设计很适合作为例子。
ATM 的基本操作是存款、取款、查看余额。关键的不变性约束是余额应大于或等于零。因为只有取款操作会触犯这项不变性约束,也就只有取款操作将受到特别对待,其他两种操作随时都可以执行。
ATM 系统设计师可以选择在分区期间禁止取款操作,因为在那段时间里没办法知道真实的余额,当然这样会损害可用性。现代 ATM 的做法正相反,在 stand-in 模式下(即分区模式),ATM 限制净取款额不得高于 k,比如 k 为 $200。低于限额的时候,取款完全正常;当超过限额的时候,系统拒绝取款操作。这样,ATM 成功将可用性限制在一个合理的水平上,既允许取款操作,又限制了风险。
分区结束的时候,必须有一些措施来恢复一致性和补偿分区期间系统所造成的错误。状态的恢复比较简单,因为操作都是符合交换率的,补偿就要分几种情况去考虑。最后的余额低于零违反了不变性约束。由于 ATM 已经把钱吐出去了,错误成了外部实在。银行的补偿办法是收取透支费并指望顾客偿还。因为风险已经受到限制,问题并不严重。还有一种情况是分区期间的某一刻余额已经小于零(但 ATM 不知道),此时一笔存款重新将余额变为正的。银行可以追溯产生透支费,也可以因为顾客已经缴付而忽略该违反情况。
总的来说,由于通信的延迟,银行系统并不依靠一致性来获取正确性,而是基于审计和补偿。另一个例子是支票风筝(check kiting),意思是客户从多个支行取走现金,在它们彼此通信之前就溜之大吉。这种透支后面会被抓到,并导致法律层面上的补偿。
e.g.
飞机上刷Visa卡的过程可以被视为网络分区的一个体现,飞机在飞行中通常无法连接地面网络(如国际航班无蜂窝网络),导致支付终端与银行系统之间形成逻辑网络分区。其实这个过程与ATM机的例子差不多。
策略1:可用性优先(AP系统)
- 分区期间的操作:飞机上的支付终端可能允许乘客离线刷卡(如预授权模式),记录交易数据并暂存。例如,某些航班允许乘客购买餐食或商品,飞行前冻结一定金额,确保乘客在飞行中刷卡时账户余额足够。分区恢复后,银行根据实际消费金额调整冻结金额。(Pre-Authorization)。
- 分区恢复后的补偿:飞机着陆后,终端通过地面网络将交易数据批量上传至银行系统。若账户余额不足或卡片无效,银行需通过补偿机制修正错误。如:重复交易补偿、透支处理。
策略2:一致性优先(CP系统)
- 分区期间的限制:若系统强制要求实时验证(如高风险交易),则分区期间可能拒绝所有刷卡请求,牺牲可用性。例如,某些航班仅允许现金支付,或禁止高额度信用卡交易。