微服务架构之事件驱动架构 - 简书

标签: | 发表时间:2022-01-05 17:19 | 作者:
出处:https://www.jianshu.com

前言

为了解决传统的单体应用(Monolithic Application)在可扩展性、可靠性、适应性、高部署成本等方面的问题,许多公司(比如Amazon、eBay和NetFlix等)开始使用微服务架构(Microservice Architecture)构建自己的应用。

微服务架构( 维基百科):
微服务(Microservices) 是一种软件架构风格 (Software Architecture Style),它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模组化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic) 的 API 集相互通讯。

但是,微服务架构在带来一系列好处的同时,也带来了若干挑战。除了分布式系统固有的复杂性以外,微服务架构也深刻影响了应用和数据库之间的关系, 与传统多个服务共享一个数据库的方式不同,微服务架构每个服务都有自己的数据库。对于开发者来说,这就为微服务中的数据管理提出了更高的要求。

微服务架构中的数据管理

在传统的单体应用中,通常使用单个的关系型数据库。这类数据库所提供的事务语义,具备 ACID特性。

ACID:

  • Atomicity(原子性):一个事务中的操作是原子的,其中任何一步失败,系统都能够完全回到事务前的状态
  • Consistency(一致性):数据库的状态始终保持一致
  • Isolation(隔离性):多个并发执行的事务不会互相影响
  • Durability(持久性):事务处理结束后,对数据的修改是永久的

应用得益于数据库的这些特性,能够用简单的方式对数据进行修改与读取,而无需花费太多精力考虑数据一致性问题。

但是,在微服务架构下,为了在微服务之间建立松耦合的关系,通常每一个微服务都会拥有自己独立的数据库,仅仅通过对外暴露的API来进行数据交换。这种情况下,我们就要面临分布式数据管理带来的挑战。也就是说, 在实现业务逻辑时,如何保证服务之间的数据一致性

实时一致性

我们首先考虑在系统中实现实时一致性的情况。比如以一个银行系统为例,客户通常会有一个储蓄账户和一个理财账户。现在,考虑客户从自己的储蓄账户向理财账户转账10000元的场景。

假设现在有两张表 deposit_account 和 finance_account,分别用于存储储蓄账户和理财账户的信息,用户的ID是201。那么,在单一数据库场景下,通过数据库事务可以很容易完成这个操作:

    Begin transaction
    update deposit_account_table set amount=amount-10000 where userId=201;
    update finance_account amount=amount+10000 where userId=1;
End transaction
commit;

这样在单体应用中,由于所有数据都是保存在同一个数据库中,通过数据库提供的ACID特性,就可以轻松实现数据的实时一致性。

但是,在微服务架构中,可能的设计是存在两个服务:储蓄服务(Deposit Service)和理财服务(Finance Service),假设由储蓄服务负责处理客户的转账请求。而如下图所示,这两个服务都分别维护自己的数据,因此储蓄服务无法直接访问理财服务的数据,而只能通过API去修改客户的余额。

微服务下的数据访问

此时,为了满足订单服务与客户服务之间的实时一致性要求,可以采用分布式事务,比如基于 两阶段提交协议(Two-phase commit, 2PC)的实现来做到这一点。(关于2PC,已经有大量的研究成果和成功实践经验,本文将不再做太多阐述,具体可自行参见相关文献和资料)

根据 CAP定理,我们追求实时一致性时,通常需要牺牲掉部分可用性。比如以上场景中,当 Finance Service 由于软硬件故障或网络问题而不可用的时候,系统将无法为用户提供内部转账服务。

此外,作为典型的同步操作,2PC也存在着比较比较严重的性能问题,并不适合高并发场景。因此,在数据一致性上我们需要寻求其他的解决方案。

最终一致性

如果我们考虑只保证系统的最终一致性,那么就可以避免使用2PC,从而提高系统可用性和性能。

仍然以以上的用户内部账户之间的转账服务为例。当用户从储蓄账户向理财账户转账时,减少储蓄账户的金额与增加理财账户的金额这两个动作,可以无需在一个事务里面完成,而是分成两步:

  1. 储蓄服务减去储蓄账户中的金额,并生成一个凭证(消息)发送给理财服务;
  2. 理财服务收到凭证后,在理财账户中增加相应的金额。

我们会发现以上过程在第1步完成之后,第2步完成之前,储蓄账户与理财账户之间实际上是存在短时间的数据不一致的。但是,只要最终第2步能够完成,系统的数据就仍然能够保持一致性,这就是我们所说的最终一致性。

在最终一致性这个前提下,即使理财服务在某段时间内不可用,系统仍然能够能为用户提供内部转账服务,从而提高了系统的可用性。

