微服务入门系列:基于事件驱动的数据管理

这是下面七篇系列文章的第五篇:

  1. 微服务入门系列:微服务介绍
  2. 微服务入门系列:使用 API 网关
  3. 微服务入门系列:微服务架构的进程间通信
  4. 微服务入门系列:微服务架构的服务发现
  5. 微服务入门系列:基于事件驱动的数据管理(本文)
  6. 微服务入门系列:微服务的部署策略选择
  7. 微服务入门系列:单体应用的微服务重构

出自微服务部落:https://blog.idevfun.io/event-driven-data-management-microservices/
原文链接:https://www.nginx.com/blog/event-driven-data-management-microservices/
作者:Chris Richardson
翻译:董干

这 7 篇文章是很不错的微服务入门系列文章,介绍了微服务所需要的主要技术,是当年(本系列文章写于2015年,但现在仍然不过时)带领译者入门微服务的文章,强烈建议深入微服务架构之前先了解本系列。


本文是关于使用微服务架构构建应用的七篇系列文章的第五篇。第一篇介绍了微服务架构模式,同单体架构模式进行了对比,并讨论了微服务的优势和缺点。第二篇第三篇分别从不同角度描述了微服务架构的通信问题。第四篇探索了与服务发现密切相关的问题。这篇文章我们转向微服务架构带来的分布式数据管理问题。

微服务与分布式数据管理问题

单体应用通常仅使用单个关系型数据库。使用关系型数据库的一个关键的优势是你的应用可以使用 ACID 事务 来得到一些重要的保障:

  • 原子性 —— 改动原子性地实施
  • 一致性 —— 数据库的状态总是一致的
  • 隔离性 —— 尽管事务并发地执行,但它们看起来就像是串型执行一样
  • 持久性 —— 一旦事务得到提交,它将不能被撤回

正因为如此,你的应用可以简单地开启一个事务,改动(包括增、删、改)等多行数据,然后提交这个事务。

另一个使用关系型数据库的好处是它提供了 SQL —— 一种丰富的、声明式且标准的查询语言。你可以容易地写出能整合跨多张表数据的查询。然后用关系型数据库管理系统(RDBMS)查询计划器决定执行这个查询的最优方式。你不需要关心如果连接数据库获取数据这样的底层细节。并且,由于所有的应用程序数据都存在于同一个数据库,查询起来很简单。

不幸的是,当使用微服务架构后,数据访问变得相对困难得多。这是因为每个微服务拥有的数据是这个服务私有的,并且只能通过它的 API 来访问。对数据的封装保证微服务之间是松耦合的,并且可以互相独立地演化。如果多个服务访问同一数据,数据格式的变化则需要耗时的、跨所涉及服务的协同改动。

不同的微服务经常会使用不同种类的数据库,这也使得这一问题更加突出。现代应用程序存储和处理各种各样的数据,关系型数据库并不总是最佳选择。对于某些使用场景来说,某种特定的 NoSQL 数据库可能会有更方便的数据模型,并提供更好的性能和规模。例如,对于存储和查询文本的服务来说,使用一种像 Elasticsearch 这样的文本搜索引擎会比较合理。类似地,一个存储社会图数据的服务可能应该使用图数据库,例如 Neo4j。因此,基于微服务的应用通常混合使用 SQL 和 NoSQL 数据库,即所谓的混合持久化方式。

分区的混合持久化数据存储架构有很多优势,包括服务松耦合和更好的性能及可扩展性。但是,它确实引入了一些分布式数据管理的挑战。

首先的挑战就是如何实现业务事务以保持跨多个服务的一致性。为了了解为什么这会是个问题,我们来看一个线上 B2B 电商的例子。Customer Service 维护客户的信息,包括他们的账户余额。Order Service 管理订单,并需要保证每一个新的订单不超过客户的账户余额。在单体应用中,Order Service 可以简单地使用 ACID 事务来检查可用余额并创建订单。

