微服务的事件驱动数据管理
这是本书关于构建微服务应用的第五章。 第一章介绍了微服务架构模式并且讨论了其优点和缺点。 第二章和第三章描述了微服务架构内部通讯的各个方面。第四章探讨了与服务发现紧密相关的问题。 本章,我们转向在微服务架构中分布式数据的管理问题。
微服务和分布数据管理问题
典型的单体应用都有一个关系型数据库。使用关系型数据库,一个很关键的好处是你的应用可以使用ACID事务,它提供了一些很重要的保障:
原子性-原子性操作
一致性-数据库的状态始终是一连续一致的。
隔离性-尽管事务是并行执行,他们看起来像是顺序执行
持久性-一旦事务被提交,它不会取消。
因此,你的应用能简单地开始事务,更改(插入,更新和删除)多行,然后提交事务。
用关系型数据库的另外一个好处是它提供了SQL,它是富,声明式的,标准化的查询语言。 你可以轻易地写一条语句组合来自多张表的数据。 RDBMS查询计划器,然后确定最佳的执行查询的方法。 你不必关系很底层的细节部分,比如如何访问数据库等。 并且,因为你所有的应用数据在一个数据库中,很容易查询。
当使用微服务架构,数据访问就变得更复杂。那是因为数据是被每个微服务所拥有,对每个服务来说是私有数据,只能通过它提供的API来访问。 封装数据确保了微服务是松耦合,每个服务可以独立的演进 。如果多个服务访问同样的数据,数据模式更新需要耗时,协同更新到所有有的服务。
更糟糕的,不同的微服务经常使用各种不同的数据库。 现代的应用存储和处理各种类型的数据,并且关系型数据库有时候并不是最好的选择。对有些用例来说,一个特别的NoSQL数据库或许有更方便的数据模型和提供更好的性能和可扩展性。 比如,存储和查询文本搜索用文本搜索引擎Elasticsearch会更合理。相似地,一个服务保存社交数据可能使用图数据库比较合适,比如Neo4j。 因此,基于微服务的应用经常使用SQL和NoSQL混合数据库,也就是所谓的polyglot persistent方法。
一个分区的,polyglot-persistent架构的数据存储有很多的优点,包括松耦合的服务和更好的性能和扩容能力。 然而,它的确带来一些分布式数据管理的挑战
第一个挑战是如何实现商业事务,它要维持跨服务的一致性。为了明白为什么这是个问题,让我们来看个在线B2B商店的例子。 客户服务(Customer Service)维护这客户的信息,包括他们的信誉记录等。 订单服务(Order Service)管理订单并且必须验证新的订单不会违反客户的信用卡限制。 在这个应用的单体式版本里面,订单服务能简单地用ACID事务来处理可用的信用卡和创建这个订单。
在微服务架构下的这个应用,ORDER(订单)和CUSTOMER(客户)是各个服务私有的,如果5-1所示:
图5-1 每个微服务拥有的私有数据库订单服务(Order Service)不能直接访问Customer表。 它仅能调用客户服务(Customer Service)提供的API。 订单服务可以使用分布式事务(Distributed Transactions),也就是2步提交机制(two-phase commit-2PC)。然而,在现代应用中,2PC通常不是一个可实施的选项。CAP理论要求你要在可用性和ACID-style 一致性之间选择之一,并且通常来说,可用性是更好的选择。 更深入一点,许多现代的技术,比如NoSQL 数据库,不支持2PC机制。 维护跨服务和数据库的一致性是本质的要求。 因此,我们需要另外的解决方案。
第二个挑战是如何实现跨服务的查询。比如,让我们想象应用需要显示一个客户和他最近的订单。 如果订单服务提供API获取客户的订单,你可以通过应用侧的连接(Join)获取这些订单。应用从客户服务获取客户信息,从订单服务获取客户订单的信息。 然而,假设订单服务(Order Service)仅支持通过主键(Primary Key)查询订单(或许它用NoSQL数据库,仅支持基于主键的存取)。 在这种情况下,没有什么很明显的方法获取需要的数据。
事件驱动架构
对多数应用来说,解决的方法是使用一种叫做事件驱动的架构(Event-Driven Architecture)。 这种架构中,当某个重大事情发生时,一个微服务发布一个事件,比如,当更新业务实体(Business Entity)时。 其他的微服务订阅了这些事件。当微服务收到一个事件它能更新它自己的业务实体,这可能会导致更多的事件被发布。
你可以用事件机制实现跨多个服务的业务事务处理(business transactions)。一个事务包括一系列的步骤。 每一步包括一个微服务更新一个业务实体并且发布一个事件触发下一个步骤。 以下的时序图(sequence diagram)显示了你如何在创建一个订单时,使用一个事件驱动的方法检查可用的信用记录。
微服务通过消息代理交换事件:
订单服务创建一个订单(其状态为NEW)并且发布一个订单创建事件
图5-2 订单服务发布一个事件客户服务消费这个创建的订单消息,保留信用卡扣款并且发布一个信用卡保留事件。
图5-3 客户服务做出响应订单服务消费信用保留消息并且更改订单的状态为OPEN。
图5-4 订单服务执行事件动作一个更复杂的情景或许会涉及额外的步骤,比如在保留库存的同时检查客户的信用卡余额。
假设(a)每个服务原子更新数据库和发布一个事件(一会讨论更多),并且(b)服务代理保证事件至少被传递一次(Delivered),那么你可以跨多个服务实现商业交易。 需要强调的是这些操作不是ACID交易。 他们提供弱保证比如最终一致性(eventual consistency)。 这种交易模型已经被认为是基础模型(BASE Model)。
你也可以用事件维护物化视图(materialize views),它是预链接数据(pre-join data)被多个微服务拥有。 这个维护视图的服务订阅相关的事件并且负责更新这个视图。图5-5 描述了一个客户订单视图(Customer Order View)更新服务,它基于客户和订单服务发布的事件更新客户订单服务视图。
图5-5 被两个服务访问的客户订单视图当客户订单视图更新服务接收到一个客户或是订单服务时,它更新客户订单视图数据存储(datastore)。 你可以用一个文档数据库(Document Database,比如MongoDB)为每个客户保存一个文档去实现客户订单视图。这个客户订单视图查询服务通过查询客户订单视图存储来处理一个客户和最近订单的请求。
事件驱动也有其优缺点。它帮助实现跨多个服务并且提供最终一致性的事务(transactions)机制。 另一个优点是它使一个应用维护物化视图(Materialized View )。
这种编程模型比应用ACID事务更复杂。通常你必须实现事务补偿机制用于恢复应用级别的故障;举个例子,如果信用卡检查失败,你必须取消交易。还有,应用必须处理非一致性数据。那是因为正在进行的交易是可视的。如果应用读取正在更新的物化视图,它也能看到数据的不一致性。 另外,事件的订阅者必须检测和忽略重复的事件。
获取原子性(Atomicity)
在事件驱动的架构中更新数据库和发布一个事件也存在原子性操作问题。比如,订单服务必须插入一行数据到订单表(Order Table)中并发布一个订单创建(Order Created)事件。这两种操作是原子操作是极其重要的要求。如果服务在更新数据库之后但在发布这个创建订单事件之前垮掉,这个系统就会变的不一致(inconsistent)。 保证原子性标准的方法是在涉及到数据库和消息代理操作时,使用分布式事务机制。然而,像以前描述的原因,比如CAP理论,这正是我们不想这样做的原因。
使用本地事务发布事件消息
对应用来说,获取原子性的一种方法是对仅涉及本地操作的事务使用多步处理(multi-step process involving only localtransactions). 这个诀窍是在保存商业实体状态的数据库中使用一个事件表(EVENT table),它的作用像是一个消息队列。这个应用开始一个本地数据库事务(Begin transaction),更新商业实体状态,插入一个事件在事件表(EVENT table)中,然后提交这个事务(commit transaction)。一个独立的应用线程(separate app thread)或进程查询这个事件表,发布事件到消息代理器,然后用本地事务标记事件已被发布。图5-6 展示了这种设计。
图5-6 用本地事务获得操作的原子性订单服务插入一行数据到订单表中(ORDER table)并且插入一条订单创建事件到事件表中(EVENT
table)。事件发布线程或发布进程查询事件表,发现未发布的事件,发布它们,然后更新事件表,标志事件已发布。
这种方法也是优缺点并存。 一种优点是它不依赖于2PC(分布事务机制-2步提交)确保每个更新事件被发布。 同时,应用发布商业基本事件,它消除了推理他们的需求(the need to infer them)。这种方法的一种缺点是对应开发者来说易于出错(error-prone),因为它要求开发者必须要发布事件(记住)。另外一种限制,当使用NoSQL数据库时,实现这种方法是一种挑战,主要是因为这种数据库的事务和查询能力局限性。
这种方法通过使用本地事务更新状态和发布事件,消除了使用2PC分布式事务机制。接下来,让我们如何通过应用简单更新状态的方法获取操作的原子性
挖掘一个数据库事务日志
不使用2PC分布式事务机制,获取原子性操作的另外一种方法是发布事件的线程或进程挖掘数据库的事务或提交日志。 应用更新数据库,因此更改被记录到数据库的事务日志中。 事务日志挖掘线程或进程读取这些事务日志并且发布事件到消息代理器。图5-7 展示了这种设计。
图5-7 消息代理器仲裁(arbitrate)数据事务一个使用这种方法的例子是开源的Linkedin Databus 项目。Databus挖掘Oracle事务日志并且发布相关的更改事件。LinkedIn使用Databus确保获取数据和系统记录的一致性。
另外一个例子是AWS DynamoDB的流机制(Streams Mechanism in AWS DynamoDB),它是一个受控的NoSQL数据库。一个DynamoDB流包括最近24小时内更新操作(create,udpate和delete操作)的时间排序(time-ordered sequence)。一个应用能从流中,读取这些更新并且,比如,发布他们为事件消息。
事务日志挖掘也有其优缺点。 优点之一,不使用2PC分布式事务机制,对每个更新都会有一个事件被发布。事务日志挖掘方式,通过分离事件发布和商业应用逻辑,简化了应用复杂性。 这种方式的一个主要的缺点是事务日志的格式因不同数据库而异,有些甚至因数据库版本不同而不同。 还有就是,从事务日志中,低级更新记录反向工程出高级商业实践会很困难。
事务日志挖掘消除了2PC分布事务提交的需要,让应用只做一件事:更新数据库。现在,让我们看看消除更新数据库并且仅仅依赖事件本身的方法。
使用事件源驱动(Event Sourcing)
事件源驱动(Event Sourcing)不是采用2PC方式,通过截然不同的,事件为中心的方式持久化商业实体获得原子性。不是保存一个实体的当前状态,而是应用保存一系列状态更改事件。应用通过重放这些事件重新构建实体的当前状态。 无论何时商业实体的状态发生改变,一个新的事件被追加到事件列表中。因为保存一个事件是单一操作,它固然是原子操作。
为了明白事件源驱动方式如何工作,可以以订单实体(Order Entity)作为例子。传统的方式,每个订单会映射成订单(Order)表里的一行和Order_Line_Item多行。
但是当使用事件源驱动的方式,订单服务(Order Service)保存一个订单的形式是状态更改事件:创建(Created),批准(Approved),装箱(Shipped),取消(Cancelled)。 每个事件包含足够的数据重构(Reconstruct)订单的状态
图5-8 事件能有完整的恢复数据事件被持久化在事件存储中,它是个事件数据库。这个存储提供增加和获取一个实体事件的API接口。 这个事件存储行为上类似于消息代理器(Message Broker)前面描述了这种架构。它提供一个API接口允许服务订阅事件。事件存储传递事件到感兴趣的订阅者。事件存储是事件驱动微服务架构的主干(Backbone)。
事件驱动方式有些优点。 它解决了一个在实现事件驱动架构中关键问题,无论何时实体状态改变都会可靠地发布事件。因此它解决了在微服务架构中数据一致性的问题。 同时,因为是持久化数据而不是领域对象,它基本上是避免了对象关系阻抗不匹配(relational impedance mismatch problem)的问题。事件驱动也提供了100%可靠的实体状态更改审计日志,可以实现在任何时间点上临时查询确定实体状态成为可能。 事件驱动的另外一个主要的优点是你的商业逻辑包含松耦合的相互交换事件的商业实体。它极大地简化了从单体应用到微服务架构迁移的复杂性。
事件驱动当然也有缺点。 它是一个不同的,不熟悉的编程形式(Style),因此会存在一些学习的曲线。 事件存储仅直接支持通过主键(Primary Key)查询商业实体。你必须使用CQRS(Command Query Responsibilty Separation)实现查询。因此,应用必须处理最终一致性数据问题。
结论
在一个微服务架构里,每个微服务有自己的私有数据存储。不同的微服务或许会用不同的SQL和NoSQL数据库。 这种数据库架构有很大有点,但也制造了一些分布式数据管理的挑战。第一个挑战是如何实现商业事务,它能维持跨服务的一致性。第二个挑战是如何实现跨服务获取数据的查询。
对多数应用来说,解决方案是使用事件驱动的架构。实现事件驱动架构的一种挑战是如何原子性地更新状态和如何发布事件消息。 有几种方式可以取得这种效果,包括使用数据库作为消息队列,事务日志挖掘,和事件源驱动等。
翻译自“Microservice -from design to deployment" by Chris Richardson with Floyd Smith.
网友评论