第五篇:事件驱动的数据管理

本文出自Event-Driven Data Management for Microservices,作者 Chris Richardson, 写于2015年5月19日


本文是构建微服务应用系列文章的第五篇。

本文将会改变方向,了解一下微服务应用中产生的分布式数据管理问题。

一、微服务和分布式数据管理的问题

单体应用通常只有一个关系型数据库。使用关系型数据库的核心优点是应用可以使用ACID事务,它提供了一些重要的保证:

  • 原子性:变化必须被原子地执行;
  • 一致性:数据库的状态总保持一致的;
  • 隔离性:虽然事务并行执行,它们看起来也是连续执行的;
  • 持久性:一旦事务提交,就不能再撤销;

这种保证的结果就是,应用只需要开始事务,改变多行数据(插入、更新和删除),并提交事务。

使用关系型数据的另外一个极大的好处是提供了SQLSQL是一种丰富的、声明式且标准化的查询语言。你可以通过查询很容易地从多个数据库中获取数据。RDBMS查询计划器接着决定执行该查询的优化方式。你不需要担心底层的细节,比如如何访问数据库。而且,因为所有的数据都在一个数据库中,这样的查询是很容易的。

不幸的是,当我们转移到微服务架构的时候,数据访问变得非常复杂。这是因为每个微服务拥有的数据对于该服务来说是私有的,仅能通过它的API来访问。封装数据能确保微服务之间松散耦合并且彼此之间独立发展。如果多个服务访问相同的数据,schema更新需要消耗大量时间将更新协调所有的服务上。

更糟糕的是,不同的微服务可能使用不同类型的数据库。如今的应用存储和处理多种类型的数据,关系型数据库并不总是最好的选择。对于一些用例来说,特定的NoSQL数据库可能有更方便的数据模型,并提供更好的性能和扩展性。例如,对于要存储和查询文本的服务来说,使用文本搜索引擎,比如Elasticsearch更有意义。类似的,存储社交网络数据的服务应该使用图数据库,比如Neo4j。这种情况下,基于微服务的应用通常要混合使用SQLNoSQL数据库,就是所谓的多途径持久化方法(polyglot persistence approach )

一个分区的、多途径的存储架构(polyglot-persistent architecture )有很多的优点,包括松散耦合的服务和更好的性能、扩展性。但是,也引入了一些分布式数据管理的挑战。

第一个挑战是如何实现跨越多个服务且保持一致性的事务,为了了解为什么这是一个问题,让我们看一下在线的B2B商店的例子。顾客服务维护顾客的信息,包括他们的信用额度。订单服务管理订单且必须确保新订单不能超过用户的额度限制。在单体应用中,订单服务简单地使用ACID事务来验证可用的信用额度并创建订单。

作为对比,微服务架构中,OEDER和CUSTOMER表对于它们各自的服务是私有的,如图5-1所示:

这里写图片描述

图5-1 每个微服务都有自己的数据

订单服务不能直接访问CUSTOMER表,它只能使用顾客服务提供的API。订单服务可能使用分布式事务,也就是著名的两阶段提交协议(two-phase commit ,2PC)。然而,在当今应用中,2PC通常不是一个可行的选择。CAP理论要求在可用性和ACID风格的一致性之间选择,而可用性通常是更好的选择。另外,现在许多技术,比如大多数的NoSQL数据库,不支持2PC。跨服务和数据库维护数据一致性很有必要的,所以我们需要其他的解决方案。

第二个挑战是如果实现从多种服务中查询数据。例如。假设应用需要展示客户和他最近的订单。如果订单服务提供API来获取顾客的订单,你会需要使用跨应用join来获取数据。应用从顾客服务处获取顾客信息,从订单服务处获取顾客的订单。但是,如果订单服务仅支持通过主键来查询订单(可能它使用NoSQL数据库并且仅仅支持基于主键的检索)。在这种情况下,没有任何明显的方法来获取所需的数据。

二、事件驱动的架构

对于多数应用来说,解决方案是使用事件驱动架构。在这个架构中,当值得注意的事情发生时,比如要更新实体对象时,微服务会发布事件。其他的微服务订阅这些事件。当微服务接收了这个事件后可以更新自己的实体对象,这个动作可能会导致更多的事件发布。

