众所周知,微服务架构解决了很多问题,通过分解复杂的单体式应用,在功能不变的情况下,使应用被分解为多个可管理的服务,为采用单体式编码方式很难实现的功能提供了模块化的解决方案。同时,每个微服务独立部署、独立扩展,使得持续化集成成为可能。由此,单个服务很容易开发、理解和维护。
微服务架构为开发带来了诸多好处的同时,也引发了很多问题。比如服务运维变得更复杂,服务之间的依赖关系更复杂,数据一致性难以保证。
本篇文章将讨论和介绍Choerodon猪齿鱼是如何保障微服务架构的数据一致性的。
主要内容包括 :
- 传统应用使用本地事务保持一致性
- 多数据源下的分布式事务
- 微服务架构中应满足数据最终一致性原则
- 使用Event Sourcing保证微服务的最终一致性
- 使用可靠事件模式保证微服务的最终一致性
- 使用Saga保证微服务的最终一致性
下面将通过一个实例来分别介绍这几种模式。
在Choerodon 猪齿鱼的 DevOps流程中,有这样一个步骤。
- 用户在Choerodon 平台上创建一个项目;
- DevOps 服务对应创建一个项目;
- DevOps 为该项目 在 Gitlab 上创建对应的group。
传统应用使用本地事务保持一致性
在讲微服务架构的数据一致性之前,先介绍一下传统关系型数据库是如何保证一致性的,从关系型数据库中的ACID理论讲起。
ACID 即数据库事务正确执行的四个基本要素。分别是:
- 原子性(Atomicity):要么全部完成,要么全部不完成,不存在中间状态
- 一致性(Consistency):事务必须始终保持系统处于一致的状态
- 隔离性(Isolation):事务之间相互隔离,同一时间仅有一个请求用于同一数据
- 持久性(Durability):事务一旦提交,该事务对数据库所作的更改便持久的保存在数据库之中,并不会被回滚
可以通过使用数据库自身的ACID Transactions,将上述步骤简化为如下伪代码:
transaction.strat();
createProject();
devopsCreateProject();
gitlabCreateGroup();
transaction.commit();
这个过程可以说是十分简单,如果在这一过程中发生失败,例如DevOps创建项目失败,那么该事务做回滚操作,使得最终平台创建项目失败。由于传统应用一般都会使用一个关系型数据库,所以可以直接使用 ACID transactions。 保证了数据本身不会出现不一致。为保证一致性只需要:开始一个事务,改变(插入,删除,更新)很多行,然后提交事务(如果有异常时回滚事务)。
随着业务量的不断增长,单数据库已经不足以支撑庞大的业务数据,此时就需要对应用和数据库进行拆分,于此同时,也就出现了一个应用需要同时访问两个或者两个以上的数据库或多个应用分别访问不同的数据库的情况,数据库的本地事务则不再适用。
为了解决这一问题,分布式事务应运而生。
多数据源下的分布式事务
想象一下,如果很多用户同时对Choerodon 平台进行创建项目的操作,应用接收的流量和业务数据剧增。一个数据库并不足以存储所有的业务数据,那么我们可以将应用拆分成IAM服务和DevOps服务。其中两个服务分别使用各自的数据库,这样的情况下,我们就减轻了请求的压力和数据库访问的压力,两个分别可以很明确的知道自己执行的事务是成功还是失败。但是同时在这种情况下,每个服务都不知道另一个服务的状态。因此,在上面的例子中,如果当DevOps创建项目失败时,就无法直接使用数据库的事务。
那么如果当一个事务要跨越多个分布式服务的时候,我们应该如何保证事务呢?
为了保证该事务可以满足ACID,一般采用2PC或者3PC。 2PC(Two Phase Commitment Protocol),实现分布式事务的经典代表就是两阶段提交协议。2PC包括准备阶段和提交阶段。在此协议中,一个或多个资源管理器的活动均由一个称为事务协调器的单独软件组件来控制。
我们为DevOps服务分配一个事务管理器。那么上面的过程可以整理为如下两个阶段:
准备阶段:
提交/回滚阶段:
2PC 提供了一套完整的分布式事务的解决方案,遵循事务严格的 ACID 特性。
但是,当在准备阶段的时候,对应的业务数据会被锁定,直到整个过程结束才会释放锁。如果在高并发和涉及业务模块较多的情况下,会对数据库的性能影响较大。而且随着规模的增大,系统的可伸缩性越差。同时由于 2PC引入了事务管理器,如果事务管理器和执行的服务同时宕机,则会导致数据产生不一致。虽然又提出了3PC 将2PC中的准备阶段再次一分为二的来解决这一问题,但是同样可能会产生数据不一致的结果。
微服务架构中应满足数据最终一致性原则
不可否认,2PC 和3PC 提供了解决分布式系统下事务一致性问题的思路,但是2PC同时又是一个非常耗时的复杂过程,会严重影响系统效率,在实践中我们尽量避免使用它。所以在分布式系统下无法直接使用此方案来保证事务。
对于分布式的微服务架构而言,传统数据库的ACID原则可能并不适用。首先微服务架构自身的所有数据都是通 过API 进行访问。这种数据访问方式使得微服务之间松耦合,并且彼此之间独立非常容易进行性能扩展。其次 不同服务通常使用不同的数据库,甚至并不一定会使用同一类数据库,反而使用非关系型数据库,而大部分的 非关系型数据库都不支持2PC。
在这种情况下,又如何解决事务一致性问题呢?
一个最直接的办法就是考虑数据的强一致性。根据Eric Brewer提出的CAP理论,只能在数据强一致性(C)和可用性(A)之间做平衡。
CAP 是指在一个分布式系统下,包含三个要素:Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),并且三者不可得兼。
- 一致性(Consistency),是指对于每一次读操作,要么都能够读到最新写入的数据,要么错误,所有数据变动都是同步的。
- 可用性(Availability),是指对于每一次请求,都能够得到一个及时的、非错的响应,但是不保证请求的结果是基于最新写入的数据。即在可以接受的时间范围内正确地响应用户请求。
- 分区容错性(Partition tolerance),是指由于节点之间的网络问题,即使一些消息丢包或者延迟,整个系统仍能够提供满足一致性和可用性的服务。
关系型数据库单节点保证了数据强一致性(C)和可用性(A),但是却无法保证分区容错性(P)。
然而在分布式系统下,为了保证模块的分区容错性(P),只能在数据强一致性(C)和可用性(A)之间做平衡。具体表现为在一定时间内,可能模块之间数据是不一致的,但是通过自动或手动补偿后能够达到最终的一致。
可用性一般是更好的选择,但是在服务和数据库之间维护事务一致性是非常根本的需求,微服务架构中应该选择满足最终一致性。
那么我们应该如何实现数据的最终一致性呢?
使用Event Sourcing保证微服务的最终一致性
什么是Event Sourcing(事件溯源)?
一个对象从创建开始到消亡会经历很多事件,传统的方式是保存这个业务对象当前的状态。但更多的时候,我们也许更关心这个业务对象是怎样达到这一状态的。Event Sourcing从根本上和传统的数据存储不同,它存储的不是业务对象的状态,而是有关该业务对象一系列的状态变化的事件。只要一个对象的状态发生变化,服务就需要自动发布事件来附加到事件的序列中。这个操作本质上是原子的。
现在将上面的订单过程用Event Sourcing 进行改造,将订单变动的一个个事件存储起来,服务监听事件,对订单的状态进行修改。