而这样一种基于最终一致性的解决方案,就是本文将要介绍的 事件驱动的架构(Event-driven Architecture)

事件驱动的架构

所谓事件驱动的架构,也就是 使用事件来实现跨多个服务的业务逻辑

在这一架构里,当有重要事件发生时,比如更新业务数据,某个微服务会发布事件,其它微服务则订阅这些事件;当某一微服务接收到事件就可以更新自己的业务数据,同时发布新的事件触发下一步更新。而事件的发布与订阅,则依赖于一个可靠的消息代理(Message Broker)。

以上文的场景为例,在事件驱动的架构中,从储蓄账户转账到理财账户的过程如下:

  1. 储蓄服务将用户的储蓄账户中的金额减少10000,并发布“向理财账户转账”事件;
  2. 理财服务获取“转账到理财账户”事件, 更新理财账户,将理财账户的金额增加10000,并发布“理财账户转入”事件;
  3. 储蓄服务获取“理财账户转入”事件,结束本次转账交易。
事件驱动的业务流程

在这里需要考虑的一个问题,就是转账失败处理。比如以上第2步如果因为“理财账户被冻结无法转入资金”之类的原因失败了,理财服务就应该发布“理财账户转入失败”事件,储蓄服务获取到该事件后,需要对储蓄账户进行回滚,将减少的金额重新增加回去。

以上的过程与传统的数据管理基于ACID模型不一样的是,它是基于 BASE模型的。

BASE:

  • Basically Available(基本可用):系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用
  • Soft State(软状态):允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性
  • Eventually Consistent(最终一致性):系统保证最终数据能够达到一致

事件发布

在事件驱动的架构中,跨服务完成业务逻辑的一个关键点是每个服务自动更新数据库和发布事件,也就是 要以原子粒度更新数据库和发布事件。例如,储蓄服务必须在对储蓄账户表进行更新,然后发布“向理财账户转账”事件,这两个操作需要原子化实现。如果服务在更新数据库之后、发布事件之前崩溃,系统会变得不一致。

保证数据更新与事件发布原子化的方法,有以下几种:

  • 使用本地事务发布事件
  • 挖掘数据库事务日志
  • 使用事件源

使用本地事务发布事件

一个实现原子化的方法是 使用本地事务来更新业务实体和事件列表,由一个独立进程来发布事件。具体来说,就是在存储业务实体状态的数据库中,使用一个事件表来充当消息队列。应用启动一个(本地)数据库事务,更新业务实体的状态,在事件表中插入一个事件,并提交该事务。一个独立的消息发布线程或进程查询该事件表,将事件发布到消息代理,并标注该事件为已发布。下图展示了这一设计。

363875b0-f8b9-45a4-a28c-eeed128b1fa0.png

储蓄服务更新储蓄账户的余额,然后在事件表中插入“转账到理财账户”的事件。事件发布线程或进程在事件表中查询未发布的事件并发布,然后更新事件表,将该事件标记为已发布。

这种方法的优点是:

  • 使用本地事务,保证了数据被更新时事件一定能够被发布
  • 实现简单,只需要系统具备本地事务的能力即可实现

这种方法的一个缺点是,数据更新操作与所要发布的事件之间的对应关系,是由应用的开发者实现的,因此有很大可能出错。

挖掘数据库事务日志

实现原子化的另一种方式是 由线程或者进程通过挖掘数据库事务或提交日志来发布事件。应用更新数据库,数据库的事务日志会记录这些变更。事务日志挖掘线程或进程读取这些日志,并把事件发布到消息代理。

比如一个B2C的电商网站,就可以通过挖掘订单数据的更新日志,来进行事件发布。如下图所示:

挖掘数据库事务日志

这一方法的范例是开源的 LinkedIn Databus 项目。Databus 挖掘 Oracle 事务日志并发布与之对应的事件,LinkedIn 则使用 Databus 维持各种来源的数据存储与记录系统一致。

另一个范例则是 AWS DynamoDB 采用的流机制。AWS DynamoDB 是一个可管理的 NoSQL 数据库,其中每个 DynamoDB 流包括 DynamoDB 表在过去 24 小时之内的时序变化,包括创建、更新和删除操作。应用能够读取这些变更,将其作为事件发布。

这种方法的优点是:

  • 要发布的事件直接来源于数据库的事务日志,因此不会出错
  • 应用无需关注事件的发布,简化了应用开发者的工作

但是这种方法也有一些缺点:

  • 事务日志的格式与所使用的数据库相关,因此事件挖掘 的实现会由于数据库的种类或版本的变化而随之需要修改
  • 由于是直接从数据库的更新记录生成事件,因此可能会无法逆向推断出业务逻辑,因此并不适合于所有场景(比如前文所述的转账场景)