微服务架构则不同,ORDER 表和 CUSTOMER 表对各自服务来说都是私有的,如下图所示。

Richardson-microservices-part5-separate-tables-e1449727641793

如图所示,Order Service 不能直接访问 CUSTOMER 表。它仅能通过 Customer Service 提供的 API 来访问。Order Service 理论上可以使用分布式事务,如两阶段提交协议 (2PC)。但是,现代应用中 2PC 通常不是一种可行方案。 CAP 理论要求你必须在可用性和 ACID 式的一致性中做出选择,而可用性通常都是更好的选择。而且,很多现代技术,例如大多数 NoSQL 数据库,并不支持 2PC。由于维护多个服务或数据库之间的数据一致性十分重要,我们需要一种方案。

另一个挑战是如何实现从多个服务查询并获取数据。举个例子,我们想象应用需要展示一个客户和它最近的订单。如果 Order Service 提供了获取客户订单的 API,那么你可以使用应用内的数据 join 来实现。应用程序先从 Customer Service 中获取相关客户信息,然后从 Order Service 中获取客户的订单。但是,如果假设说 Order Service 仅仅支持通过主键来查询订单(可能因为它使用某种仅支持通过主键获取记录的 NoSQL 数据库),此时,并没有明显的方式可以获取到需要的数据。

事件驱动架构

对于许多应用来说,解决方案是使用 事件驱动架构。在这种架构下,当一个微服务发生了一些显著的事件,例如当它更新某个业务实体时,它发布一个事件。其他的微服务可以订阅这些事件。当微服务收到事件时,它可以更新自己的业务实体,进而或许引发更多的事件发布。

可以通过事件来实现跨多个服务的业务事务。一个事务由一系列步骤组成。每个步骤包含某个微服务更新业务实体并发布触发下一个步骤的事件。下图的步骤说明了如何使用事件驱动的方式检查账户余额并创建新订单。微服务通过消息队列交换事件。

  1. Order Service 创建一个状态为 NEW 的订单,并发布一个 Order Created 事件。
    Richardson-microservices-part5-credit-check-1-e1449727610972
  2. Customer Service 消费 Order Created 事件,并预扣订单所需金额,发布一个 Credit Reserved 事件。
    Richardson-microservices-part5-credit-check-2-e1449727579423
  3. Order Service 消费 Credit Reserved 事件,并将订单状态置为 OPEN
    Richardson-microservices-part5-credit-check-3-e1449727548440

更复杂的场景可能包含更多的步骤,例如在检查用户可用余额的同时预留库存等。

假设满足 (a) 每个服务原子地进行更新数据库和发布事件操作 —— 稍后会再详述 —— ,并且 (b) 消息队列保证事件至少投递成功一次,那么你可以实现跨多个服务的业务事务。需要说明的是,这里的事务并非指 ACID 事务。这里只能提供如最终一致性这样的弱的多的事务保证。这种事务模型又被称为 BASE 模型(译者注:Basically Available, Soft state, Eventually consistent)。

事件也可以用来维护多个微服务联合数据的视图。这个服务维护一个用来订阅相关事件并进行更新的视图。例如,Customer Order View Updater Service 维护 Customer Orders 的视图,并订阅 Customer Service 和 Order Service 发布的事件。

Richardson-microservices-part5-subscribe-e1449727516992

当 Customer Order View Updater Service 接收到一个 Customer 或者 Order 事件时,它会更新 Customer Order View 数据存储。你可以使用文档数据库,例如 MongoDB 来实现 Customer Order View,并对每个用户使用一个文档存储。Customer Order View Query Service 通过查询 Customer Order View 数据存储来处理查询用户和他最近的订单的请求。