可以使用事件来实现跨越多个服务的事务一个事务包含一系列的步骤,每个步骤由一个微服务组成,它可以更新实体对象并且发布事件触发下一步骤。下面的序列图显示了当创建订单时,如何使用事件驱动架构的方法验证可用的信用额度。

微服务通过消息代理来交换事件

  • 订单服务创建状态为NEW的订单并发布订单创建事件;

    这里写图片描述

    图5-2 订单服务发布事件

  • 顾客服务消费订单创建事件,查询该订单对应的信用剩余额度,并且发布信用剩余额度事件;

    这里写图片描述

    图5-3 顾客服务响应

  • 订单服务消费信用剩余额度事件,并改变订单状态为OPEN;

    这里写图片描述

    图5-4 订单服务处理响应

更复杂场景可能涉及很多步骤,比如同时进行检查库存和验证顾客信用额度。

假如:

  1. 每个服务自动更新数据库并发布事件;
  2. 之后,消息代理确保该事件被至少交付一次,就能实现跨服务的事务;

值得注意的是这不是ACID事务。它们提供了更弱的保证,比如最终一致性。这个事务模型被称为BASE model

也可以使用事件来维护一个物化视图,它预先join多个服务的数据。维护该视图的服务订阅相关事件并更新该视图。图5-5描绘了顾客订单视图更新服务,它基于顾客服务和订单服务发布的事件来更新这个视图。

这里写图片描述

图5-5 顾客订单视图被两个服务访问

当顾客订单视图更新服务接收顾客或者订单事件,它就会更新顾客订单视图的数据存储。可以使用文档型数据库,比如MongoDB,来实现顾客订单视图,为每个顾客存储一个文档。顾客订单查询服务通过查询顾客订单视图来处理有关顾客、最近订单的请求。

一个事件驱动的架构的优缺点并存。

优点:

  • 实现了跨服务的事务,并提供最终一致性;
  • 使应用可以维护物化视图

缺点:

  • 编程模型比ACID事务要复杂的多。一般情况下,必须实现事务补偿,以便从应用层次中的故障恢复;例如,如果信用卡验证失败,必须取消订单。应用必须处理不一致的数据。因为由运行中的事务造成的变化是可见的。如果应用从一个还没有更新的物化视图中读取数据,也可以导致数据的不一致。
  • 订阅者必须检测和忽略重复的事件

三、实现原子性

在事件驱动的架构中也有数据库更新和发布事件的原子性操作问题。例如,订单服务必须插入一行数据到ORDER表,并且发布一个订单创建事件。保证两个操作的执行是原子性的很有必要。如果在更新完数据库后,发布事件之前,服务突然宕机,系统会变得不一致。确保原子性的标准方式是使用涵盖数据库和消息代理的分布式事务。然而,出于上述的原因,比如CAP理论,这真的是我们不愿意做的

四、使用本地事务发布事件

对于应用来说,一个实现原子性的方法是使用仅包含本地事务的多步骤处理。这个技巧必须有一个EVENT表,它充当一个消息队列,在这个数据库中存储着实体对象的状态。应用开始本地数据库事务,更新实体对象的状态,插入一个事件到EVENT表中,并提交这个事务。使用一个独立的线程或者进程来查询EVENT表,发布事件到消息代理中,接着使用本地事务来标志该事件已发布。图5-6 显示了这个设计:

这里写图片描述

图5-6 使用本地事务来实现原子性

订单服务插入一行数据到ORDER表中,并插入订单创建事件到EVENT表。事件发布线程或者进程查询EVENT表来获取没有发布的事件,发布该事件,接着更新EVENT表来标志事件已发布。

这个方法也是优点和缺点并存。

优点:

  • 确保了每个更新事件的发布不依赖2PC,并且应用发布业务级别的事件,这就消除了推测它们的需要;

缺点:

  • 易于出错,因为开发者必须牢记发布事件;
  • 一个限制是当使用一些NoSQL数据库时,由于有限的事务和查询能力,实现起来很有挑战性;

这个方法消除了对2PC的使用,通过让应用使用本地事务来更新状态和发布事件。现在让我们想一个只让应用更新状态来实现原子性的方法。

五、挖掘数据库事务日志

