从集中式到分布式
20世纪60年代大型主机被发明出来,凭借其安全性和稳定性的表现成为主流。但从20世纪80年代以来,计算机系统向网络化和微型化的发展日趋明显,传统的集中式处理模式越来越不能适应人们的需求。
集中式最明显的问题就是单点。
随着PC机性能的不断提升和网络技术的快速普及,大型主机的市场份额变得越来越小,很多企业开始放弃原来的大型主机,而改用小型机和普通PC服务器来搭建分布式计算机。
分布式
什么是分布式系统?
“分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。”
一个标准的分布式系统会有以下特征:
- 分布性(多台计算机在空间上随意分布)
- 对等性(副本是分布式系统最常见的概念之一)
- 并发性
- 缺乏全局时钟(缺乏一个全局的时钟序列控制)
- 故障总是发生
从集中式向分布式演变的过程中,必然引入了网络因素,而由于网络本身的不可靠性也引入额外的问题:
- 通信异常
- 网络分区(俗称:“脑裂”)
- 节点故障
虽然问题多多,但总有办法解决,就像解题有公式一样,分布式也有相应的理论支撑,比如:CAP定理和BASE理论。
CAP
“2000年7月,加州大学伯克利分校的Eric Brewer教授在ACM PODC会议上提出CAP猜想。2年后,麻省理工学院的Seth Gilbert和Nancy Lynch从理论上证明了CAP。之后,CAP理论正式成为分布式计算领域的公认定理。”
CAP定理告诉我们:一个分布式系统不可能同时满足一致性(C:Consistency),可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本要求,最多只能同时满足其中的两项。
- 一致性 每次读取都能读到最新的写入或错误,等同于所有节点访问同一份最新的数据副本。
- 可用性 每个请求都会收到一个(非错误)响应,但不能保证它包含最新的写操作。
- 分区容错性 尽管节点之间的网络丢弃或延迟了任意数量的消息,但系统仍继续运行。
当网络分区发生故障时,我们应该决定
- 取消操作,从而降低可用性,但确保一致性
- 继续进行操作,从而提供可用性,但存在不一致风险
举例子:
以下是一个有3个结点的系统拓扑:
分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
假设 M到S1的通信失败,或S1结点挂了,那么就有S1、M和S2这两个分区。
在这种情况下,我们是要容忍的,也就是说在这种情况下,系统还是要能提供服务的,不能因为这样的分区问题整体不能提供服务了。
根据CAP定理所得,CAP只能取其二,我们已经保证了P,那么就只能在C和A之间做选择。
- 选择AP,保证可用性 即让S1、M和S2这两个分区同时提供服务,保证系统的可用,但问题很明显,由于数据不能在M和S1之间同步,而一致性要求每次读取都能读到最新的写入或错误,所以无法保证数据一致性。
- 选择CP,保证一致性 在M和S1无法建立通信的这段时间,系统要进行错误恢复,恢复的这段时间系统对外是不可用状态,而可用性要求每个请求都会收到一个响应,所以无法保证可用性。恢复完成后,系统在可用状态下的数据是一致的,保证了一致性。
注意:上述当我们放弃一致性是指放弃数据的强一致性,而保留数据的最终一致性,这样的系统无法保证数据保持实时的一致性,但是能够承诺的是,数据最终会达到一个一致的状态,这就引入了一个时间窗口的概念,具体多久能够达到数据一致取决于系统的设计,主要包括数据副本在不同节点之间的复制时间长短。面对CAP,在做系统设计时我们会把更多的精力花在如何在C和A之间寻找平衡。
BASE 理论
“Eric Brewer在1997发表的论文 Cluster-Based Scalable Network Services 中第一次提出 BASE 的概念;eBay的架构师Dan Pritchett 在 2008 年发表文章 BASE: An AcidAlternative 中第一次明确提出的 BASE 理论。”
BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的简写,BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
- 基本可用
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性: - 响应时间上的损失
- 功能上的损失(降级页面)
- 弱状态 弱状态也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
- 最终一致性 最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
协议与算法
为了解决分布式一致性的问题,在长期的探索研究过程中,涌现出了一大批经典的一致性协议和算法,比如二阶段、三阶段提交协议、Paxos、Raft(muti-paxos)、ZAB(muti-paxos)算法等。
有关分布式共识(Consensus)算法的原理和实现,请参考其他资料,本文重点是分布式事务,就不过多介绍了。
分布式事务 2PC与3PC
事务
事务处理是计算机科学中的信息处理,分为单独的,不可分割的操作,称为事务。每个交易必须作为一个完整的单元成功或失败;它永远不可能仅是部分完成。事务应该具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性。
在集中式单体应用中,我们将事务操作的ACID交给数据库保证,比如Mysql通过自己实现的功能保证了ACID。
分布式事务
分布式事务是其中涉及两个或更多网络主机的数据库事务。通常,主机提供事务资源,而事务管理器负责创建和管理包含针对此类资源的所有操作的全局事务。与其他任何事务一样,分布式事务必须具有所有四个ACID(原子性,一致性,隔离性,持久性)属性,其中原子性保证工作单元的全部或全部结果。
对于一个分布式事务,它涉及多个DB的操作,这里的难点是在不可靠的网络环境下如何保证多数据库数据操作的一致性。
回想我们前面提到的解决一致性问题的协议,这其中二阶段、三阶段提交协议就是解决这个问题的理论基础。
XA和2PC、3PC
X/Open 是1984年由多个公司联合创建的一个用于定义和推进信息技术领域开放标准的公司,X/Open和开放软件基金会合并为The Open Group,并在1993-1996管理UNIX这个商标。
XA 的全称是eXtended Architecture。是1991年由 X/Open 发布的规范,用于分布式事务处理(DTP)。它是一个分布式事务协议,它通过二阶段提交协议保证强一致性。DTP模型已成为事务模型组件行为的事实上的标准。
下图是我从Open Group标准文件中对DTP模型部分的截图:
DTP模型抽象 AP(应用程序), TM(事务管理器)和 RM(资源管理器)的概念来保证分布式事务的强一致性。其中 TM 与 RM 间采用 XA 的协议进行双向通信。
与传统的本地事务相比,XA 事务增加了准备阶段,数据库除了被动接受提交指令外,还可以反向通知调用方事务是否可以被提交。TM 可以收集所有分支事务的准备结果,并于最后进行原子提交,以保证事务的强一致性。
Java 通过定义 JTA 接口实现了 XA 模型,JTA 接口中的 ResourceManager 需要数据库厂商提供 XA 驱动实现, TransactionManager 则需要事务管理器的厂商实现,传统的事务管理器需要同应用服务器绑定,因此使用的成本很高。而嵌入式的事务管器可以以 jar 包的形式提供服务,同 Apache ShardingSphere 集成后,可保证分片后跨库事务强一致性。
通常,只有使用了事务管理器厂商所提供的 XA 事务连接池,才能支持 XA 的事务。Apache ShardingSphere 在整合 XA 事务时,采用分离 XA 事务管理和连接池管理的方式,做到对应用程序的零侵入。
二阶段提交协议
上面我们说过,DTP是通过二阶段提交协议保证强一致性。那么什么是二阶段提交协议?
“在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。”
所谓的两个阶段是指:
- 第一阶段:准备阶段 (投票阶段)
- 第二阶段:提交阶段(执行阶段)
二阶段提交“事务提交”示意图:
二阶段提交“事务回滚”示意图:
至此,我们可以知道,所谓使用XA事务,就是利用XA的DTP模型,而DTP的一致性又是二阶段提交协议保证的。
三阶段提交协议
两阶段提交协议有它的优点,但缺点也很明显:
- 同步阻塞 所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
- 单点问题 协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作
- 数据不一致 在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
- 太过保守 任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
三阶段提交(Three-phase commit),是为解决两阶段提交协议的缺点而设计的。与两阶段提交不同的是,三阶段提交是“非阻塞”协议。
与两阶段提交不同的是,三阶段提交有两个改动点。
- 引入超时机制。同时在协调者和参与者中都引入超时机制。
- 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
相对于2PC,3PC主要解决的是单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。
但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况
了解了2PC和3PC之后,我们可以发现,无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。Google Chubby的作者Mike Burrows说过, there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos. 意即世上只有一种一致性算法,那就是Paxos,所有其他一致性算法都是Paxos算法的不完整版
分布式事务解决方案
通过上文的描述,其余我们已经知道了一种分布式事务的解决方案,即基于二阶段提交协议的XA事务。那么还有哪些其他方案呢?下面列举一些。
TCC
关于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。TCC 事务机制相比于上面介绍的 XA,解决了其几个缺点:
解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
数据一致性,有了补偿机制之后,由业务活动管理器控制一致性
TCC(Try Confirm Cancel)
- Try 阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)
- Confirm 阶段:确认执行真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作满足幂等性。要求具备幂等设计,Confirm 失败后需要进行重试。
- Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源 Cancel 操作满足幂等性 Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。
在 Try 阶段,是对业务系统进行检查及资源预览,比如订单和存储操作,需要检查库存剩余数量是否够用,并进行预留,Try 阶段操作是对这个可用库存数量进行操作。
基于 TCC 实现分布式事务,会将原来只需要一个接口就可以实现的逻辑拆分为 Try、Confirm、Cancel 三个接口,所以代码实现复杂度相对较高。
本地消息表
本地消息表这个方案最初是 ebay 架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章。该方案中会有消息生产者与消费者两个角色,假设系统 A 是消息生产者,系统 B 是消息消费者,其大致流程如下:
- 当系统 A 被其他系统调用发生数据库表更操作,首先会更新数据库的业务表,其次会往相同数据库的消息表中插入一条数据,两个操作发生在同一个事务中
- 系统 A 的脚本定期轮询本地消息往 mq 中写入一条消息,如果消息发送失败会进行重试
- 系统 B 消费 mq 中的消息,并处理业务逻辑。如果本地事务处理失败,会在继续消费 mq 中的消息进行重试,如果业务上的失败,可以通知系统 A 进行回滚操作
本地消息表实现的条件:
- 消费者与生成者的接口都要支持幂等
- 生产者需要额外的创建消息表
- 需要提供补偿逻辑,如果消费者业务失败,需要生产者支持回滚操作
容错机制:
- 步骤 1 失败时,事务直接回滚
- 步骤 2、3 写 mq 与消费 mq 失败会进行重试
- 步骤 3 业务失败系统 B 向系统 A 发起事务回滚操作
此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。
可靠消息最终一致性
大致流程如下
- A 系统先向 mq 发送一条 prepare 消息,如果 prepare 消息发送失败,则直接取消操作
- 如果消息发送成功,则执行本地事务
- 如果本地事务执行成功,则 mq 发送一条 confirm 消息,如果发送失败,则发送回滚消息
- B 系统定期消费 mq 中的 confirm 消息,执行本地事务,并发送 ack 消息。如果 B 系统中的本地事务失败,会一直不断重试,如果是业务失败,会向 A 系统发起回滚请求
- mq 会定期轮询所有 prepared 消息调用系统 A 提供的接口查询消息的处理情况,如果该 prepare 消息本地事务处理成功,则重新发送 confirm 消息,否则直接回滚该消息
该方案与本地消息最大的不同是去掉了本地消息表,其次本地消息表依赖消息表重试写入 mq 这一步由本方案中的轮询 prepare 消息状态来重试或者回滚该消息替代。其实现条件与余容错方案基本一致。目前市面上实现该方案的有阿里的 RocketMq。
尽最大努力通知
最大努力通知是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果 不影响主动方的处理结果。
这个方案的大致意思就是:
- 系统 A 本地事务执行完之后,发送个消息到 MQ;
- 这里会有个专门消费 MQ 的服务,这个服务会消费 MQ 并调用系统 B 的接口;
- 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B, 反复 N 次,最后还是不行就放弃。
Seata
Seata 是什么?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
基于 Seata 的 AT 模式构建企业业务的分布式事务解决方案,可以带来以下 3 个方面的 核心价值
- 低成本:编程模型 不变,轻依赖 不需要为分布式事务场景做特定设计,业务像搭积木一样自然地构建成长。
- 高性能:协议 不阻塞;资源释放快,保证业务的吞吐。
- 高可用:极端的异常情况下,可以暂时 跳过异常事务,保证整个业务系统的高可用。
Seata目前提供四种事务模型:
我们日常比较常用的是 AT和TCC,一种是无侵入的,一种是侵入性比较强的,在下面的文章中,我们将逐个说明。
Seata的架构:
3 个组件:TM(Transaction Manager)、RM(Resource Manager) 和 TC(Transaction Coordinator)。
一个典型的事务过程:
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
- XID 在微服务调用链路的上下文中传播。
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
- TM 向 TC 发起针对 XID 的全局提交或回滚决议。
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
Seata 的 全局事务 处理过程,分为两个阶段:
- 执行阶段 :执行 分支事务,并 保证 执行结果满足是 可回滚的(Rollbackable) 和 持久化的(Durable)。
- 完成阶段:根据 执行阶段 结果形成的决议,应用通过 TM 发出的全局提交或回滚的请求给 TC,TC 命令 RM 驱动 分支事务 进行 Commit 或 Rollback。
Seata 的所谓 事务模式 是指:运行在 Seata 全局事务框架下的 分支事务 的行为模式。准确地讲,应该叫作 分支事务模式。从这两个阶段的划分我们也能看出Seata也是基于二阶段提交协议的实现。
AT 模式
所谓AT模式,即自动事务(Automatic transaction)
执行阶段:
- 可回滚:根据 SQL 解析结果,记录回滚日志
- 持久化:回滚日志和业务 SQL 在同一个本地事务中提交到数据库
完成阶段:
- 分支提交:异步删除回滚日志记录
- 分支回滚:依据回滚日志进行反向补偿更新
TCC 模式
执行阶段:
- 调用业务定义的 Try 方法(完全由业务层面保证 可回滚 和 持久化)
完成阶段:
- 分支提交:调用各事务分支定义的 Confirm 方法
- 分支回滚:调用各事务分支定义的 Cancel 方法