使用事件源

事件源 采用一种截然不同的、以事件为中心的方法来保存业务实体——不同于存储实体的当前状态,应用存储的是状态改变的事件序列。每当业务实体的状态改变,新事件就被附加到事件列表,并且应用可以通过事件回放来重构实体的当前状态。鉴于保存事件是一个单一的操作,因此本质上也是原子化的。

要了解事件源如何运行,可以以储蓄服务为例。在传统的方法中,每次转账交易都会更新储蓄账户表的记录。而使用事件源的时候,储蓄服务以状态更改事件的方式存储用户的储蓄账户,每个事件都包含足够的数据去重建储蓄账户状态。

事件表

事件长期保存在事件仓库(Event Store),使用 API 添加和检索实体的事件。同时,事件仓库起到类似上文提及的消息代理的作用,通过 API 让服务订阅事件,将所有事件传达到所有感兴趣的订阅者。所以,事件仓库可以认为是数据库与消息代理的综合体,是事件源方法的支柱。

事件源方法

事件源方法有如下的优点:

事件源方法也有以下这些缺点:

  • 要实现一个可靠和高性能的事件仓库并不是一件容易的事情
  • 应用代码需要根据事件仓库的 API 进行重写
  • 事件仓库只直接支持通过主键查询业务实体,因此对于复杂视图的查询比较困难(可以通过CQRS方法解决,具体参见下文)

命令查询分离(CQRS)

在事件源方法中,不再直接存储任何业务实体的状态,而是代之以状态变更事件。在进行复杂视图的查询时,如果还按照与命令操作同样的方式,将会遇到一些困难。比如要发起如下的一个同时涉及储蓄账户和理财账户的查询操作:

    SELECT *
FROM DEPOSIT_ACCOUNT deposit, FINANCE_ACCOUNT finance
WHERE
    deposit.user_id = finance.user_id
    AND finance.state = 'active'
    AND deposit.amount > 100000
    AND finance.amount > 5000

在非事件源的方式下,可以很容易的从储蓄账户表和理财账户表查询到相应数据。但是在事件源方式下,事件仓库中存储的是一系列事件,并且只能通过主键(比如 deposit_account.id 或 finance_account.id)去查询相应的业务实体,此时要处理类似 deposit.amount > 100000 这样的查询条件以及条件组合时,是非常复杂和低效的。

为了解决这一问题,可以采用CQRS方法,将命令与查询分离。命令操作仍然通过各服务的 API 以更新事件列表的方式进行,而查询操作则通过一个统一的视图查询服务(View Query Service)完成。

CQRS方法

根据存储在事件仓库中的事件集合,可以计算得到每个业务实体的状态,这些状态以物化视图(Materialized View)的方式存储在一个数据库中。当有新的事件产生时,也同样会自动更新视图。这样,视图查询服务就可以像查询普通的数据库数据一样实现各种查询场景。具体的设计可参考下图所示:

CQRS服务架构

结论

在微服务架构中,每个微服务都有其私有数据存储,不同的微服务可能使用不同的数据库。这种架构带来便利的同时,也给分布式数据管理带来挑战,其中最大的挑战就是 在实现跨服务的业务逻辑时,如何保持服务之间的数据一致性

对于许多应用,解决方案就是使用事件驱动的架构。事件驱动的架构带来的挑战是如何原子化地更新状态和发布事件。有几个方法可以做到这一点,包括把数据库用作消息队列、事务日志挖掘和事件源。

参考文献

  1. Event-Driven Data Management for Microservices
  2. Base: An Acid Alternative
  3. Pattern: Event-driven architecture
  4. 如何用消息系统避免分布式事务?

相关 [微服务 架构 事件驱动] 推荐:

微服务架构之事件驱动架构 - 简书

- -
为了解决传统的单体应用(Monolithic Application)在可扩展性、可靠性、适应性、高部署成本等方面的问题,许多公司(比如Amazon、eBay和NetFlix等)开始使用微服务架构(Microservice Architecture)构建自己的应用. 微服务(Microservices) 是一种软件架构风格 (Software Architecture Style),它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模组化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic) 的 API 集相互通讯.

再谈EDA事件驱动架构

- - 人月神话的BLOG
EDA事件驱动架构首先不是对于传统的面向业务流程,数据等各种架构模式的完全否定,而是解决传统架构下无法很好解决的一些问题. 传统模式里面更加关注业务流程和业务对象,而EDA模式下将更加关注在整个业务流程中的关键状态点,已经由关键状态点触发的有明确业务含义的业务事件. EDA架构的核心仍然是基于消息的发布订阅模式,消息的特定就是准实时,异步和彻底解耦.

事件驱动架构避坑指南

