分布式一致性:CAP理论
集中式应用进行服务化拆分后,必然会出现一个问题:如何保证各个节点(Node)之间的数据一致性?
比如以下场景:
用户首先发起一次更新操作,映射到节点A;然后,用户又做了一次查询操作,操作映射到了节点B,此时A和B的数据如果不一致,对用户来说就会造成困扰。
分布式系统为了提高可用性,必然会引入冗余机制(副本),而冗余便带来了上面描述的一致性问题。为了解决这类问题,加州大学伯克利分校的Eric Brewer) 教授提出了 CAP 猜想。2年后, Seth Gilbert 和 Nancy Lynch 从理论上证明了猜想的可能性。从此,CAP 理论正式在学术上成为了分布式计算领域的公认定理。
一、CAP三指标
CAP 理论是一个很好的思考框架,它对分布式系统的特性做了高度抽象,抽象成了一致性、可用性和分区容错性,并对特性间的冲突做了总结。一旦掌握它,我们自然而然就能根据业务场景的特点进行权衡,设计出适合的系统模型。
我们先来看看CAP理论中三个指标的含义。
1.1 一致性(Consistence)
一致性,指的是客户端的每次读操作,不管访问哪个节点,要么读到的都是同一份最新数据,要么读取失败。
注意,一致性是站在客户端的视角出发的,并不是说在某个时间点分布式系统的所有节点的数据是一致的。事实上,在一个事务执行过程中,系统就是处于一种不一致状态,但是客户端是无法读取事务未提交的数据的,此时客户端会直接读取失败。
CAP理论中的一致性是强一致性,举个例子来理解下:
初始时,节点1和节点2的数据是一致的,然后客户端向节点1写入了新数据“X = 2”:
节点1在本地更新数据后,通过节点间的通讯,同步数据到节点2,确认节点2写入成功后,然后返回成功给客户端:
这样两个节点的数据就是一致的了,之后,不管客户端访问哪个节点,读取到的都是同一份最新数据。如果节点2在同步数据的过程中,有另外的客户端来访问任意节点,都会拒绝,这就是强一致性。
1.2 可用性(Availability)
可用性,指的是客户端的请求,不管访问哪个节点,都能得到响应数据,但不保证是同一份最新数据。你也可以把可用性看作是分布式系统对访问本系统的客户端的另外一种承诺:我尽力给你返回数据,不会不响应你,但是我不保证每个节点给你的数据都是最新的。
这个指标强调的是服务可用,但不保证数据的一致。
1.3 分区容错性(Network partitioning)
分区容错性,指的是当节点间出现消息丢失、高延迟或者已经发生网络分区时,系统仍然可以继续提供服务。也就是说,分布式系统在告诉访问本系统的客户端:不管我的内部出现什么样的数据同步问题,我会一直运行,提供服务。
分区容错性,强调的是集群对分区故障的容错能力。 因为分布式系统与单机系统不同,它涉及到多节点间的通讯和交互,节点间的分区故障是必然发生的,所以在分布式系统中分区容错性是必须要考虑的。
既然分区容错是必须要考虑的,那么这时候系统该如何运行呢?是选择一致性(C)呢,还是选择可用性(P)呢?这就引出了著名的“CAP不可能三角”。
二、CA/CP/AP选择
所谓“CAP不可能三角”,其实就是CAP理论中提到的: 对于一个分布式系统而言,一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)3 个指标不可兼得,只能在 3 个指标中选择 2 个。
上面说过了,因为只要有网络交互就一定会有延迟和数据丢失,而这种状况我们必须接受,同时还必须保证系统不能挂掉,所以节点间的分区故障是必然发生的。也就是说,分区容错性(P)是前提,是必须要保证的。
所以理论上CAP一般只能取CP或AP,CA只存在于集中式应用中。
2.2 AP架构
当选择了可用性(A)的时候,系统将始终处理客户端的查询,返回特定信息,如果发生了网络分区,一些节点将无法返回最新的特定信息,它们将返回自己当前的相对新的信息。
如下图,T1时刻,客户端往节点A写入Message 2,此时发生了网络分区,节点A的数据无法同步到节点B;T2时刻,客户端访问节点B时,节点B将自己当前拥有的数据Message 1返回给客户端,而实际上当前最新数据已经是Message2了,这就不满足一致性(C),此时CAP三者只能满足AP。
注意:这里节点B返回的Message 1虽然不是一个”正确“的结果,但是一个”合理“的结果,因为节点B只是返回的不是最新结果,并不是一个错乱的值。
三、总结
-
CA 模型:在分布式系统中不存在,因为舍弃 P,意味着舍弃分布式系统,就比如单机版关系型数据库 MySQL,如果 MySQL 要考虑主备或集群部署时,它必须考虑 P。
-
CP 模型:采用 CP 模型的分布式系统,一旦因为消息丢失、延迟过高发生了网络分区,就影响用户的体验和业务的可用性。因为为了防止数据不一致,集群将拒绝新数据的写入,典型的应用是 ZooKeeper,Etcd 和 HBase。
-
AP 模型:采用 AP 模型的分布式系统,实现了服务的高可用。用户访问系统的时候,都能得到响应数据,不会出现响应错误,但当出现分区故障时,相同的读操作,访问不同的节点,得到响应数据可能不一样。典型应用就比如 Cassandra 和 DynamoDB。
最后,关于CAP 理论有个误解:就是认为无论在什么情况下,分布式系统都只能在 C 和 A 中选择 1 个。 事实上,在不存在网络分区的情况下(也就是分布式系统正常运行时),C 和 A 能够同时保证。只有当发生分区故障的时候,也就是说需要 P 时,才会在 C 和 A 之间做出选择。
所以,我们在进行系统设计时,需要根据实际的业务场景, 在一致性和可用性之间做出权衡。
分布式一致性:BASE理论
我提到分布式系统理论上只能取CP或AP,如果要实现强一致性必然会影响可用性。但是,大多数系统实际上不需要那么强的一致性,而是更关注可用性。
比如一个3节点的集群,假设每个节点的可用性为 99.9%,那么整个集群的可用性为 99.7%,也就是说,每个月约宕机 129.6 分钟,这对于很多系统是非常严重的问题,所以生产环境,大多数系统都会采用可用性优先的 AP 模型。
在对大规模分布式系统的实践过程中,eBay 的架构师 Dan Pritchett 提出了BASE理论,其核心思想就是:
即使无法做到强一致性(Strong Consistency,CAP 的一致性就是强一致性),但分布式系统可以采用适合的方式达到最终一致性(Eventual Consitency)。
Base 理论是 CAP 理论中的 AP 的延伸,是对互联网大规模分布式系统的实践总结,强调可用性。BASE 是 基本可用(Basically Available) 、软状态(Soft-state) 和 最终一致(Eventually Consistent) 三个短语的缩写:
一、基本可用
在BASE理论中,基本可用是说,当分布式系统在出现不可预知的故障时,允许损失部分功能的可用性,保障核心功能的可用性。
具体来说,你可以把基本可用理解成,当系统节点出现大规模故障的时候,比如专线的光纤被挖断、突发流量导致系统过载,这个时候可以通过服务降级,牺牲部分功能的可用性,保障系统的核心功能可用。
1.1 实现方式
实现分布式系统基本可用的手段有很多,常见的有:
- 流量削峰
- 请求排队
- 服务降级
- 服务熔断
所以,基本可用在本质上是一种妥协,也就是在出现节点故障或系统过载的时候,通过牺牲非核心功能的可用性,保障核心功能的稳定运行。
二、最终一致
最终一致性是指,分布式系统即使无法做到强一致性,但应当根据自身业务特点,采用适当的方式在一定时限后使各个节点的数据最终能够达到一致的状态。这个时限取决于网络延时,系统负载,数据复制方案设计等等因素。
几乎所有的互联网系统采用的都是最终一致性,只有在实在无法使用最终一致性,才使用强一致性或事务,比如,对于决定系统运行的敏感元数据,需要考虑采用强一致性,对于涉账类的支付系统或清算系统的数据,需要考虑采用事务。
我们可以将CAP理论中的“强一致性”理解为最终一致性的特例,也就是说,你可以把强一致性看作是不存在延迟的一致性。在实践中,你也可以这样思考: 如果业务功能无法容忍一致性的延迟(比如分布式锁对应的数据),就实现强一致性;如果能容忍短暂的一致性延迟(比如 QQ 状态数据),则可以考虑最终一致性。
2.1 实现方式
那么如何实现最终一致性呢?你首先要知道它以什么为准,因为这是实现最终一致性的关键。一般来说,在实际工程实践中有这样几种方式:
-
读时修复
:在读取数据时,检测数据的不一致,进行修复。比如 Cassandra 的 Read Repair 实现,具体来说,就是向 Cassandra 系统查询数据的时候,如果检测到不同节点的副本数据不一致,系统就自动修复数据; -
写时修复
:在写入数据时,检测数据的不一致,进行修复。比如 Cassandra 的 Hinted Handoff 实现,具体来说,就是Cassandra 集群的节点之间远程写数据的时候,如果写失败就将数据缓存下来,然后定时重传,修复数据的不一致性。 -
异步修复
:这个是最常用的方式,通过定时对账检测副本数据的一致性,并修复。
注意,因为写时修复不需要做数据一致性对比,性能消耗比较低,对系统运行影响也不大,所以许多开源框架都是用这种方式实现最终一致性的。而读时修复和异步修复因为需要做数据的一致性对比,性能消耗比较多,所以需要尽量优化一致性对比的算法,降低性能消耗,避免对系统运行造成影响。
在实现最终一致性的时候,一般要实现自定义写一致性级别(All、Quorum、One、Any), 比如Elasticsearch在进行索引数据同步时,就支持各种写一致性级别。
三、软状态
软状态,描述的是实现服务可用性的时候系统数据的一种过渡状态,也就是说不同节点间,数据副本存在短暂的不一致。
比如,分布式存储中一般一份数据至少会有N个副本,允许系统在不同节点的数据副本之间进行数据同步的过程中存在延时。mysql replication的异步复制也是一种体现。
这里,我们只需要知道软状态是一种过渡状态就可以了,BASE理论的重点是基本可用以及最终一致性。
四、总结
BASE 理论是对 CAP 中一致性和可用性权衡的结果,它来源于对大规模互联网分布式系统实践的总结,是基于 CAP 定理逐步演化而来的。它的核心思想是:如果不是必须的话,不推荐实现事务或强一致性,鼓励可用性和性能优先,根据业务的特点,来实现非常弹性的基本可用,以及数据的最终一致性。
分布式事务:2PC
一、何谓分布式事务
1.1 单体应用
首先,来看下传统的单体应用(Monolithic App)。下图是一个单体应用的 3 个 模块,在同一个数据源上更新数据来完成一项业务,整个过程的数据一致性可以由数据库的本地事务来保证,如下图:
关于传统的数据库事务,其背后的核心就是ACID理论,本文不再赘述,读者可以参阅专门的书籍,后续我的MySQL系列也会专门讲解。
1.2 分布式应用
随着业务需求和架构的变化,单体应用进行了服务化拆分:原来的 3 个 模块被拆分为 3 个独立的服务,每个服务使用独立的数据源(Pattern: Database per service)。整个业务过程将由 3 个服务的调用来完成,如下图:
此时,每个服务自身的数据一致性仍有本地事务来保证,但是整个业务层面的全局数据一致性要如何保障呢?比如订单服务和账户服务,都有各自的数据库,必须保证操作的一致性,不能出现下单成功但是没记账的情况。这就是分布式系统所面临的典型分布式事务需求:
分布式系统需要一个解决方案来保障对所有节点操作的数据一致性,这些操作组成一个分布式事务,要么全部执行,要么全部不执行。
二、二阶段协议详解
二阶段提交(2PC, two-phase commit protocol),顾名思义,就是通过二阶段的协商来完成一个提交操作。
2PC 最早是用来实现数据库的分布式事务的,不过现在最常用的协议是 XA 协议。这个协议是 X/Open 国际联盟基于二阶段提交协议提出的,也叫作 X/Open Distributed Transaction Processing(DTP)模型,比如 MySQL 就是通过 MySQL XA 实现了分布式事务。
2PC分为两个阶段:投票阶段和提交阶段,我们来详细看下。
2.1 事务过程
二阶段提交协议,包含两类节点:
-
一个中心化协调者节点(coordinator),一般也叫做事务协调者
-
多个参与者节点(participant、cohort),一般也叫做事务参与者
二阶段提交协议的每一次事务提交过程如下:
投票阶段(commit-request phase / voting phase)
1、事务协调者请所有事务参与者进行投票:是否可以提交事务,然后等待所有参与者的投票结果;
2、参与者如果投票表示可以提交事务,那么就必须预留本地资源(执行本地事务),然后响应YES,后续也不再允许放弃事务;如果不能,就返回NO响应;
3、如果协调者接受某个参与者的响应超时,它会认为该参与者投票为NO,即预留资源失败。
提交阶段(commit phase)
在该阶段,事务协调者将基于投票阶段的投票结果进行决策:提交或取消各参与者的本地事务
1、仅当所有参与者都返回 YES 响应时,协调者才向所有参与者发出提交请求,此时所有参与者必须保证提交事务成功;
2、如果投票阶段中任意一个参与者返回 No 响应,则协调者向所有参与者发出回滚请求,所有参与者进行回滚操作。
两阶段提交协议成功场景示意图:
2PC假设所有节点都采用预写式日志(Write-Ahead Logging)来写数据,且日志写入后不会丢失。WAL 的核心思想就是先写日志,再写数据。
2.2 优缺点
优点:
-
强一致性,因为一阶段预留了资源,所有只要节点或者网络最终恢复正常,协议就能保证二阶段执行成功;
-
业界标准支持,二阶段协议在业界有标准规范——XA 规范,许多数据库和框架都有针对XA规范的分布式事务实现。
缺点:
-
在提交请求阶段,需要预留资源,在资源预留期间,其他人不能操作(比如,XA 在第一阶段会将相关资源锁定) ,会造成分布式系统吞吐量大幅下降;
-
容错能力较差,比如在节点宕机或者超时的情况下,无法确定流程的状态,只能不断重试,同时这也会导致事务在访问共享资源时发生冲突和死锁的概率增高,随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平伸缩的"枷锁";
2PC分布式事务方案,比较适合单体应用跨多库的场景,一般用spring + JTA就可以实现。但是因为严重依赖于数据库层面来搞定复杂的事务,效率很低,所以绝对不适合高并发的场景。
注意:一般来说,如果某个服务内部出现跨多库的直接操作,其实是不合规的。 按照分布式服务治理的规范,一个分布式系统,拆成几十个服务,每个服务只能操作自己对应的一个数据库,如果需要操作别的服务对应的库,不允许直连库,必须通过调用别的服务的接口来实现。
三、总结
二阶段提交协议,虽然是目前分布式事务的事实规范,但实际应用并不多。不过2PC是一种非常经典的思想,Paxos、Raft 等强一致性算法,都采用了二阶段提交操作。
所以,读者应当理解该协议背后的二阶段提交的思想,当后续需要时,能灵活地根据二阶段提交思想,设计新的事务或一致性协议。
分布式事务:3PC
一、简介
在二阶段协议中,事务参与者在投票阶段,如果同意提交事务,则会锁定资源,此时任何其他访问该资源的请求将处于阻塞状态。
正因为这个原因,三阶段协议(Three-phase commit protocol, 3PC)对二阶段协议进行了改进:
-
一方面引入超时机制,解决资源阻塞问题;
-
另一方面新增一个询问阶段(CanCommit),提前确认下各个参与者的状态是否正常。
二、协议详解
我们先来看下三阶段提交协议的成功场景:
2.1 询问阶段(CanCommit)
询问阶段,事务协调者向事务参与者发送 CanCommit 请求,参与者如果可以提交就返回 Yes 响应,否则返回 No 响应。这样的话,询问阶段就可以确保尽早的发现无法执行操作的参与者节点,提升效率。该阶段参与者也不会取锁定资源。
1、事务协调者发送事务询问指令(canCommit),询问事务参与者是否可以提交事务;
2、参与者如果可以提交就返回 Yes 响应,否则返回 No 响应,不需要做真正的操作。
对于事务协调者,如果询问阶段有任一参与者返回NO或超时,则协调者向所有参与者发送abort指令。
对于返回NO的参与者,如果在指定时间内无法收到协调者的abort指令,则自动中止事务。
2.2 准备阶段(PreCommit)
事务协调者根据事务参与者在询问阶段的响应,判断是执行事务还是中断事务:
1、如果询问阶段所有参与者都返回YES,则协调者向参与者们发送预执行指令(preCommit),参与者接受到preCommit指令后,写redo和undo日志,执行事务操作,占用资源,但是不会提交事务;
2、参与者响应事务操作结果,并等待最终指令:提交(doCommit)或中止(abort)。
2.3 提交阶段(DoCommit)
1、如果每个参与者在准备阶段都返回ACK确认(即事务执行成功),则协调者向参与者发起提交指令(doCommit),参与者收到指令后提交事务,并释放锁定的资源,最后响应ACK;
2、如果任意一个参与者在准备阶段返回NO(即执行事务操作失败),或者协调者在指定时间没收到全部的ACK响应,就会发起中止(abort)指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源。
当参与者响应ACK后,即使在指定时间内没收到doCommit指令,也会进行事务的最终提交;
一旦进入提交阶段,即使因为网络原因导致参与者无法收到协调者的doCommit或Abort请求,超时时间一过,参