另外一种不使用2PC实现原子性的方式:由发布事件的线程或者进程来挖掘数据库事务日志。应用更新数据库,所以数据库的事务日志中必然有该变化的记录,事务日志挖掘线程或者进程读取事务日志并发布事件到消息代理中,如5-7显示了这个设计:

这里写图片描述

图5-7 消息代理组件评估数据事务

这种方式的一个例证是开源的LinkedIn Databus项目。Databus挖掘Oracle的事务日志,并发布对应该变化的事件。LinkedIn使用Databus来使得多种导出的数据与系统记录保持一致。

另外一个例子是AWS DynamoDB中的流式机制,这是一个被管理的NoSQL数据库。DynamoDB流包括过去的24小时中DynamoDB表中发生的按时间排列的变化(createupdatedelete操作)。应用可以从这些流中读取那些变化,例如,作为事件发布。

事务日志挖掘优点和缺点并存。

优点;

  • 确保在不使用2PC的情况下,每个更新的事件都会发布
  • 事务日志挖掘通过将事件发布从应用的业务中独立出来,达到简化应用的目的;

缺点:

  • 事务日志的格式对于每个数据库是独有的,甚至在不同的数据库版本中也会发生变化;
  • 从记录在事务日志的底层更新中,逆向分析出高层次的业务事件是十分困难的;

事务日志挖掘通过让应用只做一件事来消除对2PC的需求:更新数据库。让我们以不同的办法来消除更新并且仅依赖于事件本身。

六、使用事件源

事件源通过使用完全不同的、以事件为中心的方法来持久化实体对象,在不依赖2PC的前提下实现原子操作。应用存储一系列状态变化事件,而不存储实体的当前状态。应用通过重播事件来重构实体的当前状态。无论何时实体的状态发生变化,新的事件都会被放入事件列表的末尾。因为保存事件是单一操作,所以它本质上是原子操作。

为了了解事件源如何工作,考虑订单实体的例子。在传统方法中,每个订单映射ORDER表中的一行数据,映射ORDER_LINE_ITEM表中的多行数据。

但是当使用事件源的时候,订单服务以状态变化事件的形式来存储订单:创建、批准、运送、取消。每个事件包含足够的数据来重构订单的状态。

这里写图片描述

图5-8 事件能完成数据恢复

事件持久化到作为事件数据库的事件存储中。这个存储提供API来加入和检索实体事件。事件存储也可以作为前面提到的架构中的消息代理,它提供了API使得服务可以订阅事件。事件存储发送所有事件到感兴趣的订阅者。事件存储是事件驱动架构的基石

事件源有几个优点。

  • 解决了实现事件驱动架构中的一个关键问题,使得无论何时状态变化都能可靠发送事件
  • 解决了微服务架构中的数据一致性问题。因为它持久化事件而不是数据,极大地避免了对象关系中的阻抗不匹配的问题。事件源也提供了对实体对象造成变化的100%可靠的审计日志,使得在任何时间点确定实体状态的时间查询变得可行。
  • 另外一个事件源的主要优点是业务逻辑由松散耦合的业务实体组成,这些业务实体之间进行事件的交换。这使得从单体应用迁移到微服务架构更加容易

事件源也有自己的缺点。

  • 由于不同的和不熟悉的编程风格,使得存在一条学习曲线

  • 事件存储仅直接支持通过主键查询业务实体。你必须使用command query responsibility separation (CQRS)

    实现查询。

    关于CQRS我实在找不出合适的词语描述它,具体的看一下此文

  • 应用必须处理最终一致的数据

七、总结

在微服务架构中,每个微服务有自己私有的数据存储。不同的微服务可能使用不同的SQLNoSQL数据库。虽然这种数据库架构有很多优点,但是它也带来了一些分布式数据管理的挑战。

  • 第一个挑战是如何在跨服务的事务中保持一致性;
  • 第二个是如何实现从多个服务中查询数据;

对于多数应用来说,解决办法是使用事件驱动架构实现事件驱动架构的一个挑战是如何原子化的更新状态和发布事件。有很多办法来完成,包括使用消息队列、事务日志挖掘和事件源。

猜你喜欢

转载自blog.csdn.net/lmy86263/article/details/75212499