编辑– 本系列文章共七部分,现已完成:
您还可以下载完整的文章集,以及有关使用 NGINX Plus 实现微服务的信息,作为电子书 -微服务: 从设计到部署。 并参阅我们关于微服务参考架构和微服务解决方案页面的系列文章。
这是使用微服务构建应用的系列文章的第五篇。 第一篇文章介绍了微服务架构模式,并讨论了使用微服务的优点和缺点。 该系列的第二篇<.htmla>和第三篇文章描述了微服务架构内通信的不同方面。 第四篇文章探讨了密切相关的服务发现问题。 在本文中,我们将换个角度来研究微服务架构中出现的分布式数据管理问题。
单片应用通常具有单个关系数据库。 使用关系数据库的一个主要好处是您的应用可以使用ACID 事务,它提供了一些重要的保证:
因此,您的应用可以简单地开始一个事务,更改(插入、更新和删除)多行,然后提交该事务。
使用关系数据库的另一大好处是它提供 SQL,这是一种丰富、声明性且标准化的查询语言。 您可以轻松编写一个组合来自多个表的数据的查询。 然后,RDBMS 查询规划器确定执行查询的最佳方式。 您不必担心如何访问数据库之类的低级细节。 而且,由于所有应用程序的数据都在一个数据库中,因此很容易查询。
不幸的是,当我们转向微服务架构时,数据访问变得更加复杂。 这是因为每个微服务拥有的数据都是该微服务私有的,只能通过其 API 访问。封装数据可确保微服务松散耦合,并可彼此独立发展。 如果多个服务访问相同的数据,则架构更新需要对所有服务进行耗时的协调更新。
更糟糕的是,不同的微服务通常使用不同类型的数据库。 现代应用存储和处理多种类型的数据,关系数据库并不总是最好的选择。 对于某些用例,特定的 NoSQL 数据库可能具有更方便的数据模型并提供更好的性能和可扩展性。 例如,对于存储和查询文本的服务来说,使用诸如 Elasticsearch 之类的文本搜索引擎是有意义的。 类似地,存储社交图数据的服务可能应该使用图形数据库,例如 Neo4j。 因此,基于微服务的应用通常混合使用 SQL 和 NoSQL 数据库,即所谓的多语言持久性方法。
用于数据存储的分区、多语言持久架构有许多优点,包括松散耦合的服务以及更好的性能和可扩展性。 然而,它确实引入了一些分布式数据管理挑战。
第一个挑战是如何实现跨多个服务保持一致性的业务事务。 为了了解为什么这是一个问题,让我们看一个在线 B2B 商店的示例。 客户服务部门维护有关客户的信息,包括他们的信用额度。 订单服务管理订单,并且必须验证新订单不超过客户的信用额度。 在该应用的单片版本中,订单服务可以简单地使用 ACID 事务来检查可用信用并创建订单。
相比之下,在微服务架构中,ORDER 和 CUSTOMER 表对于各自的服务来说是私有的,如下图所示。
订单服务不能直接访问 CUSTOMER 表。 只能使用客服提供的API。 订单服务可能使用分布式事务,也称为两阶段提交(2PC)。 然而,2PC 在现代应用中通常不是一个可行的选择。 CAP 定理要求您在可用性和 ACID 风格一致性之间进行选择,而可用性通常是更好的选择。 此外,许多现代技术(例如大多数 NoSQL 数据库)都不支持 2PC。跨服务和数据库维护数据一致性至关重要,因此我们需要另一种解决方案。
第二个挑战是如何实现从多个服务检索数据的查询。 例如,假设应用需要显示客户及其最近的订单。 如果订单服务提供了用于检索客户订单的 API,那么您可以使用应用端连接来检索该数据。 应用从客户服务中检索客户,并从订单服务中检索客户的订单。 但是,假设订单服务仅支持通过主键查找订单(也许它使用仅支持基于主键的检索的 NoSQL 数据库)。 在这种情况下,没有明显的方法来检索所需的数据。
对于许多应用来说,解决方案是使用事件驱动架构。 在这种架构中,当发生一些值得注意的事件时,微服务会发布事件,例如当它更新业务实体时。 其他微服务订阅这些事件。 当微服务接收到事件时,它可以更新自己的业务实体,这可能导致发布更多事件。
您可以使用事件来实现跨多个服务的业务交易。 一个交易由一系列步骤组成。 每个步骤都包含一个微服务,更新业务实体并发布触发下一步的事件。 下面的一系列图表展示了如何使用事件驱动的方法在创建订单时检查可用信用。 微服务通过消息代理交换事件。
订单服务创建一个状态为“新”的订单并发布订单创建事件。
客户服务使用订单创建事件,为订单保留信用,并发布信用保留事件。
订单服务使用信用预留事件,并将订单状态更改为 OPEN。
更复杂的情况可能涉及额外的步骤,例如在检查客户信用的同时保留库存。
假设 (a) 每个服务都自动更新数据库并发布事件(稍后会详细介绍)并且 (b) 消息代理保证事件至少传递一次,那么您就可以实现跨多个服务的业务事务。 值得注意的是,这些不是 ACID 事务。 它们提供更弱的保证,例如最终一致性。 该交易模型被称为BASE 模型。
您还可以使用事件来维护预先加入多个微服务所拥有的数据的物化视图。 维护视图的服务订阅相关事件并更新视图。 例如,维护客户订单视图的客户订单视图更新服务订阅客户服务和订单服务发布的事件。
当客户订单视图更新服务收到客户或订单事件时,它会更新客户订单视图数据存储。 您可以使用文档数据库(例如 MongoDB)实现客户订单视图,并为每个客户存储一个文档。 客户订单视图查询服务通过查询客户订单视图数据存储来处理对客户和最近订单的请求。
事件驱动架构有几个优点和缺点。 它可以实现跨多个服务的交易并提供最终的一致性。 另一个好处是它还使应用能够维护物化视图。 一个缺点是编程模型比使用 ACID 事务时更复杂。 通常,您必须实施补偿事务来从应用级故障中恢复;例如,如果信用检查失败,您必须取消订单。 此外,应用必须处理不一致的数据。 这是因为飞行过程中交易所做的更改是可见的。 如果应用从尚未更新的物化视图读取,也会看到不一致的情况。 另一个缺点是订阅者必须检测并忽略重复事件。
在事件驱动架构中,还存在原子更新数据库和发布事件的问题。 例如,订单服务必须在 ORDER 表中插入一行并发布订单创建事件。 这两个操作必须以原子方式完成。 如果服务在更新数据库之后但在发布事件之前崩溃,系统就会变得不一致。 确保原子性的标准方法是使用涉及数据库和消息代理的分布式事务。 然而,由于上述原因,比如 CAP 定理,这正是我们不想做的。
实现原子性的一种方法是应用使用仅涉及本地事务的多步骤过程来发布事件。 诀窍是在存储业务实体状态的数据库中有一个 EVENT 表,它充当消息队列。 应用开始(本地)数据库事务,更新业务实体的状态,将事件插入 EVENT 表并提交事务。 单独的应用线程或进程查询 EVENT 表,将事件发布到消息代理,然后使用本地事务将事件标记为已发布。 下图显示了该设计。
订单服务在 ORDER 表中插入一行,并在 EVENT 表中插入订单创建事件。 事件发布者线程或进程查询 EVENT 表中未发布的事件,发布事件,然后更新 EVENT 表以将事件标记为已发布。
这种方法有若干优点和缺点。 一个好处是,它保证每次更新都会发布一个事件,而无需依赖 2PC。此外,应用会发布业务级事件,从而无需推断它们。 这种方法的一个缺点是它很容易出错,因为开发人员必须记住发布事件。 这种方法的局限性是,当使用某些 NoSQL 数据库时,由于它们的事务和查询功能有限,因此很难实现。
通过让应用使用本地事务来更新状态和发布事件,这种方法消除了对 2PC 的需要。 现在让我们看一下通过让应用简单地更新状态来实现原子性的方法。
在没有 2PC 的情况下实现原子性的另一种方法是通过挖掘数据库的事务或提交日志的线程或进程发布事件。 应用更新数据库,导致更改被记录在数据库的事务日志中。 事务日志挖掘器线程或进程读取事务日志并将事件发布到消息代理。 下图显示了该设计。
这种方法的一个例子是开源LinkedIn Databus项目。 Databus 挖掘 Oracle 事务日志并发布与更改相对应的事件。 LinkedIn 使用 Databus 来确保各种派生数据存储与记录系统保持一致。
另一个示例是AWS DynamoDB 中的流机制,它是一个托管的 NoSQL 数据库。 DynamoDB 流包含过去 24 小时内对 DynamoDB 表中的项目所做的更改(创建、更新和删除操作)的时间顺序序列。 应用可以从流中读取这些更改,并将其发布为事件。
事务日志挖掘有各种优点和缺点。 一个好处是,它保证每次更新都会发布一个事件,而无需使用 2PC。事务日志挖掘还可以通过将事件发布与应用程序的业务逻辑分离来简化应用。 一个主要的缺点是事务日志的格式是每个数据库专有的,甚至可以在数据库版本之间发生变化。 此外,根据事务日志中记录的低级更新来逆向工程高级业务事件也很困难。
事务日志挖掘通过让应用执行一件事(更新数据库)消除了对 2PC 的需要。 现在让我们看一下一种不同的方法,该方法消除更新并仅依赖于事件。
事件源通过使用完全不同的、以事件为中心的方法来持久化业务实体,无需 2PC 即可实现原子性。 应用不存储实体的当前状态,而是存储一系列状态变化事件。 应用通过重播事件来重建实体的当前状态。 每当业务实体的状态发生变化时,就会将新事件附加到事件列表中。 由于保存事件是一个单一操作,因此它本质上是原子的。
要了解事件源的工作原理,以订单实体为例。 在传统方法中,每个订单映射到 ORDER 表中的一行,以及例如 ORDER_LINE_ITEM 表中的行。 但是当使用事件源时,订单服务会以状态改变事件的形式存储订单: 已创建、已批准、已发货、已取消。 每个事件都包含足够的数据来重建秩序的状态。
事件会持久保存在事件存储中,事件存储是一个事件数据库。 商店有一个用于添加和检索实体事件的 API。 事件存储的行为也类似于我们之前描述的架构中的消息代理。 它提供了一个 API,使服务能够订阅事件。 事件存储将所有事件传递给所有感兴趣的订阅者。 事件存储是事件驱动的微服务架构的支柱。
事件源有几个好处。 它解决了实现事件驱动架构的关键问题之一,并使得在状态发生变化时可靠地发布事件成为可能。 因此,它解决了微服务架构中的数据一致性问题。 此外,由于它持久保存的是事件而不是域对象,因此它基本上避免了对象关系阻抗不匹配问题。 事件源还提供了对业务实体所做更改的 100% 可靠的审计日志,并可以实现确定任何时间点实体状态的时间查询。 事件源的另一个主要好处是您的业务逻辑由交换事件的松散耦合的业务实体组成。 这使得从单片应用迁移到微服务架构变得更加容易。
事件源也有一些缺点。 这是一种不同的、不熟悉的编程风格,因此存在学习曲线。 事件存储仅直接支持通过主键查找业务实体。 您必须使用命令查询职责分离(CQRS)来实现查询。 因此,应用必须处理最终一致的数据。
在微服务架构中,每个微服务都有自己的私有数据存储。 不同的微服务可能使用不同的 SQL 和 NoSQL 数据库。 虽然这种数据库架构具有显著的优势,但它也带来了一些分布式数据管理挑战。 第一个挑战是如何实现跨多个服务保持一致性的业务事务。 第二个挑战是如何实现从多个服务检索数据的查询。
对于许多应用来说,解决方案是使用事件驱动架构。 实现事件驱动架构的一个挑战是如何原子地更新状态以及如何发布事件。 有几种方法可以实现这一点,包括使用数据库作为消息队列、事务日志挖掘和事件源。
在未来的博客文章中,我们将继续深入探讨微服务的其他方面。
编辑– 本系列文章共七部分,现已完成:
您还可以下载完整的文章集,以及有关使用 NGINX Plus 实现微服务的信息,作为电子书 -微服务: 从设计到部署。 并参阅我们关于微服务参考架构和微服务解决方案页面的系列文章。
客座博主 Chris Richardson 是原CloudFoundry.com的创始人,该网站是针对 Amazon EC2 的早期 Java PaaS(平台即服务)。 他现在为各组织提供咨询服务,以改进他们开发和部署应用的方式。 他还定期在http://microservices.io上撰写有关微服务的博客。
“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”