事件驱动架构有优点也有缺陷。它使得跨多个服务的事务实现和最终一致性称为可能。另一个好处是它可以使得应用可以维护一个统一视图。一个缺陷是编程模型相比使用 ACID 事务更为复杂。通常你必须实现补偿事务来从应用级别的异常中恢复。例如,当检查余额失败时,订单必须被取消。另外,应用必须处理不一致数据。这是因为正在进行中的事务步骤会对系统产生可见的影响。应用读取还未被更新的视图时也会看到不一致的情况。另外一个缺陷是,订阅者必须能够检测出并且忽略重复的事件。

实现原子性

在事件驱动架构中,还有一个问题是如何原子性地更新数据库和发布一个事件。例如,Order Service 必须在 ORDER 表中插入一条记录,然后发布一个 Order Created 事件。这两个操作是否能原子性地完成很关键。如果服务在更新完数据库但是在发布事件前崩溃,系统将会变得不一致。保证原子性的标准做法是使用包含数据库和消息队列的分布式事务。但是,由于上面提到的原因,例如 CAP 定理,这是我们想避免去做的。

使用本地事务发布事件

实现应用发布事件原子性的一种方式是使用只涉及到本地事务的多步操作。核心技巧是在和保存业务实体的同一个数据库中使用一张 EVENT 表来作为消息队列缓存。应用开启一个(本地)数据库事务,更新业务实体的状态,向 EVENT 表中插入一个事件,然后提交事务。然后另有一个应用的线程或进程不断查询 EVENT 表,向消息队列发布事件,然后使用本地事务标记事件为已发布。下图描述了这种设计。

Richardson-microservices-part5-local-transaction-e1449727484579-1

Order Service 在 ORDER 表中插入一条新记录,并且向 EVENT 表中插入一个 Order Created 事件。Event Publisher 线程或者进程查询 EVENT 表,找出未发布的事件将其发布,然后更新 EVENT 表中相应的事件为已发布状态。

这种方法也各有优缺点。一个优点是它不依赖 2PC 保证了每个更新操作都会发布一个事件。而且,应用直接发布业务级别的事件,消除了对它们的推断。一个缺点是这种方法潜在地更容易出错,因为开发者必须记得发布这些事件。这种方式的一个局限是当使用一些 NoSQL 数据库时,实现起来很困难,因为这些数据库在事务和查询方式上有一定局限。

这种方式通过使用应用内本地事务更新状态和发布事件,从而消除了使用 2PC 的需要。我们接下来看另一种仅通过更新状态就可以实现原子性的方式。

挖掘数据库事务日志

不依赖 2PC 实现原子性的另一种方式是,发布事件的线程或进程挖掘数据库的事务或者提交日志。Transaction Log Miner 线程或进程读取事务日志,然后发布事务到消息队列中。如下图所示。

Richardson-microservices-part5-transaction-log-e1449727434678

这种方式的一个例子是开源的 LinkedIn Databus 项目。Databus 挖掘 Oracle 的事务日志,并将相应的改动发布为事件。LinkedIn 使用 Databus 来使得系统记录更新后各个下游依赖的数据存储保持一致。

另一个例子是 AWS DynamoDB 流,AWS DynamoDB 是一个托管 NoSQL 数据库。DynamoDB 流包含 DynamoDB 表中最近 24 小时内对记录所做的按照时间序列排序的改动(增删改查操作)。应用程序可以从流中读取这些变化,还可以进行诸如将这些发布为事件等操作。

事务日志挖掘有优点也有缺点。一个优势是它在不使用 2PC 的情况下保证每一次变更都会发布事件。事务日志挖掘可以通过分离事件发布和应用业务逻辑简化应用。一个主要的缺陷是不同数据库的事务日志格式都是各个数据库私有的,有的甚至在同一个数据库的不同版本中发生变化。另外,将底层的事务日志数据更新记录逆向工程还原为上层的业务事件有时也会比较困难。

