事件驱动的体系结构由生成事件流、侦听这些事件的事件使用者以及将事件从生成者传输到使用者的事件通道组成。
事件可几乎实时发送,因此使用者可在事件发生时立即做出响应。 生成者脱离使用者,即生成者不知道哪个使用者正在倾听。 使用者之间也能彼此脱离,且每个使用者都能看到所有事件。 这与使用者竞争模式不同,在此模式中,使用者从队列中拉取消息,且消息仅处理一次(假设没有错误)。 在某些系统中(例如 IoT),必须大量引入事件。
事件驱动的体系结构可以使用发布/订阅(也称作 pub/sub)模式或事件流模式。
发布/订阅:消息传递基础结构会跟踪订阅。 事件发布后,它会将事件发送给每位订阅者。 事件在接收后,便无法重播,新订阅者也看不见此事件。
事件流式处理:事件会写入日志。 事件是(在分区中)经过严格排序的,而且具有持久性。 客户端不会订阅流,但是客户端可以读取该流的任何部分。 客户端负责提升它在流中的位置。 这意味着客户端可以随时加入,并可以重播事件。
在使用者端,有一些常见的变化:
简单事件处理。 事件会立即触发使用者中的某项操作。 例如,可以将 Azure Functions 与服务总线触发器配合使用,每当消息发布到服务总线主题后,函数便开始执行。
基本事件关联。 使用者需要处理少量离散的业务事件,这些事件一般通过某些标识符相关联,以保留早期事件中的某些信息供处理后续事件时使用。 NServiceBus 和 MassTransit 等库支持此模式。
复杂事件处理。 使用者使用 Azure 流分析之类的技术处理一系列事件,寻找事件数据中的模式。 例如,如果移动平均超过特定阈值,便可聚合在某个时间范围内从嵌入式设备读取的信息,并生成通知。
事件流处理。 使用 Azure IoT 中心或 Apache Kafka 等数据流平台作为管道引入事件并将其馈送到流处理器。 此流处理器可处理或转换流。 不同应用程序子系统可能有多种流处理器。 此方法非常适合 IoT 工作负荷。
事件可能来源于系统之外,例如 IoT 解决方案中的物理设备。 在这种情况下,系统必须能够以数据源需要的容量和吞吐量来引入数据。
构建事件有效负载有两种主要方法。 在控制事件使用者时,请根据使用者做出此有效负载结构决策;在单个工作负荷中根据需要混合方法。
在有效负载中包含所有必需的属性:如果希望使用者拥有所有可用信息,而无需查询外部数据源,将使用此方法。 但是,由于多个 记录系统(尤其是在更新后)导致数据一致性问题。 合同管理和版本控制也可能变得复杂。
在有效负载中仅包括键(s):在此方法中,使用者检索必要的属性(如主键),以便从数据源中独立提取剩余数据。 虽然此方法由于单个记录系统提供了更好的数据一致性,但它的性能可能比第一种方法要差,因为使用者必须经常查询数据源。 与耦合、带宽、合同管理或版本控制相关的问题较少,因为事件更小,合同更简单。
在上面的逻辑图中,每种类型的使用者都显示为单个框。 实际情况中通常有多个使用者实例,可避免使用者成为系统中的单点故障。 处理事件的容量和频率可能还需要多个实例。 此外,单个使用者可以处理多个线程上的事件。 如果必须按照顺序处理事件,或者需要“恰一次”语义,这就会带来一些挑战。 请参阅尽量减少协调。
许多事件驱动体系结构中有两个主要拓扑:
代理拓扑。 组件将事件广播为整个系统的事件,而其他组件要么对事件执行操作,要么忽略该事件。 当事件处理流相对简单时,此拓扑非常有用。 没有集中协调或业务流程,因此此拓扑可能非常动态。 此拓扑高度分离,这有助于提供可伸缩性、响应能力和组件容错能力。 没有任何组件拥有或了解任何多步骤业务交易的状态,并且操作是异步执行的。 随后,分布式事务存在风险,因为没有本机方法可以重启或重播。 需要仔细考虑错误处理和手动干预策略,因为此拓扑可能是数据不一致的来源。
中介器拓扑。 此拓扑解决了代理拓扑的一些缺点。 有一个事件中介器可以管理和控制事件流。 事件中介器维护状态并管理错误处理和重启功能。 与中介器拓扑不同,组件将事件广播为命令,并且仅广播到指定的通道,通常是消息队列。 这些命令不应被使用者忽略。 此拓扑可提供更强的控制,更好的分布式错误处理,以及可能更好的数据一致性。 此拓扑确实会增加组件之间的耦合,事件中介器可能会成为瓶颈或可靠性问题。
何时使用此架构
- 多个子系统必须处理相同的事件。
- 延迟时间最短的实时处理。
- 复杂事件处理,如模式匹配或时间范围内的聚合。
- 大量且快速的数据,如 IoT。
好处
- 生成者和使用者相脱离。
- 没有点到点的集成。 容易向系统添加新使用者。
- 使用者在事件发生时便可立即响应。
- 高度可缩放、弹性和分布式。
- 子系统具有独立的事件流视图。
挑战
有保证的传递。
在某些系统中,尤其是在 IoT 方案中,保证数据传递至关重要。
按顺序或者“恰一次”处理事件。
每种使用者类型通常都在多个实例中运行,以提供复原能力和可伸缩性。 如果必须按顺序(在使用者类型中)处理事件,或未实现幂等消息处理逻辑,处理起来就会比较困难。
跨服务协调消息。
业务流程通常涉及多个服务发布和订阅消息,以实现整个工作负载的一致效果。 工作流模式(如协调模式和 Saga 业务流程)可用于可靠管理各种服务中的消息流。
错误处理。
事件驱动的体系结构主要使用异步通信。 异步通信的一个挑战是错误处理。 解决此问题的一种方法是使用单独的错误处理程序处理器。 因此,当事件使用者遇到错误时,它会立即以异步方式将错误事件发送到错误处理程序处理器,然后继续操作。 错误处理程序处理器尝试修复错误,并将事件发送回原始引入通道。 但是,如果错误处理程序处理器发生故障,那么它可以将错误事件发送给管理员进行进一步检查。 如果使用错误处理程序处理器,则错误事件在重新提交时将按顺序处理。
数据丢失。
异步通信的另一个挑战是数据丢失。 如果任何组件在成功处理之前崩溃,并将事件移交给其下一个组件,则会删除该事件,并且永远不会使其进入最终目标。 为了最大程度地减少数据丢失的可能性,请持久保存传输中的事件,并仅在下一个组件确认收到事件时删除或取消排队事件。 这些功能通常称为 客户端确认模式 和 最后一个参与者支持。
实现传统的请求-响应模式。
有时,事件生成者需要事件使用者的即时响应,例如在继续订单之前获取客户资格。 在事件驱动的体系结构中,可以通过请求-响应消息传送实现同步通信。
此模式通常通过利用多个队列(请求队列和响应队列)来实现。 事件生成者向请求队列发送异步请求,暂停该任务的其他操作,并在回复队列中等待响应;有效地将此转换为同步过程。 然后,事件使用者处理请求并通过响应队列发送回复。 此方法通常使用会话 ID 进行跟踪,因此事件生成者知道响应队列中的哪个消息与特定请求相关。 原始请求还可以在回复标头或其他相互同意的自定义属性中指定响应队列的名称(可能是临时的)。
维护适当的事件数。
生成过多的细粒度事件会使系统饱和并压倒系统,从而难以有效地分析事件的整体流。 需要回滚更改时,此问题会加剧。 相反,过度合并事件也会造成问题,导致事件使用者不必要的处理和响应。
若要实现正确的平衡,请考虑事件的后果以及使用者是否需要检查事件有效负载以确定其响应。 例如,如果你有符合性检查组件,则仅发布两种类型的事件可能就足够了: 合规 和非 合规。 此方法仅允许相关使用者处理每个事件,从而防止不必要的处理。
其他注意事项
- 要包含在事件中的数据量可能是影响性能和成本的重要考虑因素。 在事件本身中放置处理所需的所有相关信息可以简化处理代码并减少额外查找。 将极少量的信息(例如仅仅几个标识符)置于事件中将减少传输时间和成本,但需要处理代码来查找所需的任何其他信息。 有关此内容的详细信息,请查看此博客文章。
- 虽然请求仅对请求处理组件可见,但事件通常对工作负荷中的多个组件可见,即使这些组件不使用也不打算使用它们。 以“假设违规”思维模式进行操作,请注意事件中包含的信息,以防止意外的信息泄露。
- 许多应用程序使用事件驱动的体系结构作为其主要体系结构;但是,此方法可以与其他体系结构样式相结合,从而产生混合体系结构。 常见组合包括 微服务 和 管道和筛选器。 集成事件驱动的体系结构可消除瓶颈并在高请求量期间提供 后台压力 ,从而提高系统性能。
- 特定域 通常跨越多个事件生成者、使用者或事件通道。 对特定域的更改可能会影响许多组件。
相关资源
- 社区讨论视频,介绍在协调和业务流程之间进行选择时需考虑的事项。