使用 Pub/Sub 的事件驱动型架构

本文档讨论本地消息队列驱动型架构与 Pub/Sub 上实现的基于云的事件驱动型架构之间的差异。如果试图将本地模式直接应用于基于云的技术,就可能会导致您错过独特的价值,但就是这种独特的价值让云极具吸引力。

本文档适用于将设计从本地架构迁移到基于云的设计的系统架构师。本文档假设您对消息传递系统有初步了解。

下图展示了消息队列模型和 Pub/Sub 模型的概览。

将消息队列模型的架构与使用 Pub/Sub 的事件驱动模型进行比较。

在上图中,消息队列模型与 Pub/Sub 事件流模型进行了比较。在消息队列模型中,发布者将消息推送到队列,每个订阅者可以侦听特定队列。在使用 Pub/Sub 的事件流模型中,发布者将消息推送到多个订阅者可以侦听的主题。以下各部分介绍了这些模型之间的差异。

比较事件流和基于队列的消息传递

如果您使用本地系统,则表示您已熟悉企业服务总线 (ESB)消息队列。事件流是一种新的模式,对于现代实时系统来说,有着重要的区别和明显的优势。

本文档介绍传输机制和事件驱动型架构中的载荷数据之间的主要区别。

消息传输

在这些模型中迁移数据的系统称为消息代理,并在这些框架中实现多种框架。第一种概念之一是底层机制,可将来自发布者的消息传输到接收者。在本地消息框架中,原始系统通过使用消息队列作为传输工具,将显式的远程分离消息发送到下游处理系统。

下图展示了消息队列模型:

来自发布者的消息将推送到每个订阅者的唯一队列。

在上图中,消息使用消息队列从上游发布者进程流向下游订阅者进程。

系统 A(发布者)将消息发送至为系统 B(订阅者)指定的消息代理上的队列。虽然队列的订阅者可以由多个客户端组成,但所有这些客户端都是系统 B 的重复实例,这些实例是为实现伸缩和可用性而部署。如果其他下游进程(例如,系统 C)需要使用来自提供方的相同消息(系统 A),则需要一个新队列。您需要更新提供方,以将消息发布到新队列。此模型通常称为消息传递

这些队列的消息传输层不一定提供消息顺序保证。通常,消息队列应提供一个顺序保证模型,其中数据以严格的先进先出 (FIFO) 访问模型排序,类似于任务队列。此模式最初易于实现,但最终会带来扩缩和运维方面的挑战。为了实现有序消息,系统需要一个中央流程来组织数据。此过程限制了扩缩能力并降低了服务可用性,因为它是单点故障。

这些架构中的消息传递代理倾向于实现其他逻辑,例如跟踪哪个订阅者接收了哪些消息以及监控订阅者负载。订阅者往往只是被动反应,不了解整个系统,只是在接收消息后运行某一功能。这些类型的架构被称为智能管道(消息队列系统)和哑端点(订阅者)。

Pub/Sub 传输

与面向消息的系统类似,事件流处理系统还将消息从源系统传输到分离的目标系统。然而,基于事件的系统倾向于将消息发布到共享主题,然后一个或多个接收者订阅该主题以侦听相关消息,而不是将每条消息发送到以进程为目标的队列。

下图显示了上游发布者如何将各种消息发送到单个主题,然后路由到相关的下游订阅者:

来自发布者的消息将推送到所有订阅者的单个主题。

这种发布和订阅模式是术语 pub/sub 的来源。此模式也是名为 Pub/Sub 的 Google Cloud 产品的基础。在本文档中,pubsub 是指模式,而 Pub/Sub 是指产品。

在 pubsub 模型中,消息传递系统不需要了解任何订阅者。它不会跟踪收到的消息,也不会管理使用进程中的负载。相反,订阅者会跟踪已收到的消息,并负责自我管理负载级别和扩缩。

这样做的一个好处是,当您遇到 pubsub 模型中数据的新用途时,无需更新源系统即可发布到新队列或复制数据。相反,您将新使用方关联到新订阅,而不会对现有系统产生任何影响。

事件流系统中的调用几乎总是异步的,它们发送事件而不等待任何响应。异步事件允许为提供方和使用方提供更大的扩缩选项。但是,如果您期望 FIFO 消息顺序保证,则此异步模式可能会带来挑战。

消息队列数据

在消息队列系统和基于 pubsub 的系统中的系统之间传递的数据在这两种上下文中通常称为“消息”。但是,呈现该数据的模型是不同的。在消息队列系统中,消息反映了一个旨在改变下游数据状态的命令。如果您查看本地消息队列系统的数据,发布者可能会明确说明使用方应该做什么。例如,清单消息可能指示以下内容:

<m:SetInventoryLevel>
    <inventoryValue>3001</inventoryValue>
</m: SetInventoryLevel>

在此示例中,提供方会告知使用方需要将清单级别设置为 3001。这种方法可能具有挑战性,因为提供方需要了解每个使用方的业务逻辑,并且需要为不同的使用场景创建单独的消息结构。这种消息队列系统是大多数企业实施的大型单体式应用的常见做法。但是,如果您希望以更快的速度、扩大规模并推动创新,这些集中式系统可能会成为瓶颈,因为更改存在风险且速度较慢。

这种模式也存在操作上的挑战。当出现错误数据、重复记录或其他问题并且需要更正时,此消息传递模型会带来重大挑战。例如,如果您需要回滚上述示例中使用的消息,您不知道将更正后的值设置为什么,因为您没有引用先前状态。在发送该消息之前,您不知道清单值是 3000 还是 4000。

Pubsub 数据

事件是另一种发送消息数据的方式。唯一的特点是事件驱动型系统专注于发生的事件,而不是本应发生的结果。不是发送指示使用方应该采取什么操作的数据,数据侧重于实际产生的事件细节。您可以在各种平台上实现事件驱动型系统,但它们经常出现在基于 pubsub 的系统上。

例如,清单事件可能如下所示:

{ "inventory":-1 }

上一个事件数据表明发生了事件,将清单减少了 1。这些消息侧重于过去发生的事件,而不是将来要更改的状态。发布者能够以异步方式发送消息,从而使事件驱动系统比消息队列模型更容易扩缩。在 pubsub 模型中,您可以分离业务逻辑,使提供方只需了解对其执行的操作,而无需了解下游进程。该数据的订阅者可以选择如何最好地处理他们收到的数据。因为这些消息不是命令式命令,所以消息的顺序变得不那么重要。

使用此模式,可以更轻松地回滚更改。在此示例中,无需其他信息,因为您可以对清单值求反,从而往相反的方向移动它。迟到或无序的消息不再是问题。

模型对比

在这种情况下,您的清单中有四件相同产品的商品。一位客户退回了一件产品,下一位客户购买了三件相同的产品。对于这种情况,假设退回产品的消息有延迟。

下表比较了以正确顺序接收清单计数的消息队列模型与接收无序清单计数的相同模型的清单级别:

消息队列(顺序正确) 消息队列(顺序错乱)
初始清单:4 初始清单:4
消息 1:setInventory(5) 消息 2:setInventory(2)
消息 2:setInventory(2) 消息 1:setInventory(5)
清单级别: 2 清单级别:5

在消息队列模型中,接收消息的顺序很重要,因为消息包含预先计算的值。在此示例中,如果消息以正确的顺序到达,则清单级别为 2。但是,如果消息无序到达,则清单级别为 5,这是不准确的。

下表比较了基于 Pub/Sub 系统按正确顺序接收清单计数的清单级别与同一顺序从接收清单计数开始的系统。

Pubsub(顺序正确) Pubsub(顺序错乱)
初始清单:4 初始清单:4
消息 2:"inventory":-3 消息 1:"inventory":+1
消息 1:"inventory":+1 消息 2:"inventory":-3
清单级别:2 清单级别:2

在基于 pubsub 的系统中,消息的顺序无关紧要,因为它会由生成事件的服务通知。无论消息到达的顺序如何,清单级别都是准确的。

下图展示了消息队列模型中的队列如何执行命令来告诉订阅者在 pubsub 模型中应如何改变状态,而订阅者会响应事件数据,陈述发布者中所发生的情况:

比较响应命令与响应事件的结账示例。

实现事件驱动型架构

在实现事件驱动型架构时,需要考虑各种概念。以下部分介绍了其中一些主题。

送达保证

系统讨论中出现的一个概念是消息传递保证的可靠性。不同的供应商和系统可能提供不同级别的可靠性,因此了解这些变化很重要。

第一种类型的保证会提出一个简单问题:如果发送了消息,是否保证会送达?这称为“至少传送一次”。消息保证至少传送一次,但它可能发送多次。

不同类型的保证是“最多传送一次”。在“最多传送一次”的情况下,消息最多仅传送一次,但不能保证消息实际会传送。

传送保证的最终变体是一次性传送。在此模型中,系统发送一份且仅一份消息副本保证送达。

顺序和复制