事务日志挖掘通过让应用仅仅做一件事,即更新数据库,就能达到消除引入 2PC 的需要。让我们接下来看另一种不通过更新操作而仅通过依赖事件实现的另一种方式。

使用事件溯源

事件溯源 通过使用一种根本不同的、以事件为中心的方法持久化业务实体而不依靠 2PC 实现原子性。这种方式应用不保存实体的当前状态,而是保存一系列顺序发生的改变状态的事件。应用通过重放这些事件来重新构造当前实体的状态。当业务实体发生变化时,新的事件追加到事件列表里。由于保存事件是个单一操作,它天生具有原子性。

为了描述事件溯源如何工作,这里考虑订单实体作为示例。传统的方式是每个订单映射到 ORDER 表的一行数据和订单详情商品表例如 ORDER_LINE_ITEM 的多行数据。但当使用事件溯源时,Order Service 以一系列改变状态的事件 Created, Approved, ShippedCancelled 保存此订单。每个事件都包含重新构造此订单状态的数据。

Richardson-microservices-part5-event-sourcing-e1449711558668

这些事件持久化存储在事件存储数据库 Event Store 中。它包含有添加和获取实体事件的 API。Event Store 在行为上和前面介绍的消息队列有些类似。它提供了使得服务可以订阅这些事件的 API。Event Store 将这些事件投递到所有感兴趣的订阅者。Event Store 是事件驱动微服务架构的基础和核心。

事件溯源有很多优点。它解决了实现事件驱动架构的一个关键问题,使得无论何时当状态变化时都能可靠地发布事件。由此它解决了微服务架构的数据一致性问题。另外,由于持久化事件而非领域模型对象,它基本避免了 对象——关系阻抗匹配问题。事件溯源还提供了 100% 可靠的业务实体变更审计日志,并且使得实现给定任何时间点临时查询确定实体的状态成为可能。事件溯源的另一个优点是业务逻辑由互相交换事件的松耦合业务实体组成。这使得从单体应用到微服务架构的迁移容易许多。

事件溯源也有一些缺点。它是一种不同且不熟悉的编程模式,因此具有一定的学习曲线。事件存储只直接支持通过主键查询业务实体。你必须使用 命令查询职责分离(CQRS)来实现查询。因此,应用程序必须能处理只能最终一致的数据。

总结

在微服务架构中,每个微服务都有自己的专属数据存储。不同的微服务可以使用不同的 SQL 或 NoSQL 数据库。这种数据库架构有着显著的优势,但也引入了分布式数据管理的挑战。首先要解决的挑战是如何实现能够跨服务保持一致性的业务事务。另一个挑战是如何实现从多个服务获取数据的查询。

对于许多应用来说,解决方案是使用事件驱动的架构。实现事件驱动架构的一个挑战是如何保持原子性地更新状态和发布事件。解决这个问题有多种方式,包括使用数据库作为消息队列、事务日志数据挖掘以及事件溯源。

后面的文章中,我们会继续深入探索微服务的其他角度。


这是下面七篇系列文章的第五篇:

  1. 微服务入门系列:微服务介绍
  2. 微服务入门系列:使用 API 网关
  3. 微服务入门系列:微服务架构的进程间通信
  4. 微服务入门系列:微服务架构的服务发现
  5. 微服务入门系列:基于事件驱动的数据管理(本文)
  6. 微服务入门系列:微服务的部署策略选择
  7. 微服务入门系列:单体应用的微服务重构

出自微服务部落:https://blog.idevfun.io/event-driven-data-management-microservices/
原文链接:https://www.nginx.com/blog/event-driven-data-management-microservices/
作者:Chris Richardson
翻译:董干

这 7 篇文章是很不错的微服务入门系列文章,介绍了微服务所需要的主要技术,是当年(本系列文章写于2015年,但现在仍然不过时)带领译者入门微服务的文章,强烈建议深入微服务架构之前先了解本系列。

Show Comments

Get the latest posts delivered right to your inbox.