介绍基于事件的架构

介绍基于事件的架构

译自:Introduction to Event-Driven Architecture

后面将引入几篇与EDA相关的文章,目的在于充分掌握EDA架构的优劣势。

目录

在前面的微服务介绍一文中讨论了服务的颗粒度,以及保证松耦合的必要性。文中提出,服务应该是自治且完全独立的,并尽量减少同步通信。今天,我们将讨论松耦合意味着什么,并探索一种在微服务社区中越来越受欢迎的"交易技巧"-事件驱动架构。

简单定义

事件驱动架构(EDA)是一个促进生产和消费事件的软件架构规范。

一个事件表示一个感兴趣的动作。通常,事件对应一个创建或修改某些实体状态的动作。例如,在电子商务应用程序中下订单是一个事件,分发一个已下单的产品也是一个事件。一个消费者提交一个对接收的产品的评论也是一个事件。

永远不会发生的事件

关于事件的奇特之处在于不会明确地将它们传达给可能关心它们的特定服务。事件"只会发生"。更为重要的是事件只会单纯地发生,与是否存在关心这些事件的服务无关。这听起来像是经常被引用的哲学思想:"如果一颗森林中的树,没有人听到它,那么它会发出声音吗?"。但这也是事件之所以强大的原因--事件会转换为一条对某些正在发生的事情的(自包含)记录,事件及其扩展程序(从根本上讲)与它们的处理程序是分离的。实际上,事件记录的生产者并不知道消费者是谁,甚至不知道是否存在消费者。

一条记录通常包含描述一个事件的信息。在之前的订单为例,其对应的事件的JSON描述如下:

{
  "orderId": "760b5301-295f-4fec-95f8-6b303a3b824a",
  "customerId": 28623823,
  "productId": 31334,
  "quantity": 1,
  "timestamp": "2021-02-09T11:12:17+0000"
}

Node:尽管记录和事件存在细微的差别,但它们经常可以互换,即术语"事件"通常指代一个事件的"记录",为了简化描述,本文中将自由地使用这两个术语。

上述是对一个订单的高度简化。发起订单(购物车服务)的应用并不知道谁(如何,以及为什么)处理该订单。生产者会保证潜在的消费者能够捕获处理事件所需的一切信息。也就是说,订单记录不一定严格包含实现订单所需的每个属性。例如,不一定会直接指定产品的尺寸,存放位置以及消费者的送货地址等信息,但可以解析通过捕获的订单记录中的ID间接获得这些信息。关系数据库中的外键概念也同样适用于事件。

通道传输的事件

如果生产者和消费者都互不感知对方,那么两者该如何通信?

答案是通过术语"记录"进行粘合。事件通常被持久化到一个众所周知的位置,称为日志(有时也会用到术语"账簿")。日志是底层的,只能在后续消费者可以访问的地方附加生产者保存的事件数据结构。brokers(位于生产者和消费者之间的持久化中间件)负责操作日志。一旦产生了一个事件,任何人都可以消费该事件。

当处理事件驱动系统时,我们经常会使用术语"流"来描述一个或多个日志接口。日志是物理上的概念(使用文件实现),一条流是逻辑上的概念,表示构成事件的一组无边界的记录,但记录要遵守某种特定的顺序。不同的流平台可能使用专有名称来指代一条流。Apache Kafka使用topics和partitions来描述流。

生产者、消费者和流的关系如下:

介绍基于事件的架构

Event-Driven Architecture Reference Model

回顾一下相关概念:

  • 事件是在离散时间点发生的感兴趣的动作:可能从外部对其进行观察和描述。
  • 事件持久化为记录:事件和记录尽管是相关的,但在技术上是不同的。一个事件表示事情的发生(如状态变更),本身是无形的。而一条记录是对该事件的精确描述。我们通常使用术语"事件"来指代其对应的记录。
  • 生产者是通过将相应的记录发布到流中来检测事件的接收器。(发布一条记录则表示发生了一个事件)
  • 流是持久化的有序的记录。它们通常由一个或多个基于磁盘的日志来进行持久化,当然,也可以使用数据库表、分布式共识协议,甚至是区块链式的分散账本来支持持久化。
  • Brokers 负责对流的访问,方便读写操作,处理消费者状态以及在流上执行各种"内务"。例如,一个broker可能在记录溢出时对流的内容进行截取。
  • 消费者读取流,然后对接收到的记录作出回应。消费者对事件的回应可能会伴随一些额外的操作。例如,一个消费者可能会在本地数据库中持久化一条表项(通过发布的"更新"事件来重构远端实体的状态)(即更新对远端实体的描述)。
  • 消费者和生产者可能会重叠。例如,对事件的回应方,也可能产生一个或多个派生的事件。

