当前位置: 首页 > 科技观察

神一般的CAP理论应用在哪里?

时间:2023-03-20 02:04:23 科技观察

【.com原稿】对于开发或设计分布式系统的架构师和工程师来说,CAP是必须掌握的理论。图片来自PexelsBut:本文的重点不是讨论CAP理论和细节,而是谈谈CAP如何在微服务开发中起到引导作用。将通过几个微服务开发的例子来说明,尽量贴近开发。CAP定理,又称布鲁尔定理,是美国加州大学计算机科学家埃里克·布鲁尔提出的猜想,后来被证明是分布式计算领域公认的定理。但是,Brewer在提出CAP的时候,并没有对这三个CAP(Consistency、Availability、Partitiontolerance)进行详细的定义,所以网上对于CAP的解读也有很多不同的声音。CAP定理CAP定理有两个版本在发展,我们以第二个版本为标准:在分布式系统(指相互连接并共享数据的节点集合)中,当涉及读写操作,只能保证一致性、可用性和分区容忍度中的两个,必须牺牲另一个。这一版CAP理论讨论分布式系统,多强调了互联和数据共享两点。其实也澄清了第一版三选二的一些缺陷。分布式系统不一定具有互连和共享数据。例如,Memcached集群之间没有连接和共享数据。因此,Memcached集群等分布式系统不属于CAP理论的范畴,但MySQL集群是相互连接的,数据共享复制,所以MySQL集群是CAP理论的研究对象。一致性(Consistency)一致性是指写操作之后的读操作无论在哪个节点上都需要返回写操作的值。可用性(Availability)非故障节点在合理的时间内返回合理的响应。分区容忍度(PartitionTolerance)当网络被分区时,系统仍然可以继续旅行社的职责。在分布式环境中,网络不可能100%可靠,可能会出现故障,所以分区是一个必要的选择。如果选择CA而放弃P,如果发生分区,为了保证C,系统需要禁止写入,此时会和A发生冲突;如果是保证A,就会有正常的分区可以写入数据,而有的故障分区不能写入数据,就会和C发生冲突。所以分布式系统理论上不可能选择CA架构,但是必须选择CP或AP架构。分布式事务BASE理论BASE理论是对CAP的扩展和补充,是对CAP中AP方案的补充。甚至在选择AP方案的情况下,最终如何更好的做到C。BASE是BasicAvailable、FlexibleState和FinalConsistency这三个短语的缩写。其核心思想是即使无法实现强一致性,应用也可以采用合适的方式实现最终一致性。CAP在服务中的实际应用例子,似乎看不懂太多。项目的CAP可以参考李云华的《从零开始学架构》一书中的第21、22章,里面比较详细的描述了CAP的理论细节和CAP的版本。进化过程。这里重点介绍如何在我们的微服务中引导和应用神级CAP,大概会举几个常见的例子。服务注册中心,选CA还是CP?服务注册中心解决的问题在讨论CAP之前,先明确一下服务注册中心主要解决什么:服务注册:实例将自己的服务信息注册到注册中心。部分信息包括服务的主机IP和服务的端口,以及暴露的服务本身的状态和访问协议信息。服务发现:实例请求注册中心所依赖的服务信息。服务实例通过注册中心获取在其注册的服务实例的信息,并使用这些信息请求它们提供的服务。目前用作注册中心的一些组件大致有:Dubbo的ZookeeperSpringCloud的Eureka,ConsulRocketMQ的nameServerHDFSnameNode的nameServer,目前主流的微服务有Dubbo和SpringCloud,Zookeeper和Eureka用的最多.下面我们就来看看应该根据CAP理论上如何选择注册中心。(SpringCloud也可以用ZK,但不是主流,不讨论)Zookeeper选择CPZookeeper保证CP,即任何时候对Zookeeper的访问请求都能得到一致的数据结果,系统故障-容忍网络分段,但不能保证服务的每一次可用性。从实际情况分析,在使用Zookeeper获取服务列表时,如果正在选举ZK或者ZK集群中超过半数的机器不可用,则不会获取到数据。因此,ZK无法保证服务可用性。Eureka选择APEureka来保证AP。Eureka在设计时优先保证可用性,每个节点都是平等的。部分节点故障不会影响正常节点的工作,不会有类似ZK的leader选举过程。如果客户端发现无法注册或连接到某个节点,它会自动切换到其他节点。只要有一个Eureka,就可以保证整个服务可用,但是有可能这个服务的信息不是最新的信息。ZK和Eureka之间的数据一致性首先要搞清楚。Eureka最初是作为注册中心创建的,而ZK更多的是作为分布式协调服务存在。正是由于其特性,Dubbo赋予了注册中心更多的责任,保证其管辖的所有服务之间的数据(配置数据、状态数据)是一致的。所以也就不难理解为什么ZK设计成CP而不是AP了。ZK的核心算法ZAB是为了解决分布式系统中多个服务之间数据的一致性同步问题。更深层次的原因是ZK是按照CP原则构建的,这意味着它必须保持每个节点的数据一致。如果ZK下的节点断开连接或者集群中存在网络分段(比如交换机的子网之间无法互通),那么ZK会将它们从自己的管理范围中移除,外界无法访问这些节点,即使这些节点是健康的。提供正常的服务,所以这些节点请求会丢失。而尤里卡则完全没有这方面的顾虑。其节点相对独立,不需要考虑数据一致性问题。这应该是因为Eureka的诞生就是为注册中心设计的。与ZK相比,取消了Leader节点选择和事务日志机制,更有利于维护和保证Eureka运行的健壮性。下面来看看在注册服务中数据不一致会给Eureka带来哪些问题。无非就是某个节点注册的服务多,某个节点注册的服务少。在某个时刻,可能会导致某些IP节点被调用的次数较多,而某些IP节点的调用次数较少。也可能有一些本该删除却没有删除的脏数据。服务注册应选择AP或CP进行服务注册,对于同一个服务,即使注册中心不同节点存储的服务注册信息不同,也不会造成灾难性的后果。对于服务消费者来说,能消费才是最重要的。即使拿到的数据不是最新的数据,失败了消费者自己也可以尝试再试。总比拿不到实例信息整个服务不可用好,追求数据的一致性。所以对于服务注册来说,可用性比数据一致性更重要,所以选择AP。分布式锁,选CA还是CP?下面介绍分布式锁的三种实现方式:基于数据库实现分布式锁,基于Redis实现分布式锁,基于Zookeeper实现分布式锁,基于数据库实现分布式锁,建表结构:使用UNIQUEKEYidx_lock(method_lock)表的唯一主键。当执行锁定时,执行插入操作。如果成功进入数据库,则认为加锁成功。当数据库报Duplicateentry时,说明无法获取到锁。但是对于单主又不能自动切换主从的MySQL来说,实现P分区容错基本是不可能的(MySQL自动主从切换目前还没有完美的解决方案)。可以说这种方法强烈依赖于数据库的可用性。数据库写操作是单点的。一旦数据库挂了,锁就不可用了。这种方法基本上不在CAP的讨论范围之内。基于Redis实现分布式锁。Redis单线程串行处理自然解决了序列化问题,对于解决分布式锁来说再完美不过了。实现方法:setnxkeyvalueExpire_time获取锁时返回1,获取失败返回0。为了解决数据库锁无主从切换的问题,可以选择Redis集群或者Sentinel哨兵模式来实现主从故障切换。当Master节点出现故障时,Sentinel节点会从Slave中选出,重新成为新的Master节点。哨兵模式故障转移由哨兵集群进行监控和判断。当Maser异常时,复制暂停,重新选举新的Slave成为Master。Sentinel在重选时不关心主从数据是否有复制和一致。所以Redis的复制模式属于AP模式。为了保证可用性,在主从复制中,“主”有数据,“从”可能还没有数据。这时候,一旦master挂掉或者网络抖动等原因,就可能切换到“slave”节点。这时候两个业务线程可能同时获取到两个锁。流程如下:业务线程-1向master节点申请锁。业务线程1获取锁。业务thread-1获取到锁,开始执行业务。此时Redis刚刚生成的锁还没有在master和slave之间同步。Redis此时主节点挂掉Redis从节点,升级为主节点。业务线程2要新的主节点申请锁。业务thread-2获得新主节点返回的锁。业务线程2拿到锁,开始执行业务。当业务thread-1和业务thread-2同时执行任务时,上述问题不是Redis的缺陷,而是Redis采用AP模型,本身不能保证我们的一致性需求。Redis官方推荐Redlock算法保证,问题是Redlock至少需要三个Redis主从实例才能实现,维护成本比较高。相当于Redlock用三个Redis集群实现另一套共识算法,比较繁琐,业界很少使用。Redis可以作为分布式锁使用吗?这不是Redis本身的问题,而是要看业务场景。我们首先要确认我们的场景是适合AP还是CP。如果我们在社交发帖等场景中没有很强的事务一致性问题,Redis提供的高性能AP模型是非常适合的。但是如果是事务类型,是对数据一致性非常敏感的场景,我们可能需要寻找更合适的CP模型。刚才分析了基于Zookeeper的分布式锁的实现。Redis实际上并不能保证数据的一致性。我们先看看Zookeeper是否适合我们需要的分布式锁。首先,ZK模式是CP模式,即当提供ZK锁给我们访问的时候,在ZK集群中,可以保证锁存在于ZK的每个节点中。这其实是ZK的Leader通过两阶段提交写请求来保证的,这也是ZK集群规模大的瓶颈点。①ZK锁实现原理在说ZK锁问题之前,我们先了解一下Zookeeper中的几个特点。这些特性构建了ZK的分布式锁。ZK的特点如下:有序节点:在/lock等父目录下创建有序节点时,会严格按照lock000001、lock000002、lock0000003等节点顺序创建节点。有序节点可以严格保证每个自节点都是按照顺序命名和生成的。临时节点:客户端建立一个临时节点。当客户端的会话结束或会话超时时,Zookepper会自动删除该节点ID。事件监听:在读取数据的时候,我们可以在节点上设置监听。当节点的数据发生变化时(1节点创建,2节点删除,3节点数据变化,4自身节点变化),Zookeeper会通知客户端。结合这些特点,看看ZK是如何结合分布式锁的:业务线程1和业务线程2分别申请在ZK的/lock目录下创建有序的临时节点。业务线程-1抢到了/lock0001的文件,也就是整个目录中序号最低的节点,即线程-1拿到了锁。业务线程-2只能抓到/lock0002的文件,不是序号最小的节点,线程2获取锁失败。业务线程-1与lock0001建立连接并保持心跳,即锁的租约。当业务线程-1完成业务后,会释放与ZK的连接,即释放锁。②ZK分布式锁的代码实现ZK官方提供的客户端不支持直接实现分布式锁。我们需要自己编写代码来使用ZK的这些特性来实现。无论是使用CP还是AP的分布式锁,首先要了解我们使用分布式锁的场景,为什么要使用分布式锁,使用分布式锁解决什么问题。先说场景再说分布式锁选择技术。不管是Redis,还是ZK,比如Redis的AP模型会限制很多的使用场景,但是它是其中性能最高的。Zookeeper的分布式锁比Redis可靠很多,但是其繁琐的实现机制使其性能不如Redis,而且ZK会随着集群的扩大而退化得更多。简单来说,先了解业务场景,再选择技术。分布式事务,如何摆脱ACID加入CAP/BASE?说到事务,ACID是传统数据库常用的设计理念。它追求强一致性模型。关系型数据库的ACID模型,一致性+可用性高,分区难实现。微服务中已经不能支持ACID了,我们还是回到CAP来寻找解决方案,但是根据上面的讨论,在CAP定理中,要么只能使用CP,要么只能使用AP。如果我们追求数据一致性而忽略可用性,这在微服务中肯定行不通。如果我们追求可用性而忽略了一致性,那么一定有一些重要数据(比如支付、金额)存在漏洞,这也是不可能的。接受。所以我们想要一致性和可用性。都是不可实现的,但是我们能不能在一致性上做一些妥协,不追求强一致性,转而追求最终一致性,所以引入BASE理论。在分布式事务中,BASE最重要的是为CAP提出了最终一致性解决方案。BASE强调牺牲高一致性来换取可用性。数据在一段时间内是允许不一致的,只要保证最终的一致性即可。实现最终一致性弱一致性:系统不能保证后续访问返回更新后的值。只有在满足某些条件后才能返回更新后的值。从更新操作开始到系统保证任何观察者始终看到更新值的这段时间称为不一致窗口。最终一致性:这是弱一致性的一种特殊形式;存储系统保证如果一个对象没有新的更新,最终所有的访问都将返回该对象的最后更新值。BASE模型BASE模型与传统的ACID模型相反。与ACID不同,BASE强调牺牲高一致性来换取可用性。数据在一段时间内是允许不一致的,只要保证最终的一致性即可。BASE模型是一种反ACID模型,与ACID模型完全不同,牺牲了高一致性来换取可用性或可靠性:BasicallyAvailable。支持分区失败(例如sharding分片数据库)Softstate软状态,状态可以在一段时间内不同步,异步。Eventuallyconsistent,最终的数据是一致的,不是一直一致的。分布式事务在分布式系统中,要实现分布式事务,无外乎几种方案。方案不同,但都遵循BASE理论,即最终一致性模型:两阶段提交(2PC)补偿事务(TCC)本地消息表MQ事务消息①两阶段提交(2PC)和一个数据库XAtransaction,但是目前在真正的互联网中实际应用基本很少,两阶段提交就是利用了XA原理。在XA协议中,分为两个阶段:事务管理器要求事务中涉及的每个数据库预提交(Precommit)这个操作,并反映是否可以提交。事务协调器要求每个数据库提交数据或回滚数据。我来告诉你为什么在互联网系统中没有修改的二阶段提交在业界基本很少用到。最大的缺点就是同步阻塞的问题。资源就绪后,资源管理器中的资源一直处于阻塞状态,直到提交完成才释放资源。在互联网高并发、大数据的今天,两阶段提交已经不能适应现在互联网的发展。另外,虽然两阶段提交协议是为分布式数据的强一致性而设计的,但仍然存在数据不一致的可能。例如:在第二阶段,假设协调者发送了一个事务Commit通知,但是由于网络问题,该通知只有部分参与者收到并执行了Commit操作,而其余的参与者一直在事务中,因为他们没有收到通知处于阻塞状态,此时出现数据不一致。②补偿事务(TCC)TCC是一种面向服务的两阶段编程模型。每个业务服务必须实现三个方法:Try、Confirm和Cancel。这三个方法可以对应SQL事务中的Lock、Commit、Rollback。相比于两阶段提交,TCC解决了几个问题:同步阻塞,引入超时机制,超时后进行补偿,不会像两阶段提交那样锁定整个资源,将资源转化为业务逻辑形式,粒度变小.由于有补偿机制,可以由业务活动管理者进行控制,保证数据的一致性。Try阶段:Try只是初步确认的初步操作。其主要职责是完成所有业务检查和储备业务资源。Confirm阶段:Confirm是Try阶段检查完成后继续执行的确认操作。它必须满足幂等操作。如果Confirm执行失败,事务协调器会触发继续执行,直到满足为止。Cancel就是取消执行:如果Try失败,释放了Try阶段预留的资源,也必须满足幂等性,可能会像Confirm一样不断执行。一个下单生成订单抵扣库存的例子:接下来我们看看我们的下单抵扣库存的流程是如何添加到TCC中的:尝试的时候,库存服务会预留N个库存给这个订单使用,让订单服务生成一个“未确认”的订单,同时生成这两个预留资源。确认时,会使用Try中预留的资源。在TCC事务机制中,如果在Try阶段正常预留的资源可以在Confirm阶段全部提交。Try期间,如果其中一个任务执行失败,会执行Cancel接口操作,释放Try阶段预留的资源。这里不关注TCC事务是如何实现的,重点是分布式事务在CAP+BASE理论中的应用。实现请参考:https://github.com/changmingxie/tcc-transaction③LocalmessagetableLocalmessagetable该方案最初由eBay提出,eBay的完整方案:https://queue.acm.org/detail.cfm?id=1394128本地消息表的实现应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理。对于本地消息队列来说,核心是将大事务转化为小事务。还是用上面下单扣库存的例子:我们在创建订单的时候,新建一个本地消息表,将创建的订单写入和扣库存到本地消息表,放在同一个事务中(依赖于数据库本地事务以确保一致性)。配置定时任务轮询本地事务表,扫描本地事务表,将未发送的消息发送给库存服务。库存服务收到消息后,会减少库存并写入服务器的交易表,更新交易表的状态。库存服务器通过定时任务或直接通知订单服务,订单服务更新本地消息表中的状态。这里需要注意的是,对于一些扫描发送不成功的任务,会重新发送,所以必须保证接口的幂等性。本地消息队列基于BASE理论和最终一致性模型,适用于对一致性要求不高的场合。④MQ事务RocketMQ在4.3版本正式宣布支持分布式事务。在选择RokcetMQ做分布式事务时,请务必选择4.3以上的版本。分布式事务在RocketMQ中实现,其实就是对本地消息表的封装,将本地消息表移到MQ内部。作为一种异步保底交易,交易消息通过MQ异步解耦两个交易分支。RocketMQ事务消息的设计过程也借鉴了两阶段提交理论。整体交互流程如下图所示:MQ事务是对本地消息表的一层封装,将本地消息表移动到MQ内部,所以也是基于BASE理论,是一个最终的一致性模式,不需要强一致性。这样的高层事务是适用的,MQ事务把整个流程异步化了,也很适合在高并发的情况下使用。RocketMQ选择同步/异步刷机,同步/异步复制,其背后的CP和AP思考虽然同步刷机/异步刷机,同步/异步复制并不直接适用于CAP,但在配置过程中也涉及到可用性和一致性的考虑。同步刷/异步刷RocketMQ消息可以持久化,数据会持久化到磁盘。为了提高性能,RocketMQ尽量保证磁盘的顺序写入。当消息被Producer写入RocketMQ时,有两种写入磁盘的方式:异步刷新:消息快速写入内存中的Pagecache,并立即返回写入成功状态。当内存消息累积到一定程度,就会触发Uniformwritetodisk操作。这种方式可以保证高吞吐量,但也存在消息未保存到磁盘而丢失的风险。同步刷写:Pagecahe,消息快速写入内存,立即通知刷写线程刷盘,等待刷写完成,唤醒等待线程,返回消息写入成功状态。同步复制/异步复制一个Broker组有Master和Slave,消息需要从Master复制到Slave,所以有同步和异步两种复制方式:同步复制:反馈给Client写成功状态。异步复制:只要master写入成功,就可以将写入成功状态反馈给client。异步复制的好处是可以提高响应速度,但是是以牺牲一致性为代价的。通常,实现此类协议的算法需要添加额外的补偿机制。同步复制的优点是可以保证一致性(一般通过两阶段提交协议),但是开销大,可用性差(见CAP定理),带来更多的冲突和死锁等问题。值得一提的是,Lazy+Primary/Copy复制协议在实际生产环境中非常实用。RocketMQ设置要结合业务场景,合理设置刷盘方式和主从复制方式,尤其是SYNC_FLUSH方式,频繁触发写盘动作会显着降低性能。通常Master和Slave应该设置为ASYNC_FLUSH刷盘方式,Master和Slave之间配置SYNC_MASTER复制方式,这样即使一台机器出现故障,依然可以保证数据不丢失。总结一下,在微服务的构建中,永远逃不过CAP理论,因为网络永远不会稳定,硬件永远都会老化,软件也可能有bug。因此,分区容错是微服务中无法回避的命题。可以说只要是分布式,只要是集群都面临AP还是CP的选择,但是当你很贪心的时候,一致性和可用性都需要,所以只能在上面做一点妥协一致性,即引入BASE理论,在业务允许的情况下实现最终一致性。选择CA还是CP真的要看对业务的理解,比如钱,库存相关的会优先考虑CP模式,比如社区发帖相关的可以优先选择AP模式,这个说白了基于对业务的理解是一个选择和妥协的过程。作者:陈宇哲简介:十余年开发架构经验,国内较早的一批微服务开发实施者。曾在国内互联网公司网易、唯品会担任高级研发工程师,后在初创公司担任技术总监/架构师。目前在洋葱集团担任技术研发副总监。【原创稿件,合作网站转载请注明原作者及出处为.com】