在本地架构中,消息通常遵循 FIFO 模型。为了实现此模型,集中式处理系统管理消息的排序,以确保准确排序。有序消息传递会带来挑战,因为对于任何失败的消息,所有消息都需要按顺序重新发送。任何集中式系统都可能成为可用性和可扩缩性的挑战。通常,只能通过向现有机器添加更多资源,对管理排序的中央系统进行扩缩。使用单个系统管理排序,任何可靠性问题都会影响整个系统,而不仅仅是那台机器。

高可扩缩性和可用的消息传递服务通常使用多个处理系统来确保消息至少传送一次。许多系统都无法保证管理消息顺序。

事件驱动型架构不依赖于消息顺序,但可以容忍重复的消息。如果需要排序,子系统可以实现聚合和窗口技术;然而,这种方法牺牲了该组件的可扩缩性和可用性。

过滤和扇出技术

由于事件流可能包含每个订阅者不一定需要的数据,因此通常需要限制给定订阅者接收的数据。管理此要求有两种模式:事件过滤器和事件扇出。

下图显示了事件驱动型系统,其中包含用于过滤订阅者的消息的事件过滤条件:

带有事件过滤条件的事件驱动型模型,可将消息过滤给订阅者。

在上图中,事件过滤条件使用过滤机制来限制到达订阅者的事件。在此模型中,单个主题包含消息的所有变体。消息传递系统中的过滤逻辑不是让订阅者阅读每条消息并验证它是否适用,而是评估消息,将其与其他订阅者隔开。

下图显示了事件过滤器模式的一种变体,称为事件扇出,它使用多个主题:

带有事件扇出的事件驱动模型,可重新发布有关主题的消息。

在上图中,主要主题包含消息的所有变体,但事件扇出机制会重新发布与那部分订阅者相关的主题的消息。

未处理的消息队列

即使在最佳系统中,也可能出现故障。未处理的消息队列是一种处理此类故障的技术。在大多数事件驱动型架构中,消息系统会继续向订阅者提供消息,直到订阅者确认消息。

如果消息存在问题(例如消息正文中的无效字符),订阅者可能无法确认消息。系统可能无法处理该场景,甚至可能终止该进程。

系统通常会重试未确认或错误的消息。如果无效消息在预定的时间段后未确认,则消息最终将超时并从主题中丢弃。从运维角度来看,查看消息而不是任其消失会很有用。这就是未处理消息队列的用武之地。不是从主题中删除消息,而是将消息移动到另一个主题,在那里可以重新处理或查看消息,以了解消息出现错误的原因。

直播记录和重放

事件流是连续的数据流。访问这些历史数据会非常有用。您可能想知道系统如何达到某种状态。您可能会遇到需要审核数据的安全相关问题。能够捕获事件的历史日志对于事件驱动型系统的长期操作至关重要。

历史事件数据的一种常见用途是与重放系统一起使用。重放用于测试目的。通过在其他环境(如阶段和测试)中重放生产中的事件数据,您可以针对真实数据集验证新功能。您还可以重放历史数据,以从失败状态中恢复。如果系统发生故障或丢失数据,团队可以从已知良好的时间点重放事件历史记录,并且服务可以重建其丢失的状态。

当订阅者需要在不同时间访问一系列事件时,在基于日志的队列或日志记录流中捕获这些事件也很有用。日志记录流可以在具有离线功能的系统中看到。通过使用您的流历史记录,您可以通过读取从“上次读取”指针开始的流来处理最新的新条目。

数据视图:实时和近乎实时

由于所有数据都流经系统,因此您能够使用数据就非常重要。访问和使用这些事件流的方法有很多,但常见的使用场景是了解特定时刻的数据的整体状态。这些通常是计算相关问题,例如可供其他系统使用或供人类使用的“数量”或“当前级别”。有多种实现方式可以回答以下问题:

  • 实时系统可以持续运行并跟踪当前状态;但是,如果系统仅具有内存中计算,任何停机时间都会将计算设置为零。
  • 系统可以从历史表中为每个请求计算值,但这可能会成为一个问题,因为尝试在数据增长时为每个请求计算值最终变得不可行。
  • 系统可以按特定时间间隔创建计算快照,但仅使用快照并不能反映实时数据。

一种有用的实现模式是具有近乎实时和实时功能的 Lambda 架构。例如,电子商务网站上的产品页面可以使用近乎实时的清单数据视图。当客户下订单时,实时服务用于确保清单数据的最新状态更新。为了实现此模式,服务会响应来自快照表的近乎实时的请求,此快照表包含给定间隔的计算值。实时请求同时使用快照表和自上次快照以来记录表中的值,以获取确切的当前状态。事件流的这些具体化视图提供了可操作的数据,以推动实际业务流程。

后续步骤