通过异步性和通用性进行解耦

为什么EDA能够大大降低耦合度?

对耦合的一种比较务实的定义是:一个组件受其他组件影响的程度。耦合存在于空间(组件在结构上相关联)和时间(时间会影响组件之间的关系程度)上。对于后者,一个比较好的例子是,一个服务同步调用其他服务的REST API。如果被调用的服务down,则该服务将无法继续处理(响应被阻塞)。如果两个服务必须同时运行,则二者之间会存在一定程度的临时耦合(temporal coupling)。如果两个服务高度依赖,则称之为强耦合,反之,则称为松耦合。

介绍基于事件的架构

Conceptual model of coupling

EDA采用两种方法来抑制耦合。

  • 回顾一下,事件是不能通信的,它们只会发生。发起事件的组件(通过发布记录)并不知道其他组件是否存在。因此,即使消费者不可用,生产者也不会停止工作---broker会暂时缓存事件,而不会对生产者施加反向压力。
  • broker对事件记录的持久化大大消除了时间观念。一个生产者可能会在T1时间发布一个事件,而一个消费者可能会在T2事件才会读取该事件,T1T2之间的间隔可能是毫秒级别的(所有组件正常)或小时级别的(如果某些消费者down或忙于其他事情)。

EDA并不是银弹,它没有一并消除耦合的概念(否则,系统中的组件将不再共同作用)。现在将关注点转移到broker上:为了让生产者和消费者有意义地进行解耦,它们必须依赖一个broker。这种方式增加了系统架构的复杂度,并引入了其他故障点。这也是为什么brokers必须是高性能且具有容错能力,否则,我们只是将一组问题换成另一组。

事件处理的方式

时间处理通常分为三种常用的方式。这些方式并不互斥,它们经常会同时存在于一个大型的事件驱动系统中。

离散事件处理

用于处理离散事件:例如在社交媒体平台上发布一个帖子。离散事件处理的特征在于出现的事件之间通常并无关联,可以独立处理。

事件流处理

用于处理一系列相关联的无边界事件流,事件的记录以某种顺序呈现,并携带一些与发生的事件有关的信息。例如,当一个业务实体发生联合变更时,消费者可能会按照生产者指定的顺序进行变更,并在本地数据库中保存一份该实体的副本。由于需要关注事件处理的顺序,因此不能离散地处理这类事件。消费者需要避免条件竞争,即多个消费者实例可能会同时修改数据库中的某条记录,进而由于乱序更新而导致数据不一致。

比较有名的流事件平台,如Kafka会依赖记录的key和partitions来保留更新顺序。Kafka同时也保证对一个实体的所有变更会被某个消费者处理,避免多个消费者并行处理事件而导致并发竞争。

复杂事件处理

复杂事件处理(CEP)是一种从一系列简单事件中得出或识别复杂事件的模式。例如监控一座建筑内的温度和延误感应器,便于推断是否发生了火情,并进行持续跟踪。单独的温度变化并不足以引发报警。更具意义的是温度峰值和变化率汇总而成的群体事件,进而有可能挽救生命。

通常更多会涉及此类处理,要求事件处理器持续跟踪先前的事件,并提供一个有效的方式进行请求和汇总。

什么时候使用EDA

一些场景下可以使用事件驱动架构带来的优势:

  • 不透明的消费者生态系统。这种情况下,生产者并不了解消费者,后者可能是一个短暂的过程,可能在短时间内来来往往!
  • 高扇出。一个事件可能由多个不同的消费者处理的场景。
  • 复杂的模式匹配。可能将事件串在一起来推断出更复杂的事件。(这类场景可能需要进行汇总,即上面描述的复杂事件处理)
  • 命令查询的责任分离。CQRS是一种分离数据存储区的读取和更新操作的模式。实现CQRS可以提高应用的可扩展性和弹性(在数据一致性上进行了取舍)。 这种模式通常与EDA相关。

EDA的好处

  1. 缓存和容错能力。事件消费的速率可能与生产者不同步,生产者不能为了与消费者保持一致而放慢速率。
  2. 生产者和消费者解耦,避免笨拙的点到点集成。EDA下很容易添加新的生产者和消费者,也很容易修改生产者和消费者的实现(前提是遵守约束事件记录的合同/方案)。
  3. 大规模扩展。通常会把部分部分事件流切分为若干不相关的自流,然后并行处理。随着事件的积压,我们也可以扩展消费者的数量来满足负载需求。像Kafka这样的平台会严格按序处理事件,并允许跨流进行大规模并行处理。

