第五篇:事件驱动的数据管理
本文出自Event-Driven Data Management for Microservices,作者 Chris Richardson, 写于2015年5月19日
本文是构建微服务应用系列文章的第五篇。
本文将会改变方向,了解一下微服务应用中产生的分布式数据管理问题。
一、微服务和分布式数据管理的问题
单体应用通常只有一个关系型数据库。使用关系型数据库的核心优点是应用可以使用ACID事务,它提供了一些重要的保证:
- 原子性:变化必须被原子地执行;
- 一致性:数据库的状态总保持一致的;
- 隔离性:虽然事务并行执行,它们看起来也是连续执行的;
- 持久性:一旦事务提交,就不能再撤销;
这种保证的结果就是,应用只需要开始事务,改变多行数据(插入、更新和删除),并提交事务。
使用关系型数据的另外一个极大的好处是提供了SQL
,SQL
是一种丰富的、声明式且标准化的查询语言。你可以通过查询很容易地从多个数据库中获取数据。RDBMS查询计划器接着决定执行该查询的优化方式。你不需要担心底层的细节,比如如何访问数据库。而且,因为所有的数据都在一个数据库中,这样的查询是很容易的。
不幸的是,当我们转移到微服务架构的时候,数据访问变得非常复杂。这是因为每个微服务拥有的数据对于该服务来说是私有的,仅能通过它的API
来访问。封装数据能确保微服务之间松散耦合并且彼此之间独立发展。如果多个服务访问相同的数据,schema
更新需要消耗大量时间将更新协调所有的服务上。
更糟糕的是,不同的微服务可能使用不同类型的数据库。如今的应用存储和处理多种类型的数据,关系型数据库并不总是最好的选择。对于一些用例来说,特定的NoSQL
数据库可能有更方便的数据模型,并提供更好的性能和扩展性。例如,对于要存储和查询文本的服务来说,使用文本搜索引擎,比如Elasticsearch更有意义。类似的,存储社交网络数据的服务应该使用图数据库,比如Neo4j。这种情况下,基于微服务的应用通常要混合使用SQL
和NoSQL
数据库,就是所谓的多途径持久化方法(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 订单服务处理响应
更复杂场景可能涉及很多步骤,比如同时进行检查库存和验证顾客信用额度。
假如:
- 每个服务自动更新数据库并发布事件;
- 之后,消息代理确保该事件被至少交付一次,就能实现跨服务的事务;
值得注意的是这不是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表中发生的按时间排列的变化(create
、update
和delete
操作)。应用可以从这些流中读取那些变化,例如,作为事件发布。
事务日志挖掘优点和缺点并存。
优点;
- 确保在不使用2PC的情况下,每个更新的事件都会发布;
- 事务日志挖掘通过将事件发布从应用的业务中独立出来,达到简化应用的目的;
缺点:
- 事务日志的格式对于每个数据库是独有的,甚至在不同的数据库版本中也会发生变化;
- 从记录在事务日志的底层更新中,逆向分析出高层次的业务事件是十分困难的;
事务日志挖掘通过让应用只做一件事来消除对2PC
的需求:更新数据库。让我们以不同的办法来消除更新并且仅依赖于事件本身。
六、使用事件源
事件源通过使用完全不同的、以事件为中心的方法来持久化实体对象,在不依赖2PC
的前提下实现原子操作。应用存储一系列状态变化事件,而不存储实体的当前状态。应用通过重播事件来重构实体的当前状态。无论何时实体的状态发生变化,新的事件都会被放入事件列表的末尾。因为保存事件是单一操作,所以它本质上是原子操作。
为了了解事件源如何工作,考虑订单实体的例子。在传统方法中,每个订单映射ORDER表中的一行数据,映射ORDER_LINE_ITEM表中的多行数据。
但是当使用事件源的时候,订单服务以状态变化事件的形式来存储订单:创建、批准、运送、取消。每个事件包含足够的数据来重构订单的状态。
图5-8 事件能完成数据恢复
事件持久化到作为事件数据库的事件存储中。这个存储提供API
来加入和检索实体事件。事件存储也可以作为前面提到的架构中的消息代理,它提供了API
使得服务可以订阅事件。事件存储发送所有事件到感兴趣的订阅者。事件存储是事件驱动架构的基石。
事件源有几个优点。
- 解决了实现事件驱动架构中的一个关键问题,使得无论何时状态变化都能可靠发送事件;
- 解决了微服务架构中的数据一致性问题。因为它持久化事件而不是数据,极大地避免了对象关系中的阻抗不匹配的问题。事件源也提供了对实体对象造成变化的100%可靠的审计日志,使得在任何时间点确定实体状态的时间查询变得可行。
- 另外一个事件源的主要优点是业务逻辑由松散耦合的业务实体组成,这些业务实体之间进行事件的交换。这使得从单体应用迁移到微服务架构更加容易。
事件源也有自己的缺点。
由于不同的和不熟悉的编程风格,使得存在一条学习曲线;
-
事件存储仅直接支持通过主键查询业务实体。你必须使用command query responsibility separation (CQRS)
实现查询。
关于CQRS我实在找不出合适的词语描述它,具体的看一下此文
应用必须处理最终一致的数据;
七、总结
在微服务架构中,每个微服务有自己私有的数据存储。不同的微服务可能使用不同的SQL
和NoSQL
数据库。虽然这种数据库架构有很多优点,但是它也带来了一些分布式数据管理的挑战。
- 第一个挑战是如何在跨服务的事务中保持一致性;
- 第二个是如何实现从多个服务中查询数据;
对于多数应用来说,解决办法是使用事件驱动架构。实现事件驱动架构的一个挑战是如何原子化的更新状态和发布事件。有很多办法来完成,包括使用消息队列、事务日志挖掘和事件源。