关于《谈谈分布式系统的CAP理论》的几点看法

“尽信书不如无书”

每一次阅读,能够从中看到自己的不足,同时,能提出不一样的观点就更好了。不一样的观点被提出,不仅希望自己对文章内容理解从表面和认同转向深入和探索,也希望自己融入更多元的视角,而不是“死读书,读死书”。因此,围绕传统分布式系统的 CAP 理论,谈谈自己对数据一致性、容错、容灾的一些看法。



原文概梗和扩展阅读

  原文 CAP 理论概要


崔同学(地址:https://www.zhihu.com/people/cui-chang-ze)
AWS搬砖
405 人赞同了该文章
文章地址:https://zhuanlan.zhihu.com/p/33999708
文章转载于
HollisChuang's Blogwww.hollischuang.com/archives/666
作者HollisChuang

CAP理论作为分布式系统的基石,应该是每个入门分布式系统(包括区块链)的人都应该学习的内容,本文是我在学习本理论的一个记录,分享出来以节省大家查资料时间。2000年7月,加州大学伯克利分校的Eric Brewer教授在ACM PODC会议上提出CAP猜想。2年后,麻省理工学院的Seth Gilbert和Nancy Lynch从理论上证明了CAP。之后,CAP理论正式成为分布式计算领域的公认定理。

一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项


通过CAP理论,我们知道无法同时满足一致性、可用性和分区容错性这三个特性,那要舍弃哪个呢?

CA without P:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但其实分区不是你想不想的问题,而是始终会存在,因此CA的系统更多的是允许分区后各子系统依然保持CA。CP without A:如果不要求A(可用),相当于每个请求都需要在Server之间强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式。AP wihtout C:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。


对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9,即保证P和A,舍弃C(退而求其次保证最终一致性)。虽然某些地方会影响客户体验,但没达到造成用户流程的严重程度。


对于涉及到钱财这样不能有一丝让步的场景,C必须保证。网络发生故障宁可停止服务,这是保证CA,舍弃P。貌似这几年国内银行业发生了不下10起事故,但影响面不大,报道也不多,广大群众知道的少。还有一种是保证CP,舍弃A。例如网络故障事只读不写。


孰优孰略,没有定论,只能根据场景定夺,适合的才是最好的。


  ACID 扩展阅读


一个事务本质上有四个特点ACID:

  1. Atomicity原子性

  2. Consistency一致性

  3. Isolation隔离性

  4. Durability耐久性


ACID一致性是有关数据库规则,如果数据表结构定义一个字段值是唯一的,那么一致性系统将解决所有操作中导致这个字段值非唯一性的情况,如果带有一个外键的一行记录被删除,那么其外键相关记录也应该被删除,这就是ACID一致性的意思。


CAP理论的一致性是保证同样一个数据在所有不同服务器上的拷贝都是相同的,这是一种逻辑保证,而不是物理,因为光速限制,在不同服务器上这种复制是需要时间的,集群通过阻止客户端查看不同节点上还未同步的数据维持逻辑视图。


当跨分布式系统提供ACID时,这两个概念会混淆在一起,Google’s Spanner system能够提供分布式系统的ACID,其包含ACID+CAP设计,也就是两阶段提交 2PC+ 多副本同步机制(如 Paxos)



  BASE 扩展阅读



正因为 CAP 中的一致性和可用性是强一致性和高可用,后来又有人基于 CAP 理论 提出了BASE 理论,即基本可用(Basically Available)、软状态(Soft State)、最终一致性(Eventual Consistency)。BASE的核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方法来使系统达到最终一致性。显然,最终一致性弱于 CAP 中的 线性一致性。很多分布式系统都是基于 BASE 中的”基本可用”和”最终一致性”来实现的,比如 MySQL/PostgreSQL Replication 异步复制。

对于CAP一些不同的思考

由于我是个法学专业半路出家的程序员,理论不是我擅长,有不对和不专业的地方敬请大家指正。我主要是从自己设计、研发分布式系统的经验出发,谈谈自己对 CAP 概念不一样的感知和思考。


  关于 CAP 理论对象的定位是否应该升级?


窃以为这里 CAP 理论研究的对象是:数据库、软件、网络,用数据库的一致性、软件的高可用和网络的可访问性来阐释:如何设计好的分布式系统。当今(2022年)的分布式系统已经远超数据库、软件和网络的概念,再以这三者为对象讨论:什么是好的分布式系统是否有点儿过时了?是否可以把这些概念做一下升级呢?

比如 C 一致性是否可以升级为研究数据和服务的一致性,这里的思考主要有三。其一,当年,数据大多以数据库技术为基础进行存储、处理和供给,以数据库为一致性研究对象无可厚非。而今,数据不仅有数据库技术,还有对象存储技术、日志存储技术、流式数据引擎、流批一体式数据引擎、图数据引擎等等。其二,应该引入服务并确保其一致性。虽然,您可能会说服务是基于数据的,比如说抢购服务基于时间一致性、库存服务基于商品数量一致性等等,但是,不可否认有一些一致性无法简单用数据描述,比如逻辑一致性。当我们的服务处理数据的时候,数据的正确性是一个方面,另一面是逻辑一致性的保证。比如在平台应用中的规则部分、互动玩法的任务部分等等,数据很可能只是 Trigger 的功能,而逻辑才是需要确保一致性的部分,尤其是在一个复杂业务中局部规则发生改变导致依赖的规则时更甚。如果是一个复杂的分布式系统,服务的接口协议变更、服务升级、服务下线等等情况,会让服务一致性问题更加需要保障。最后,如果我们的服务是算法模型驱动的,且服务有在线训练的部分,那么权重的一致性也要考虑,这就是新的业务场景从外部向内施加的一致性要求影响。

是否可以把 A 的部分升级为“容错”、P 的部分升级为“容灾”?之所以这样升级,因为,A 的软件部分升级为容错就能够涵盖:网络延迟、中断等情况,而不仅仅指狭义的可用,毕竟可用在不同的软件环境和设计下是不同的。这里软件环境指:系统内核模块或设别驱动、 I/O 等底层能力、基础框架、业务应用等,软件设计指:有/无状态服务、高/低并发、大/小数据量、内存/网络/磁盘 I/O 密集等针对性设计,这些环境和设计下对“高可用”的解释是不同的。P 升级为“容灾”可以涵盖超出“数据分区”的概念,同时,容灾还能够涵盖那些必须一致和可用的场景,当我在设计“部署架构”的时候,会考虑:南北分布、同一运营商分布、同城分布、同机房分布等,南北分布对应骨干网络灾备能力、同一运营商分布对应运营商服务灾备能力、同城分布对应城市灾备能力、同机房分布对应集群灾备能力,灾备能力基础上考虑访问速度、带宽成本等问题,已经超出了“容灾”的范围,可以因成本制约唯有并入广义的“可用性”。

最后,CAP 升级为一致性(Consistency) 、容错(Fault tolerance)、容灾(Disaster Tolerance) 。但是,如果深入思考一下,我发现所谓一致性其实是包含在容错里的,因为,对一致性问题也是错误的一种,对一致性错误的容忍度就是在做容错。因此,最终在架构上需要关注的核心概念就剩下两个:容错(Fault tolerance)、容灾(Disaster Tolerance) 。不论是 ACID 或 CAP 围绕数据一致性的各种设计,实际上大部分是在做容错。举个例子,拿 ACID 里原子性 Atomicity 来说,原子性是为了让两个操作成为一个操作,避免在两个操作过程中产生数据变化从而出现错误,因此,这也是容错的一部分。



  云原生对容错、容灾带来的变化是什么?



由于整个软件工程的基础设施在云计算的加持下产生了巨大变化,因此,对于容错和容灾也不能直接套用过往的经验和方法,否则,要么会过度设计、要么会设计不足甚至错误。下面,简单分享一下我个人对云原生对容错、容灾带来变化的一些思考。


  • 云原生本质是什么?


有的观点说:云原生是一种基于云计算的弹性和分布式优势构建和运行程序的编程思想和设计、实现方法。有的观点认为:云原生就是基于云计算提供的能力构建应用程序并部署在云端。有的观点是:基于微服务、容器化、DevOps、持续交付为核心要素的软件开发模式。



这些观点都没有错,不同的目标、不同的视角和不同的应用场景下,都可以归纳云原生的定义。因为,软件工程乃至行业都在飞速发展和变化,其解决的问题随着时间推移愈加复杂。最初,使用 Pascal、Fortran、Basic 编程的时候,由于当时的程序大多是 CLI 输入和输出能力都比较匮乏,程序的功能也比较单一。在接触了 C/C++、VB.Net、C# 后,才对软件工程有了较为深入的理解。后来,在数据库技术盛行的时代,Oracle、MySQL 替代了 FoxBase 等电子表格,提供了关系型数据库能力、视图、事务等概念也极大降低了软件工程的复杂度。数据库技术之后就是服务技术的时代,互联网普及让软件从单机走向联网,网络编程技术加持下面向数据库编程转入面向服务编程。



面向服务编程带来了 CAP、ACID、BASE 想要解决的诸多问题,比如数据一致性问题。这个时代的服务一致性问题由于服务较简单,例如:状态、鉴权、数据请求等,都可以用数据一致性来解决,但是,服务是对数据加工处理后基于程序逻辑提供的,因此本质上还是有所不同,ORM 或 DataAdapter 除外。随着服务的增多,服务的托管因硬件更迭被摩尔定律推动的速度极快,自己购买并托管服务器被容器化技术打败,这才是云计算出现的根本原因:成本。不更新服务器会限制服务用户的能力,更新服务器又跟不上硬件迭代发展的速度,而容器技术把硬件标准化且隐去了,卖计算机到卖计算(当然还有存储),这一字之差使云计算的成本大幅降低。



由于老板觉得用云计算好,而程序员积累的技术生态和技术栈还停留在托管服务器时代,这中间的巨大的矛盾和需求推动了容器编排、微服务、DevOps 等技术的发展。有了这些云技术基础设施和云技术生态的双重加持,云原生应用设计、开发成本逐渐降低,老板心中的云计算成为了技术人员心中的云原生。所以,云原生本质上是把软件工程的技术栈和技术生态云化,并以此为基础的设计和构建软件工程之方法。



  • 软件工程技术栈云化对容错、容灾带来的改变是什么?


如果软件工程技术栈云化,基于这些云化的基础设施进行软件工程,最大的容错、容灾变化就在“可控性”上。表面上看可控性变差了,因为我把更多容错、容灾能力交给了云计算基础设施和云原生技术栈,因而无法直接管控。但是,事实上这些云计算基础设施和云原生技术栈,每天都在经历各种软件开发和应用场景的考验和挑战,这将使这些基础设施和能力更加成熟和稳健。就像使用开源框架,表面上看复杂的框架中任何的问题和 BUG 都会带来不可控性和风险,但流行的开原框架在各种 Issue 和 MergeRequest 以及各种应用场景的历练下,各种牛人 CodeReview,比自己开发的框架更稳健和可控。



既然云原生更加稳健和可控,我就不用关心容错、容灾了吗?非也。任何稳健和可控都是有代价的,比如对数据库的可用性做主从设计、备份冗余设计等,都是用额外成本来供给健壮性。云原生则用 SLA(服务水平协议)来描述自身的健壮性,更高的 SLA 可能需要支付更高的费用。但是,如果你深究 SLA 则会发现,不同类型的云原生能力或服务 SLA 是不同的,比如:消息队列的 SLA 和图片识别服务的 SLA 是不同的,人脸识别服务的 SLA 应用在登陆和安检等场景与应用在生成游戏 Avatar 的要求也是不同的,这里容错和容灾的关系就很微妙了,很难清晰的定义这些场景里 SLA 的意义,以及这些意义在容错和容灾分别带来的挑战和要求是什么?因此,需要针对具体的应用对 SLA 理解,然后针对性设计容灾能力。



此外,如果简单认为云原生可以不关注容灾,只根据使用场景和 SLA 来进行容错设计,那么最终会形成什么情况?拿交易支付为例,如果我的服务都在杭州的某个集群,而这个集群因为骨干网问题失联了,云原生的云计算供应商会帮助我把请求都路由到江苏宿迁的另一个集群,如果我使用了云原生的数据存储,那么数据也可以在另一个集群被一致性访问,如果我使用了云基础设施自己处理数据同步和一致性,将对这种情况束手无策,无法完成容灾或灾难恢复,我将丢失交易支付信息造成用户无法支付。因此,当我将整个应用都基于云原生技术栈进行设计和构建,则只需要关注容错部分,容灾部分通过 SLA 保证就足够了。如果云原生技术栈无法完成软件工程的设计和构建,则需要把基于云原生和非云原生的混合架构中容错和容灾的部分识别出来,分别进行特殊的设计和构建、部署,从而对非云原生的部分做好容灾。


  做容错和容灾的基本方法是什么?



一语以蔽之:我做容错和容灾的基本方法就是识别不确定性后改造它们以提供确定性。而这里所指的不确定性大体有两类,一类是错误,另一类是灾难。错误和灾难指什么?作为法学专业毕业的,举个法律上的例子:错误对应过失和灾难对应不可抗力,为什么这么说呢?在法律上对过失犯罪的定义有两种:过于自信的过失和疏忽大意的过失。过于自信的过失只能够预见结果的发生但轻信可以避免,疏忽大意的过失则是指应该预见结果的发生但没有预见到。



对于过于自信的过失比较好理解,就是在写代码的时候,虽然知道可能发生一些错误,但因自己凭经验判断这种异常情况和边界值不会发生,从而在设计和构建应用的时候,没有引入保护性编程、单元测试等预防手段,也没有进行错误捕获和自定义错误处理等保护措施。疏忽大意的过失则比较有趣,什么叫做应该预见结果的发生呢?首先应该是软件工程中的基本方法、理念、思想,例如:高内聚低耦合、单一职责等。如果以高标准来看,则是那些顶级程序员能够想见的都应该被预见,这显然有点难和理想化,按下不表,后面机器学习部份再说。



对于灾难在法律上指不可抗力,在软件工程里就是那些人力无法干预和改变的情况,比如:断网、断电、硬件故障等。这些不可抗力在法律上可以免除责任,但是,在软件工程里“不可抗力”很可能造成商誉损失、用户体验损失、收入损失等,因此,软件工程里需要付出适当的成本进一步降低这些问题发生的概率。而对于“付出多少?降低多少?”这种问题,要根据实际的投入产出比来计算和取舍。然而,云原生的供应商可以借助规模效应,在不同应用间平衡这些成本,因此具有巨大的成本优势。所以,云计算公司收入的来源之一是:容灾成本随用户规模扩大而下降,利用容灾价值创造巨大利润。



因此,容错和容灾的不确定性识别,首先可以从:过失——软件工程、不可抗力——云原生基础设施两个方面考量。先说不可抗力——云原生基础设施容灾能力部分,这部分的不确定性主要来源于:行业标准。假设对服务的容灾能力有特殊需求,如果出了问题比如云服务商关键基础设施挂了,如果该云服务商遵从行业标准,而不是只想着用云原生基础设施把我 Lock-in,我可以轻松迁移到另一个遵从行业标准的云服务商,继续对用户提供服务。反之,如果我选择的云服务商不遵从行业标准,就会给我带来巨大不确定性,我可能因为自己的业务不能在极端情况下迁移,而选择那些遵从行业标准的云服务商。



对于混合云则有所不同,某些应用使用的云基础设施可能更基础、不那么云原生。例如在自己处理数据存储时,不确定性在容灾层面需要考虑数据存储的冗余、日志和数据恢复能力等等。假设出现不可抗力的情况时,能够在另一个容器、集群上借助这些能力进行灾难恢复。这时的不确定性,不仅来源于对云服务商的选择,还来源于选择的云基础设施的弹性缩扩容能力、容器启动能力、日志和数据传输能力、冗余数据存储成本等。把灾难恢复能力的确定性设计和构建好,就是对灾难发生和其损害的不确定性的抗争,其间利弊权衡,既包含设计和构建软件工程的复杂度,还包含其耗费的成本。



容错则更加复杂,因为错误本就是个主观概念:代码出错、程序出错、组件出错、模块出错、子系统出错、系统出错等等,甚至一些情况本来没有出错,但在运行时用户使用是错误的。例如:正确的显示闹钟的时间调整,但给过去设置闹钟对用户来说是没有意义的,如果程序在用户设置闹钟时提供了对过去时间的选择,从程序本身来说是没有错的,但对于产品设计或交互设计来说却不正确,这就把错误的外延伸到产品和交互设计领域。因此,容错概念范畴内对错误的定义应该收敛至:程序无法继续执行、服务无法正常响应等等,需要从具体的业务场景中,根据实际功能要求来进行探查。



对于容错中错误的定义,除了上文向外扩展的看,还可以向内收敛的看。向内收敛的看,可以给错误下一个较为清晰的定义:系统、程序或用户抛出的异常,而容错则是:对系统、程序或用户抛出异常的容忍度。怎么理解呢?比如:系统异常是指程序的系统级函数调用发生错误,如下代码:


if ((pid = fork()) < 0){    // strerror 返回一个文本串,描述了和某个error值相关联的错误。    fprintf(stderr, "fork error: %s\n", strerror(errno));    exit(0)}


当调用 fork() 系统函数创建进程的时候,可能因为系统描述符满了,无法完成调用而产生异常。此时,系统会返回异常值来标识异常的类型等信息,这就是系统异常的一种。示例中用 pie<0 判断调用异常进行捕获,还有很多语言提供的异常捕获能力。捕获异常后进行的处理有很多类型,示例中的异常就可以用重试来提升容忍度,从而达到一定的容错效果。

容错处理有很多方法,比如写一个解压缩程序,无法启动更多进程解压缩,可以提示用户当前进程已满只用单进程解压。如果程序以启动多进程为前提再进行解压缩,程序就会向用户报错,表明自己因无法启动更多进程而无法解压缩。可见,这两个容错处理方法对错误的容忍度是不同的,前者容忍度更高,后者容忍度更低。那么,前者容错效果笔后者好么?表面上看是这样的,因为第一种容错方法保证程序继续执行。但是,仔细想想也不尽然,假如解压缩的是几 TB 的高清视频,如果单进程可能会耗费几个小时,而用户大概率无法容忍,可能容忍度比较差的后者,反而是“容错性”更好的设计实现方法也说不定。当然,也可以设计个弹窗让用户选择,是否忍受几个小时?还是去关闭几个应用再来解压缩?用户说了算,也是种容错的办法。

如果说系统异常容错处理是对系统抛出的异常进行容忍度的构建,那么,程序抛出异常时进行合理处理就是对程序的容错。同样的,用户抛出或反馈的异常也需要响应和处理从而实现容错。比如用户反馈、生成工单、接手工单并处理、改进程序容错处理或系统容错处理、发布并解决用户异常,虽然链路比较长,但本质上还是对异常的识别、理解和处理来提升容忍度的过程。因此,不管是系统、程序还是用户抛出的异常,异常处理增加容忍度就是把不确定性确定化的核心手段。在系统、程序或用户使用应用时,感受到不正常并抛出异常,才能感知/捕获错误,这个感知/捕获过程就把不确定性一步步缩小,并最终定格在容忍度设计上。用测试驱动开发、保护性编程等手段,可以预先设计和测试错误容忍度,并用系统机制、语言能力、自定义错误和异常处理等手段,最合理的“容忍”以提供容错性。只要在某个领域处理的异常足够多,在某个领域里容错设计和构建就能驾轻就熟。


  对容灾和容错的一点儿展望


如前所述:只要在某个领域处理的异常足够多,在某个领域里容错设计和构建的能力就足够强。假设,这里把程序员用算法模型替代会怎样?之所以在某个领域处理的异常足够多,是因为在这个领域写了或看了(CodeReview)足够多的代码,或者处理过足够多线上、线下的故障和问题,那么,就能够牢牢记住各种系统、程序和用户异常的情况和特点,未来在写代码的时候能够迅速对这些情况进行识别、反应、设计和构建对应的容错能力。因此,如果把这些过程对应的信息蒐集起来形成标注数据:xx异常对应xx类别和xx容错处理,这就像 VSCode 等 IDE 提供的代码模板一样,教会算法模型识别、反应、设计和构建对应的容错能力。



如果初期算法模型不是很精确,可以把这些能力放在 IDE 里,作为提高编码效率的手段,让程序员来决定选择哪种方式进行容错处理。程序员的选择可以帮助我不断校准模型算法,让模型算法识别、反应、设计和构建容错能力的召回率和准确率不断攀升。最终,有信心的把这些能力部署到线上,在线上的程序出现异常而没有被捕获、处理时,则由算法模型自动识别、反应、设计和构建容错能力,用容错能力帮助线上的程序进行容错处理,这或许就是:自愈合。



当然,这仅是自愈合的一种,有些简单的方法例如:自动执行预案、自动回滚等等,很多方法都可以基于规则得到良好执行,只有当这些规则发生冲突、无法判断等各种复杂情况下,我才需要考虑用算法模型自愈合。希望未来在这个方面能够有更进一步的研究和实践再分享出来,感兴趣的也可以联系我一起交流探讨。


团队介绍

我们是阿里巴巴-大淘宝技术-导购和营销产品(原频道与D2C智能团队),也是阿里经济体前端委员会智能化方向的核心团队,隶属于大淘宝技术(大淘宝技术,一支致力于成为全球最懂商业的技术创新团队,旗下包含淘宝技术、天猫技术等团队和业务,是一支是具有商业和技术双重基因的螺旋体)。

我们在 「杭州阿里巴巴西溪园区」 办公,
我们的定位是「用诗人的浪漫和科学家的严谨打造最懂 AI 的 smart and international 的前端团队」,
我们的使命是「前端智能让业务创新更高效」。

✿  拓展阅读


作者|甄焱鲲甄子
编辑|橙子君

本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

{{o.name}}
{{m.name}}

猜你喜欢

转载自my.oschina.net/u/4662964/blog/5567465