这是本电子书的第五章,内容涉及使用微服务构建应用程序。第一章介绍了微服务架构模式,并讨论了使用微服务的优点和缺点。第二章和第三章描述了微服务架构内部通信的不同方面。第四章探索了服务发现紧密相关的问题。在这章,我们换个角度,看看微服务架构中出现的分布式数据管理问题。
整体式应用程序通常具有单个关系数据库。使用关系型数据库的关键好处是,您的应用程序可以使用 ACID 事务, 这提供了一些重要的保证:
因此,您的应用程序可以简单地开始一个事务,更改(插入、更新和删除)多行,然后提交事务。
使用关系数据库的另一大好处是它提供了SQL, 它是一种丰富的,声明式的,标准的查询语言。您可以轻松编写组合来自多个表的数据的查询。然后,RDBMS 查询计划程序确定执行查询的最佳方式。您不必担心如何访问数据库之类的低级细节。而且,由于应用程序的所有数据都位于一个数据库中,因此很容易查询。
不幸的是,当我们移到微服务架构中是,数据访问变得更加的复杂。这是因为每个微服务都有自己的数据,并且数据堆微服务是私有的。并且只能够通过他的API访问。封装这些数据以确保微服务是松耦合的,并且能够独立于其他微服务演进。如果多个微服务访问同样的数据,表结构更新需要耗时,协调所有服务。
更糟糕的是,不同的微服务经常使用不同种类的数据库。现代应用程序存储和处理各种类型的数据。并且关系型数据库并非一直是最好的选择。在一些使用场景下,一个特定的nosql数据库可能具有更方便的数据模型,并提供更好的性能和可伸缩性。例如,对于存储和查询文本的服务来说,使用文本搜索引擎(如 Elasticsearch)是有意义的。同样,存储社交图谱数据的服务可能应该使用图数据库,例如Neo4j。基于微服务的应用程序通常混合使用SQL和NoSQL数据库,即所谓的多语言持久性方法。
用于数据存储的分区、多语言持久性体系结构具有许多优点,包括松散耦合的服务以及更好的性能和可伸缩性。但是,它确实引入了分布式数据管理的一些挑战。
第一个挑战是如何实现跨多个服务时,保持一致性的业务事务。看看为什么这是一个问题, 让我们看一下在线B2B商店的示例。客户服务(Customer Service)维护客户的信息,包括他们的信用额度。订单服务(Order Service)管理订单,并且必须验证一个新的订单不违反客户的信用额度。在此应用程序的整体版本中,订单服务可以简单地使用 ACID 事务来检查可用信用额度并创建订单。
相比之下,在微服务体系结构中,ORDER 和 CUSTOMER 表对各自的服务是私有的,如图 5-1 所示。
订单服务并不能够直接访问客户表。它只能使用客户服务提供的API。订单服务可能会使用分布式事务,称为两阶段提交(2PC:two-phase commit)。但是,在现代应用中,2PC通常不是一个可行的选择。CAP 原理要求你在可用和 ACID-风格 一致性之间做出选择,例如大多数NoSQL 数据库,并不支持 2PC. 跨服务和数据库保持数据一致性至关重要,因此我们需要另一种解决方案。
第二个调整是如何实现查询,这个查询要从多个服务获取数据。例如,让我们想想一下应用程序需要显示一个客户和他最近的订单。如果订单服务提供一个API,用于检索一个客户的订单。然后,您可以使用应用程序端联接检索此数据。应用程序从客服服务中检索客户,并从订单服务中检索客户的订单。然而,假设,订单服务值支持根据主键查询订单(可能他使用了一个NoSQL数据库,只支持基于主键的检索)。在这种情况下,没有明显的方法来检索需要的数据。
对于许多应用程序,解决方法是使用基于事件驱动的架构(event-driven architecture)。在这个架构中,微服务在发生值得注意的事情时发布事件。例如,当它更新业务实体时。其他的微服务订阅这些事件。当一个微服务收到一个事件时,他可以更新他自己的业务实体,这可能导致发布更多的事件。
你可以使用事件来实现跨多个服务的事务。一个事务包含一系列的步骤。每个步骤包含一个微服务更新一个业务主题,并发布一个事件,这个时间触发下一个步骤。下面的时序图显示了在创建一个订单的时候,你可以如何使用一个基于时间驱动的方法来检查信用卡是否可用。
微服务通过消息中间件交换事件:
一个更复杂的场景可能涉及更多的步骤,例如在客户信用卡检索的同时,检索库存。
如果(a)每个服务以原子方式更新数据库并发布一个事件(稍后会详细介绍),并且(b) Message Broker 保证事件至少传递一次,那么您就可以实现跨多个服务的业务事务。重要的是要注意这些不是 ACID 事务。它们提供的保证要弱得多,例如最终的一致性。此事务模型称为 BASE 模型。
您可以使用事件来维护预联接多个微服务拥有的数据的具体化视图。维护视图的服务订阅相关事件并更新视图。图5-5 描述客户订单视图更新程序服务,该服务根据客户服务和订单服务发布的事件更新客户订单视图。
当这个客户订单视图更新服务收到一个客户或者订单时间,他更新这个客户订单视图数据存储。你可使用一个文档数据库,比如MongoDB,为每个客户存储一个文档,来实现客户订单视图。客户订单视图查询服务处理一个客户和最近的订单请求,通过查询客户订单视图数据库。
一个事件驱动结构有多个好处和缺点。它支持实现跨多个服务并提供最终一致性的事务。另一个好处是,它还使应用程序能够维护物质化视图(materialized views)。
这种编程模型的一个缺点是,它比使用ACID事务更加的复杂。通常,您必须实施补偿事务才能从应用程序级故障中恢复。例如,如果信用卡检查失败,你必须取消一个订单。同样,应用程序必须处理不一致的数据。这是因为正在进行的事务所做的更改是可见的。如果应用程序从尚未更新的具体化视图中读取,则还可以看到不一致之处。另一个缺点是订阅者必须检测并忽略重复事件。
在一个基于时间驱动的架构中,也会有自动更新数据库和发布时间的问题。例如,订单服务必须在 ORDER 表中插入一条数据,然后发送一个订单创建事件。这两个操作必须以原子方式完成。如果在更新完数据库后服务崩溃了,但还没有发布这个事件。系统变诶不一致。确保原子性的标准方法是使用涉及数据库和消息代理的分布式事务。然而,由于上述原因,例如CAP定理,这正是我们不想做的事情。
实现自动通信的一种方法是让应用程序使用仅涉及本地事务的多步骤过程发布事件。诀窍是有一个事件(EVENT)表, 它的功能是充当消息队列,在这个数据库里面存储了消息实体的状态。这个应用开始一个本地数据库事务,更新业务实体的状态,插入一个事件到事件(EVENT)表里面,发布事件到消息中间件(Message Broker), 然后使用一个本地的事务来标记事件已经发布了。图5-6显示了这种设计。
订单服务插入一条数据到订单(ORDER)表里面,事件发布者线程或进程在 EVENT 表中查询未发布的事件,发布事件,然后更新事件(EVENT)表,来标记事件已经发布了。
这个方法有一些优点和缺点。一个好处是,它保证在不依赖于2PC(2阶段提交)的情况下为每次更新发布事件。此外,应用程序还会发布业务级事件,从而消除了推送这些事件需求。这种方法的一个缺点是,它可能容易出错,因为开发人员必须记得发布事件。这种方法的局限性在于,由于某些NoSQL数据库事务和查询功能的限制,因此在使用这类数据库时实现起来具有挑战性。
这个方法通过应用程序使用本地事务来更新事务的状态,发布事务。就消除了对二阶段步骤(2PC)的依赖。让我们现在看一下这个方法,通过应用程序简单的更新状态来实现原子性。
不使用2PC实现原子性的另一种方法是,由挖掘数据库事务或提交日志的线程或进程发布事件。应用程序更新数据库,所有变更记录在了数据库的传输日志里。事务日志挖掘线程或进程读取事务日志并将事件发布到消息代理(Message Broker)。图5-7显示了这种设计.
这个方法的另外一个例子是开源的 LinkedIn Databus 项目。数据总线挖掘 oracle 事务日志并发布与更改相对应的事件。LinkedIn使用Databus来保持各种派生数据存储与系统记录一致。
另一个例子是AWS DynamoDB中的流机制,它是一个管理的 NoSQL 数据库。DynamoDB流包含在过去24小时内对DynamoDB表中的项进行更改(创建、更新和删除操作)的按时间顺序的序列。一个应用程序可以从流中读取这些变更,并且,例如,发布这些事件。
事务日志挖掘有各种优点和缺点。一个好处是他在不使用 2PC 的情况下,为每次更新都发布了事件。事务日志挖掘也能简化应用程序,通过从应用程序的业务逻辑中分离事件发布。一个主要的缺点是,事务日志的格式是每个数据库专有的,甚至可以在不同的数据库版本之间更改。同样,从事务日志中记录的底层更新到工程顶层业务事件可能很困难。
传输日志的挖掘,通过让应用程序来做数据库的更新来消除对2PC的依赖。现在让我们看看另一种消除更新并仅依赖于事件的方法。
事件源通过使用一种完全不同的、以事件为中心的方法来持久化业务实体,从而在不使用2PC的情况下实现原子性。应用程序不存储实体的当前状态,而是存储一系列状态更改事件。当业务实体的状态发生变化时。一个新事件被添加到事件列表中。因为保存事件是一个单一操作,所以它本质上是原子的。
要了解事件源是如何工作的,可以考虑将订单实体作为一个示例。在传统方法中,每个订单映射到order表中的一行,并映射到其中的行。例如,一个ORDER_LINE_ITEM表。
但是当使用事件源,订单服务以其状态更改事件的形式存储订单: Created, Approved, Shipped, Cancelled. 每个事件都包含足够的数据来重建订单的状态。
事件持久化在事件存储中, 这是一个事件数据库。数据库有一个用于添加和检索实体事件的API。事件存储的行为也类似于我们前面描述的体系结构中的消息代理。它提供了一个允许服务订阅事件的API。事件存储将所有事件传递给所有感兴趣的订阅者。事件存储是事件驱动的微服务体系结构的支柱。
事件源有一个好处。它解决了实现事件驱动架构的一个关键问题,并使在状态改变时可靠地发布事件成为可能。因此,它解决了微服务体系结构中的数据一致性问题。另外,因为它持久化的是事件而不是域对象,所以它在很大程度上避免了对象关系阻抗不匹配问题。事件来源还提供了对业务实体所做更改的100%可靠的审计日志,并使实现确定实体在任何时间点的状态的临时查询成为可能。事件来源的另一个主要好处是,您的业务逻辑由交换事件的松散耦合的业务实体组成。这使得从单一应用程序迁移到微服务体系结构要容易得多。
事件源也有一些缺点。它是一种不同的,并且不熟悉的编程风格。所以存在学习曲线。事件存储值支持通过主键直接查询业务实体。你必须使用命令行查询响应分离(CQRS)来实现查询。因此,应用程序必须最终处理一致的数据。
在一个微服务架构中,每个微服务有他自己的私有数据库。不同的微服务可能使用不同的 SQL 和 NoSQL 数据库。虽然这种数据库架构具有显著的优点,但它也带来了一些分布式数据管理的挑战。第一个挑战是如何实现保持多个服务一致性的业务事务。第二个挑战是如何实现从多个服务检索数据的查询。
对于许多应用程序来说,解决方法是使用事件驱动架构(event-driven architecture). 实现事件驱动架构的挑战是如何自动更新状态,和如何发布事件。有几种方法可以实现这一点,包括使用数据库作为消息队列、事务日志挖掘和事件源。
基于微服务方法的存储设计大量的和各种各样的数据存储,更复杂的是你如何访问和更新数据,在保持数据一致性上,给开发和运维带来了巨大的挑战。NGINX 为这类数据管理提供了关键的支持,主要体现在三个方面:
NGINX微服务参考体系结构的三个模型中包含了特定于微服务的数据管理的例子,为你自己的设计决策和实现提供了一个起点。