- - DockOne.io
事件驱动架构非常强大,非常适合分布式微服务环境. 通过引入代理中介,事件驱动架构提供了更好的解耦架构、更容易的可扩展性和更高程度的弹性. 请求应答模式 (client-server) vs. 事件流模式 (pub-sub). 但与请求应答客户端-服务器类型架构相比,事件流模式的搭建更复杂. 在Wix过去的几年里,我们逐渐的将我们不断增长的微服务集(目前为 2300 个)从请求-应答模式迁移到事件驱动架构模式.

事件驱动架构在 vivo 内容平台的实践

- - 掘金 架构
当下,随着微服务的兴起,容器化技术的发展,以及云原生、serverless 概念的普及,事件驱动再次引起业界的广泛关注. 所谓事件驱动的架构,也就是使用事件来实现跨多个服务的业务逻辑. 事件驱动架构是一种设计应用的软件架构和模型,可以最大程度减少耦合度,很好地扩展与适配不同类型的服务组件. 在这一架构里,当有重要事件发生时,比如更新业务数据,某个服务会发布事件,其它服务则订阅这些事件;当某一服务接收到事件就可以执行自己的业务流程,更新业务数据,同时发布新的事件触发下一步.

谈微服务架构

- - 人月神话的BLOG
其实在前面很多文章谈到SOA,特别是系统内的SOA和组件化的时候已经很多内容和微服务架构思想是相同的,对于微服务架构,既然出现了这个新名称,那就再谈下微服务架构本身的一些特点和特性. 从这个图可以看到微服务架构的第一个重点,即业务系统本身的组件化和服务化,原来开发一个业务系统本身虽然分了组件和模块,但是本质还是紧耦合的,这关键的一个判断标准就是如果要将原有的业务系统按照模块分开部署到不同的进程里面并完成一个完整业务系统是不可能实现的.

微服务与架构师

- - 乱象,印迹
因为工作的关系,最近面试了很多软件架构师,遗憾的是真正能录用的很少. 很多候选人有多年的工作经验,常见的框架也玩得很溜. 然而最擅长的是“用既定的技术方案去解决特定的问题”,如果遇到的问题没有严格对应的现成框架,就比较吃力. 这样的技能水平或许适合某些行业,但很遗憾不符合我们的要求. 软件架构师到底应该做什么,又为什么这么难做好,这都是近来的热门问题,我也一直在和朋友们讨论.

面向服务与微服务架构

- - CSDN博客推荐文章
最近阅读了 Martin Fowler 和 James Lewis 合著的一篇文章  Microservices, 文中主要描述和探讨了最近流行起来的一种服务架构模式——微服务,和我最近几年工作的实践比较相关感觉深受启发. 本文吸收了部分原文观点,结合自身实践经验来探讨下服务架构模式的演化. 面向服务架构 SOA 思想概念的提出已不是什么新鲜事,大概在10年前就有不少相关书籍介绍过.

微服务架构实践感悟

- - mindwind
从去年初开始接触微服务架构的一些理念,然后到今年开始实施系统第四个大版本的架构升级决定采用这套架构理念. 最近关于微服务架构的讨论还是多起来,因为国外一些著名互联网公司(如:Amazon、Netflix 等)从实践中摸索出了一套新的大型系统架构方法论,并取得了成功,树立了很好的示范,然后这套方法论渐渐就被一些技术理论派 人士命名为微服务架构(Microservices).

微服务架构成功之路

- - CSDN博客推荐文章
本文来源于我在InfoQ中文站翻译的文章,原文地址是:http://www.infoq.com/cn/news/2015/07/success-of-microservices. 近年来,在软件开发领域关于微服务的讨论呈现出火爆的局面,有人倾向于在系统设计与开发中采用微服务方式实现软件系统的松耦合、跨部门开发;同时,反对之声也很强烈,持反对观点的人表示微服务增加了系统维护、部署的难度,导致一些功能模块或代码无法复用,同时微服务允许使用不同的语言和框架来开发各个系统模块,这又会增加系统集成与测试的难度,而且随着系统规模的日渐增长,微服务在一定程度上也会导致系统变得越来越复杂.

微服务架构-模块迁移

- - 人月神话的BLOG
对于遗留的单体应用,要进行微服务架构的改造往往比一个全新应用基于微服务架构实现更加困难. 对于单体应用的微服务架构改造,最常见的方式仍然是将低耦合的模块逐步迁出. 下面以一个采购系统中招投标模块迁出为例进一步思考单体应用的微服务架构改造步骤. 在整个模型中我们将模型进行简化,当迁出一个功能模块进行微服务化的时候,首先要考虑的就是对该模块进行集成架构分析,考虑该模块和外围的集成情况,其次才是考虑该模块内部的私有数据.