你好,我是看山。
本文源自并发编程网的翻译邀请,翻译的是 Jakob Jenkov 的 《软件架构》 中关于事件驱动的内容,虽然是 2014 年的文章,但是从软件架构层面上,并不过时。
以下是正文。
事件驱动架构是一种系统或组件之间通过发送事件和响应事件彼此交互的架构风格。当某个事件发生时,组件A不直接调用组件B,而只是发出一个事件。组件A不知道哪些组件监听并处理这些事件。事件驱动架构可以在进程内和进程间使用。比如,GUI框架中会大量使用事件驱动。【译者注:目前很多系统采用微服务架构,事件驱动使用的更加广泛了。】此外,正如我在并发模型教程 中所提到的,装配线并发模型(AKA reactive,非阻塞并发模型)也使用了事件驱动架构。
本文主要介绍进程之间的事件驱动架构,后文提到这个词的时候也是指进程交互方式。
进程间的事件驱动架构
事件驱动架构是一种架构风格,先将请求事件集中存放在一个或多个事件队列中,然后事件从这些事件队列转发到后端服务,处理这些事件。
因为事件可以被看做是消息流,所以事件驱动架构也被称为消息驱动架构或者流处理架构。流处理架构又可以被称为lambda架构。为了保证统一,后文会继续使用事件驱动这个名词。
事件队列
在事件驱动架构中,你会有一个或多个集中的事件队列,所有的事件被处理前,会先保存在集中的事件队列中。下面给出一个简单示例:
事件插入队列时是有序的,这样就可以顺序处理这些事件。
事件日志
写入事件队列时,消息可能写入到事件日志(通常是磁盘存储)中。如果发生系统崩溃,系统只需要重放事件日志即可恢复到崩溃前的状态。下面是一个事件驱动架构的示例,其中包括一个用于持久化事件的事件日志:
我们还可以通过备份事件日志,来备份系统状态。在将新版本的系统部署在生产环境之前,可以使用这个备份数据对其性能进行测试。或者,通过重放事件日志的备份,来重现某些错误。
事件收集器
请求都是通过网络传输,比如HTTP或者其他协议。为了保持一致,可以通过事件采集器接收来自不同来源的事件。下面是一个添加了事件收集器的事件驱动架构示例:
响应队列
有时,我们还需要向请求(即事件)返回响应,所以,很多事件驱动架构除了包含事件队列,还会有一个响应队列。下面是包含事件队列(入队队列)和响应队列(出队队列)的事件驱动架构示例:
如你所见,响应队列必须路由到正确的事件收集器。比如,如果HTTP收集器(本质上是web服务器)通过HTTP接收的请求发送到事件队列中,则该事件生成的响应可能也需要通过HTTP收集器发回客户端。
通常,响应队列不会持久化,也就意味着它不会写入事件日志,只有输入的事件才会持久化到事件日志中。
读事件 vs. 写事件
如果将所有传入的请求都认为是事件,就需要将这些事件都推送到事件队列中。如果事件队列是实现了持久化(持久化到事件日志中),就意味着所有事件都需要持久化。通常持久化都比较慢,如果我们能够过滤掉一些不需要持久化的事件,我们就能够提升队列的性能。
我们将事件持久化到事件日志的原因是,我们可以重放事件日志,并重建因为事件引起的系统状态变化。为了支持这个特性,实际上只需要持久化更改系统状态的事件。换句话说,我们只需要将事件分为读事件和写事件。读事件只读取系统数据,不会更改,写事件会更改系统数据。
通过根据读和写划分事件,我们只需要持久化写事件的消息即可。这将提升事件队列的性能,提升比例大小,取决于读写事件之间的比例。
为了将事件划分为读写事件,需要在事件到达事件队列之前,也就是事件收集器中进行区分。否则,事件队列无法知道到达的事件是否需要持久化。
还可以将事件队列拆分为两个,一个用于存储读事件的事件队列,一个用于存储写事件的事件队列。这样读事件就不会慢于写事件,事件队列也不需要检查每条事件是否需要持久化。读事件队列不需要进行持久化,写事件队列始终持久化事件。
下面是一个事件驱动架构的示例,其中事件队列分为读和写事件队列:
上图示例中箭头比较乱,但实际上创建3个丢列并在它们之间分发消息简单很多。
事件日志重放的挑战
事件驱动架构的一大优点是,在系统崩溃或系统重启情况下,只需要重放事件日志,就能够重建系统状态。在日志可以独立于时间和周边系统的情况下重放日志,这是一个很大的优势。
但是,完全独立于时间重放事件日志有时候很难实现。接下来介绍下事件日志重放的一些挑战。
处理动态数据
如前所述,写事件处理时可能会修改系统数据。有些情况,这种数据的修改受事件处理时动态数据的影响。比如,处理事件的日期和时间或者特定日期和时间的货币汇率。
这些动态数据会对事件重放造成困难。如果在不同的时间重放事件日志,处理该事件的服务可能会解析不同的动态值,比如其他的日期和时间或其他汇率。因此,在不同的日期重放事件日志,可能会出现重建系统数据与最初处理事件产生的数据不一致。
要解决动态数据的问题,可以让写事件队列将所需的动态数据标记在事件中。但是,要实现这种方案,需要事件队列知道每条事件消息需要哪些动态数据。这样会使事件队列的设计复杂化,每次需要新的动态数据时,事件队列都需要知道如何查找这些动态数据。
另外一种解决方案是,写事件队列只在写事件上标记事件的日期和时间。使用事件的原始日期和时间,处理事件的服务可以查找给定日期和时间对应的动态数据。比如,可以通过原始的日期和时间,查询当时有效的汇率。这就要求处理事件的服务需要基于日期和时间查询动态数据,但是这只是理想状态。
与外部系统的交互
事件日志重放的另一个挑战是与外部系统的协调。比如,事件日志中包含电商平台的订单,在第一次处理这个事件时,需要将订单发送到外部支付网关,以从客户信用卡中收费。
如果重放事件日志,就不希望再次为同一个订单向客户收费。因此,就不希望在事件重放时,将订单发送到外部支付网关。
事件日志重放解决方案
解决重放事件日志问题挺不容易的。有些系统没有问题,可以直接重放事件日志;有些系统可能需要知道原始事件的日期和时间;有些系统可能需要知道更多类似于事件原始处理过程中从外部系统获取的原始数据。
重放模式
在任何情况下,倾听写事件队列中事件的任何服务都必须知道传入事件是原始事件还是重放事件。这样,处理服务就能够确定如何处理动态数据或者如何与外部系统交互了。
多步骤事件队列
另外一个解决方案是采用多步骤事件队列。第一步,收集所有写事件;第二步,解析动态数据;第三步,与外部系统交互。如果需要重放事件日志,只需要跳过第一步和第二步,重放第三步即可。具体如何实现,需要取决于具体的系统设计。