EDA的缺点

  1. 仅限异步处理。虽然EDA是一种有效的系统解耦模式,但它也将应用限制为异步事件处理。EDA并不能很好地处理像请求响应这样的交互(发起者必须等待响应才能继续处理)。
  2. 引入额外的复杂度。传统的客户端-服务器以及请求-响应仅会涉及两方,在采用EDA之后则引入了第三方-broker,作为生产者和消费者之间的媒介。
  3. 故障掩盖。这一点比较奇特,因为它似乎与解耦系统的本质背道而驰。当系统高度耦合时,一个系统中的错误会快速传递下去,并引起我们的关注。大多数场景下,我们需要避免这种情况:当一个组件失败时,尽量减小它对其他组件的影响。故障掩盖的负面影响是,它会在不经意间隐藏本应引起我们注意的问题。可以通过为每个事件驱动的组件添加实时监控和日志来解决,但这样做也带来了新的复杂度。

介绍基于事件的架构

需要注意的点

EDA不是万能药,与很多强大的工具一样,它有可能被错误地使用。下面列出的内容不应该被认为是EDA的缺点,而应该作为开发人员和架构师在设计和实现事件驱动的系统时应注意的一系列陷阱。

  1. 复杂的编排。使用松耦合组件,用户可能会感到困惑,整个架构看起来像是一个Rube Goldburg机器(可以借助下图理解Rube Goldburg),整个业务逻辑也被实现为一系列(带有副作用的包装的)事件:一个组件发起的事件可能触发另一个组件发起另一个事件,然后触发另一个组件发起事件,以此类推。这种组件间的交互很快会变得无法理解。

    介绍基于事件的架构

  2. 将命令和事件混淆。一个事件用于单纯地描述发生的事情。它不会指定如何处理事件。而一个命令是针对特定组件的直接指令。由于命令和事件都是某种类型的消息,非常容易混淆,把命令误以为是一个事件。

    命令也可以放到EDA下,但要分清与事件的区别。命令可能会修改系统状态,通常会需要回滚方案。

  3. 消费者不可知。事件应该以某种方式捕获相关的属性,但并不会限制如何处理这些事件。说起来容易,做起来难。有时我们可能会无法获得足够的信息来限制添加到事件记录的内容(无法确定这些添加到记录中的信息是否最终有用)。

个人认为最重要的是上面的第二点,要区分事件和命令。事件是已经发生的操作,而命令表示正在处理中的操作(未完成或失败)。消息队列通常用于处理命令,而kafka则被设计来处理事件,当然这类处理方式在分布式事务中称为MQ事务

总结

微服务架构模式是构建更可维护、可扩展、更健壮的软件系统所涉及的难题之一。从问题分解的角度来看,微服务非常棒,但也带来了很多棘手的问题,其中一个就是耦合。与一开始相比,随意将系统拆分为少数微服务的做法可能会使您处于更糟糕的局面。有一个术语可以对其进行描述"分布一体式"。

为了帮助解决困惑,并定位耦合的问题,我们引入了事件驱动架构。

EDA是一个可以帮助降低系统组件间的耦合的有效工具,它是一种使用生产者、消费者、事件和流进行交互的模型。一个事件表示一个感兴趣的动作,任何组件都可能异步地发布和消费事件,而无需感知对方的存在。EDA允许组件独立操作和演化。但它不是解决所有问题的银弹。EDA是一个不错的选择,它带来的好处大大超过了采用它的成本。可以说,EDA是成功部署微服务的必要要素。

本文主要描述了EDA中的事件的本质,以及对事件的处理逻辑,事件可以是离散的,也可以是有顺序的。除事件外EDA其实也可以处理命令和查询请求,但要针对各自的特性和业务逻辑进行针对性的处理。除此之外还应该注意到EDA的局限性,避免不合时宜地使用EDA。

在分布式环境中,一个比较难处理的问题是分布式事务,通常指数据库事务。分布式事务选型一般涉及2PC、3PC、TCC、SAGA、以及Seata。没有完美的分布式事务解决方案,它们不过是各自在性能、一致性、可用性等方面做取舍,寻求某些场景偏好下的权衡。

介绍基于事件的架构

参考:

发表评论

相关文章