Chris Richardson 微服务系列 第五篇 微服务之事件驱动的数据管理
这是使用微服务架构构建应用系列的第五篇文章.第一篇文章介绍了微服务架构模式并讨论了使用微服务的优势和劣势 ;第二篇和第三篇文章讨论了微服务架构不同层面的通信问题;第四篇文章密切探讨了服务发现的相关问题;本文章呢,我们换个口味,看看微服务架构模式中的分布式的数据管理问题。
微服务与分布式数据管理问题
一个单体应用一般只有一个关系型数据库,使用一个关系型数据的优势是应用可以实现ACID,为业务操作进行保证:
- Atomicity – 操作是原子性的
- Consistency – 数据库中的状态永远是一致的
- Isolation – 尽管事务是并发的执行,但他们表现的像顺序执行,事务之间不会彼此影响
- Durable – 一旦事务提交就不能再撤销
因此,应用可以很简单的开始一个事务,操作(增删改)多条数据,然后提交事务。
使用关系型数据库的另一个巨大优势是可以使用SQL,一种丰富的声明式的标准查询语言。 你可以很简单的写一条关联多个数据表的查询语句,关系型数据库管理系统会解析SQL来决定最优执行方案执行该查询。你不必关心诸如数据库如何访问这种低层次的细节问题,另外,你所有的应用数据在一个数据库中,查询起来很容易。
很不幸的是,当转换到微服务架构的时候,数据访问变的越来越复杂。这是因为每个服务拥有的数据是该服务所私有的,要想访问该服务的数据只能通过API。对数据进行封装可以确保微服务之间是松散耦合的,各个服务可以独立于其他服务进行演进。如果多个服务访问同样的数据,那么Schema的更新是很耗时而且需要协调所有服务进行更新。
不同微服务可能使用不同的数据库会让事情变得更糟。现代应用程序存储和处理各种各样的数据,关系数据库并不总是最好的选择。在一些用例下,一种特殊的NoSQL数据库可能有更加方便的数据模型并且提供更好的性能与扩展性 ,比如,对一个存储和查询文档的服务来讲,使用Elasticsearch这样的文档查询引擎可能更加合适;同样的,存储社交图谱的服务更合适使用Neo4J这样的图数据库 。总之,微服务架构的应用经常使用SQL和NoSQL的混合存储,通常叫做多态型数据持久性方案。
一个分区的多态型的数据持久性方案有诸多好处:松耦合、更高的性能、扩展性等,然而这也引入了分布式数据管理的挑战!
第一个挑战就是如何跨多个服务实现业务事务、维护数据的一致性。为什么这是一个问题?让我们看一个在线B2B商店的例子:用户服务维护诸如用户信用额度这样的信息,订单服务管理订单并且确保新订单没有超过用户的信用额度限制。在传统单体应用中,订单服务可以在创建新订单时简单的使用ACID事务去查看用户可用的信用额。
然而在微服务架构中,ORDER 和CUSTOMER表是各个服务所私有的,如下图所示:
订单服务不能直接去访问用户表而只能使用用户服务提供的API。订单服务可能潜在的使用分布式事务,也就是两段提交,然而现在的应用中两段提交通常不是一个可行的选择。CAP理论要求你在可用性和ACID风格的一致性之间进行选择 ,而且。许多流行的技术,比如NoSQL并不支持两段提交,这就尴尬了!维护跨服务和跨数据库的数据一致性是很有必要的,所以我们需要另一种解决方案。
另一项挑战就是如何在多服务中获取数据。比如。假设我们应用需要展示用户信息和他最近的订单,如果订单服务提供API查询用户订单,那么你可以使用 应用程序连接查询数据 :应用从用户服务查询用户信息,从订单服务查询订单。 假设,订单服务仅支持主键查询订单(也许使用了仅支持主键查询的NoSQL),这种情况下,没有好的方法来检索数据。
事件驱动架构
对很多应用来讲,方案就是使用事件驱动架构。在该架构中,一个微服务在一些业务发生时发布一个事件,比如更新一条业务记录,其他微服务会订阅该事件,当微服务收到事件后会更新自己的业务记录,导致其他的事件被发布。
你可以使用事件实现跨服务的业务事务。一个事务包含一系列步骤,每个步骤由某微服务更新业务记录并发布事件触发下一步骤组成。下面的图显示如何使用事件驱动方式在创建订单时检查可用信用额度 ,微服务间通过Message Broker交换事件。
- 订单服务创建状态为NEW的订单并发布Order Created事件
Paste_Image.png
- 客户服务消费Order Created事件,为该订单预订信用并发布Credit Reserved事件
Paste_Image.png
- 订单服务消费Credit Reserved事件并更改订单状态为OPEN
Paste_Image.png
更复杂的场景可能调用多个步骤,比如保留订单同时检查用户信用。
假设(a)每个服务原子性的更新数据库并发布事件– 并且– (b) Message Broker保证事件至少被交付一次,那么你就可以实现跨服务的业务事务了。我们必须注意到一点是,这并不是ACID事务,他们提供更弱一点的最终一致性,这种事务模型可以参考 BASE model.
你也可以使用事件来维护提前关联了多个微服务的物化视图。该服务通过订阅相关事件并更新视图,比如,用户订单视图通过订阅订单事件和用户事件来更新用户订单视图。
当客户订单视图更新服务接收到来自客户或者订单的事件时,它将更新客户订单视图。你可以使用Mongodb这样的文档数据库为每个用户存储一份文档来实现客户订单更新视图,客户订单视图查询服务可以查询客户订单视图数据库提供客户信息与最近客户订单数据。
事件驱动架构有很多优势也有一些劣势。它使得跨服务事务成为可能,并提供了最终一致性,另一个优势就是它使得应用可以维护物化视图;但是劣势之一是编程模型比ACID事务模型更为复杂 ,通常我们要实现补偿事务来恢复应用级别的错误。比如,一旦信用额度确认失败,你必须要取消订单,并且应用要处理不一致的数据, 这是因为随意的事务是可见的,应用如果读取未更新的物化视图,将会获取到不一致的数据,另外一个劣势是,订阅必须可以检测并忽略重复的事件。
实现原子性
在事件驱动的架构中,还有一个更新数据与发布事件的原子性问题。比如,订单服务需要插入一条数据到 ORDER表并且发布Order Created事件,这两个操作原子性完成是很有必要的:假如在数据库更新完成后,事件发布之前服务挂掉了,系统将会存在不一致。标准确保原子性的方式是使用分布式事务调用数据库系统和Message Broker。然而,根据前面我们描述的理由,比如CAP理论,我们是想避免这么干的。
使用本地事务发布事件
应用发布事件并保证原子性的方法之一是采用多步骤本地事务方法。技巧是有一张EVENT表,表其实就是模拟一个message queue。当然数据库中还存着业务数据的状态,应用开始一个本地数据库事务,更新业务数据记录并往EVENT表中插入一条数据,最后提交事务。一个单独的应用线程或进程轮询EVENT表,并根据查询结果往Message Broker推送事件消息,然后使用本地事务标记事件被发布。下图描述了该设计:
订单服务插入数据到ORDER表并且插入Order Created event 到EVENT表。数据发布者线程或进程轮询EVENT表中未发布的事件,发布事件,然后更新EVENT表标记事件已被发布。
这种方案有优势也有劣势。优势之一是保证了在不使用两段提交前提下事件在每次更新后一定被发布 ,当然,应用发布了业务级别的事件,减少了再去推断事件类型的麻烦。劣势之一是这种方案很容易出错,因为要求程序员必须记得更新后去发布事件,该方案另一个局限是使用NoSQL时,由于NoSQL的事务和查询能力局限,实现起来困难。
这种方案使用本地事务来更新数据和发布事件减少了两段提交的使用,现在让我们来看只需更新状态就达到原子性的方法。
挖掘数据库事务日志
另一种不需要两段提交就实现原子性的方法是挖掘数据库事务日志或提交日志实现事件发布,这就要求变化操作要被记录在数据库事务日志当中,事务日志挖据线程或进程读取事务日志并发布事件到 Message Broker。下图展示了这种方案:
例子之一就是开源的LinkedIn Databus 项目。 Databus 挖掘 Oracle事务日志并发布相应的事件, LinkedIn使用 Databus 来保持各种派生数据存储与记录系统的一致。
另一个例子就是NoSQL数据库streams mechanism in AWS DynamoDB, DynamoDB 流包含在过去 24 小时向 DynamoDB 表中的记录所做的时序性变化 (创建、 更新和删除操作)。应用程序可以从流中读取这些更改并将它们作为事件发布。
事务日志挖掘有优势也有劣势。优势之一是在不使用两段提交的前提下保证了事件一定被发布,事务日志挖据也可以通过拆分应用业务逻辑事件的发布简化整个应用。该方案一个巨大的劣势是:事务日志每个数据库都不同,甚至相同数据库不同版本也会不同(摊手),而且我们很难从低级别事务日志的更新记录中反推高级别的业务事件。
事务日志挖掘通过更新数据库来避免使用两段提交。现在让我们看看一种消除了更新,并完全依靠的事件的方案:
使用事件源
事件源通过使用截然不同,以事件为中心的方案持久化业务实体,达到了不使用两段提交前提下 的原子性。这种方案存储一系列状态变化的事件而不是存储当前实体的状态。应用可以通过重放事件来重新构建实体的当前状态。一旦业务实体发生变化,一个新的事件就会被添加到事件列表中,由于保存事件是单一操作,本身就是原子性的。
为查看事件源如何工作,我们以订单实体为例子。传统方案下,每个订单会映射到ORDER表并记录数据到ORDER_LINE_ITEM。但当使用事件源编程时,订单服务以存储订单状态变化事件来存储订单:订单创建、批准、运输、取消。每个事件有充足的信息来重新构建订单。
事件存储到专门存储事件的数据库中,数据库提供添加和查询实体事件的API。这个事件数据库也会像我们上面描述的Message Broker一样提供让其他服务订阅事件的API,事件数据库向对其感兴趣的订阅者发布事件,这个事件数据库是事件驱动型微服务的骨架。
事件源的优势:它解决了一个实现事件源架构的关键问题,使得事件发生时可靠的发布事件成为可能,因此,它解决了微服务架构中数据一致性的问题。当然,因为他持久化事件而非领域对象,也避免了面向对象到关系型数据库中的阻抗不匹配问题。事件源也为实体变动提供了100%可靠的审计日志,并使其能够执行可以确定在任何时间点实体状态的时态查询。事件源的另一巨大优势是,业务逻辑与交换事件的实体是松耦合的,这使得迁移一个单体应用到微服务架构更加简单。
事件源也有一些劣势:对程序员来讲这是完全不同的,非常陌生的编程风格,学习曲线陡峭。事件数据库仅仅直接支持主键方式查询业务实体,必须使用 Command Query Responsibility Segregation (CQRS) 来实现查询。因此,应用必须来处理最终一致性。
总结
在微服务架构中,每个服务拥有自己的数据存储。不同服务可能是一不同的SQL或者NoSQL数据库。这样的数据库架构有很多优势,当然也带来了分布式数据管理的挑战。挑战之一就是如何实现跨多服务的业务逻辑事务来维持一致性,挑战之二就是如何从多服务查询数据。
对很多应用来讲,使用事件驱动架构来应对这些挑战。实现事件驱动架构的挑战之一是如何保证更新数据状态和发布事件的原子性 ,有几种方案来实现,包括以数据库为message queue,事务日志挖掘,事件源。
剩余的文章将讨论其他方面的问题。