本系列文章通过逐章回答《Fundamentals of Software Architecture》(下文简称 FOSA)一书中的课后思考题,来深入理解书中的核心概念和理论,从而提升我们的软件架构设计能力。本篇为第十四章内容。

本章的课后题是:

  1. What are the primary differences between the broker and mediator topologies?

    代理拓扑(broker)和中介者拓扑(mediator)两种拓扑的根本区别是什么?

  2. For better workflow control, would you use the mediator or broker topology?

    为了更好的流程控制,你会选择代理拓扑还是中介者拓扑?

  3. Does the broker topology usually leverage a publish-and-subscribe model with topics or a point-to-point model with queues?

    在代理拓扑中,是经常使用基于主题的发布订阅模式还是基于队列的点到点模式?

  4. Name two primary advantage of asynchronous communications.

    列出 2 个异步通信的主要优势。

  5. Give an example of a typical request within the request-based model.

    举一个 request-based 模式的典型例子。

  6. Give an example of a typical request in an event-based model.

    举一个 event-based 模式的典型例子。

  7. What is the difference between an initiating event and a processing event in event-driven architecture?

    在事件驱动架构中,初始事件和处理中事件二者有什么不同?

  8. What are some of the techniques for preventing data loss when sending and receiving messages from a queue?

    有哪些技术可以防止在从队列发送和接收消息时丢失数据?

  9. What are three main driving architecture characteristics for using event-driven architecture?

    使用事件驱动架构的三个主要驱动架构特性是什么?

  10. What are some of the architecture characteristics that are not well supported in event-driven architecture?

    事件驱动架构不能很好地支持哪些架构特性?


传统的软件设计如同一个等级森严的组织,组件 A 直接向组件 B 下达命令(例如,调用一个函数或 API)。而事件驱动架构则更像一个现代化的、扁平的协作网络。组件 A 只是发布一个事实(嘿,我这里发生了一件事!),而其他对此事感兴趣的组件(B, C, D...)可以自行决定如何响应。这种从命令到响应的范式革命,是事件驱动架构(Event-Driven Architecture, EDA)的灵魂所在。

异步通信

EDA 的力量源泉来自于异步通信,它有以下优点:

  1. 极高的系统韧性与可用性 (Resiliency and Availability):在同步调用中,如果服务 B 宕机,服务 A 的调用会立刻失败,导致整个链路中断。但在异步模式下,服务 A 将事件发送给一个中间人(消息代理),然后就可继续自己的工作。即使服务 B 此时宕机,事件也会被安全地存放在代理中,待 B 恢复后再进行处理。这使得系统能够优雅地处理局部故障,整体可用性大大提高。
  2. 卓越的可伸缩性与弹性 (Scalability and Elasticity):生产者和消费者被完全解耦,可以独立进行伸缩。如果事件产生的速度突然加快,我们只需要增加消费者实例的数量即可,而无需对生产者做任何改动。这种按需、独立伸缩的能力是构建高弹性系统的关键。

拓扑

典型的 EDA 有 2 种拓扑,分别为:

  • 代理拓扑(broker)
  • 中介者拓扑(mediator)

broker

FOSA Figure 14-2. Broker topology

一个典型的 broker 拓扑如上图所示,它包含以下几个部分:

  • initiating event:初始事件,它用于启动整个事件流,一般来源于系统外部。
  • event channel:事件通道,用于传递事件,比如 Go 的 channel,或者分布式系统中的消息队列,如 RabbitMQ、Kafka 等。一个事件通道一般对应一个订阅主题(topic)。
  • event processor:事件处理器,它们会根据需求,订阅自己感兴趣的 topic,从 event channel 中获取事件进行处理。
  • processing event:处理事件,是由事件处理器生成并异步广播的事件,用于广告它刚刚完成了什么操作。这些事件是事件流的中间步骤,通知其他事件处理器某个操作已经完成,以便它们可以继续后续的处理。无论是否有其他的 event processor 关心这些事件,最佳实践中还是建议一直发布这些事件,这对于后续的扩展性非常良好。

它具有以下特点:

  • 核心思想:它的唯一职责就是高效、可靠地分发事件。所有的业务逻辑和处理步骤都存在于各个独立的事件处理器(服务)中。
  • 工作流:工作流是分散且隐式的。一个事件可能被多个消费者同时处理,触发多个并行的、互不相关的后续流程。
  • 通信模型:利用基于主题的发布/订阅(Publish-Subscribe)模型。一个事件被发布到特定主题(Topic)上,所有订阅了该主题的消费者都能收到一份该事件的副本并进行处理。这使得系统具有极强的扩展性,可以随时增加新的订阅者来响应现有事件,而无需修改任何已有代码。
  • 优点:事件生产者和事件消费者之间是完全解耦的。生产者不知道谁会消费它的事件,消费者也不知道是谁生产了它所消费的事件。它们唯一的共同依赖是消息代理以及事件的契约(Schema)
  • 缺点:端到端的工作流是隐式的,缺乏全局视图。如果流程出了问题,很难追踪到底是哪个环节的协同出了错,这对于异常处理和数据一致性要求较高的系统不是很友好。

完整例子可参考下图:

FOSA Figure 14-4. Example of the broker topology

mediator

FOSA Figure 14-5. Mediator topology

一个典型的 mediator 拓扑如上图所示,它跟 broker 有些许不同:

  • event queue:事件队列,它跟 event channel 有所不同,专门用于 event mediator 接收 initiating event
  • event mediator:事件中介者,了解处理事件所需的步骤,并生成相应的处理事件,这些事件被发送到专用事件通道(event channel),采用点对点消息传递方式。在一些复杂的场景中,也可以设置多个 event mediator,并分配到不同的层次中,以更好的管理复杂业务流程。

它具有以下特点:

  • 核心思想:它像一个流程编排引擎,包含了实现复杂业务流程的核心逻辑。
  • 工作流:工作流是集中且显式的。中介者接收一个初始事件,然后根据预设的逻辑,一步步地调用不同的服务来完成一个完整的、有状态的业务流程。
  • 通信模型:利用基于队列的点对点(Point-to-Point)模型
  • 优点:工作流是显式的,易于理解、监控和管理。复杂的错误处理、重试、补偿逻辑都可以在中介者中集中处理。
  • 缺点:中介者本身可能成为一个复杂的单点(但通常是高可用的集群),所有流程的修改都必须在其中进行,降低了系统的灵活性。

完整例子可参考下图:

FOSA Figure 14-9. Step 2 of the mediator example

对比

对比维度 代理拓扑 (Broker Topology) 中介者拓扑 (Mediator Topology)
核心组件 轻量级、无状态的消息代理 重量级、有状态的流程中介者
智能位置 分散在各个事件处理器中 集中在中介者中
工作流 协同式 (Choreography),隐式,涌现式 编排式 (Orchestration),显式,集中式
流程控制 弱,难以进行全局控制 强,易于进行精细控制和监控
耦合模型 极致解耦(仅依赖代理和事件契约) 轮轴式耦合(所有服务都依赖中-介者)
灵活性 极高,易于增加新的事件响应者 较低,流程变更需修改中介者
典型技术 消息队列、流平台 (Kafka, RabbitMQ) 工作流引擎、ESB (AWS Step Functions, Camel)
适用场景 简单通知、数据广播、高度可扩展的系统 复杂、多步、有状态的业务流程,Saga 模式

Request-Reply

  1. Give an example of a typical request within the request-based model.

    举一个 request-based 模式的典型例子。

  2. Give an example of a typical request in an event-based model.

    举一个 event-based 模式的典型例子。

request-based vs event-based

对比维度 基于请求的模型 (Request-Based) 基于事件的模型 (Event-Based)
核心意图 命令 (Command) 通知 (Notification / Fact)
详细说明 请求方必须知道接收方的确切地址和接口(例如,一个 URL 端点和其 API 契约)。它们之间是点对点的、强依赖的关系。 发布方和消费方互相完全不知道对方的存在。它们唯一的共同依赖是消息中间件和事件的格式。这种解耦是其最大优势。
通信模式 通常是同步的 (Synchronous) 总是异步的 (Asynchronous)
详细说明 请求方发送请求后,会阻塞并等待一个响应。从请求方的视角看,整个调用是一个连续、不间断的操作。 发布方发送事件后,立即继续自己的工作(“发后即忘” Fire-and-Forget)。它不等待任何结果。
例子 打电话 发布社交动态

event-based 实现 reply

虽然事件驱动架构的核心是异步和解耦,但在很多业务场景中,请求方确实需要得到一个明确的回复。例如,一个 Web 前端请求处理一个复杂的计算,它不能永远等待,而是需要在一个合理的时间内得到计算结果。

在事件模型之上实现请求-响应模式,关键在于解决两个核心问题:

  1. 响应应该发往何处? (因为接收方并不知道请求方是谁)
  2. 收到的响应如何与当初的请求对应起来? (因为请求方可能同时发出了多个请求)

解决方案是巧妙地利用消息的两个元数据字段:回复地址 (Reply-To)关联标识 (Correlation ID)

FOSA Figure 14-20. Request-reply message processing using a correlation ID

Step 1: 请求方 (Requester) 发起请求

  1. 创建临时回复队列:请求方首先为自己创建一个唯一的、临时的、专用于接收本次响应的队列。这个队列的生命周期通常与本次请求-响应过程绑定。
  2. 生成关联 ID:请求方生成一个全局唯一的字符串,作为 Correlation ID
  3. 构造请求消息:请求方创建请求消息,其内容是业务数据。在消息的属性(Properties)或头信息(Headers)中,设置两个关键字段:
    • Reply-To: 填入刚才创建的临时回复队列的名称。
    • Correlation ID: 填入刚才生成的唯一 ID。
  4. 发送并等待:请求方将这个构造好的消息发送到一个众所周知的请求队列(例如 calculation-request-queue)。然后,它开始监听自己的那个临时回复队列,等待一个包含相同 Correlation ID 的消息出现。通常还会设置一个超时时间。

Step 2: 响应方 (Replier) 处理请求并回复

  1. 接收请求:响应方服务从请求队列中消费一条消息。
  2. 处理业务逻辑:执行消息内容所要求的业务计算或操作。
  3. 提取元数据:从收到的请求消息的属性中,提取出 Reply-ToCorrelation ID 的值。
  4. 构造响应消息:响应方创建响应消息,其内容是业务处理的结果。
  5. 设置并发送响应:在响应消息的属性中,必须将从请求中收到的那个 Correlation ID 原封不动地设置回去。然后,将此响应消息发送到请求消息中 Reply-To 字段所指定的那个队列地址。

Step 3: 请求方 (Requester) 接收响应

  1. 接收消息:请求方在其临时回复队列上收到了一个消息。
  2. 匹配关联 ID:它检查收到的响应消息中的 Correlation ID 是否与它当初发送的那个 ID 相匹配。
  3. 完成闭环:如果 ID 匹配,则证明这就是它所等待的响应。请求-响应的流程至此完成。请求方可以处理响应结果,然后安全地删除那个临时的回复队列。

可靠性

What are some of the techniques for preventing data loss when sending and receiving messages from a queue?

有哪些技术可以防止在从队列发送和接收消息时丢失数据?

这是一个生产者、消费者和代理三方共同的责任:

1. 代理端 (Broker Side)

  • 持久化 (Persistence):代理在将事件放入队列或主题时,会先将其写入磁盘,确保即使代理重启,事件也不会丢失。
  • 集群与复制 (Clustering and Replication):通过将代理部署为集群,并将事件在多个节点间进行复制,可以防止单点故障导致的数据丢失。

2. 客户端 (Client Side)

  • 消费者确认 (ACK):消费者在成功处理完一个事件后,必须向代理发送一个 ACK 信号。如果消费者在处理过程中崩溃而未发送 ACK,代理会认为该事件未被成功处理,并会将其重新投递给其他消费者。
  • 事务性发件箱 (Transactional):这是一个非常关键的高级模式。为了确保"写入业务数据库"和"发送事件"这两个操作的原子性,开发者会将待发送的事件与业务数据变更放在同一个本地数据库事务中,写入一个发件箱(Outbox)表。然后由一个独立的轮询进程负责读取发件箱表,并将事件可靠地发送给代理。这彻底解决了"业务成功但事件未发出"的问题。

架构权衡

What are three main driving architecture characteristics for using event-driven architecture?

使用事件驱动架构的三个主要驱动架构特性是什么?

  • 可伸缩性与弹性 (Scalability & Elasticity):如前所述,独立伸缩组件的能力是其核心优势。
  • 可扩展性 (Extensibility):系统极易扩展。当需要增加新功能时,只需开发一个新的服务来订阅感兴趣的现有事件即可,完全无需改动已有服务。
  • 响应性 (Responsiveness):对于需要快速响应用户的系统,可以将耗时任务异步化。例如,用户提交视频后,系统立即返回"上传成功,正在处理中",然后通过事件驱动后台的转码、审核等一系列复杂流程。

What are some of the architecture characteristics that are not well supported in event-driven architecture?

事件驱动架构不能很好地支持哪些架构特性?

  • 简单性 (Simplicity):EDA 显著增加了系统的复杂性。你需要管理消息代理,处理异步编程的挑战(如调试、错误处理),并应对最终一致性带来的心智负担。
  • 事务性 (Transactional):实现跨多个服务的原子性操作(即分布式事务)变得异常困难。虽然可以通过 Saga 等模式来模拟长事务,但其实现复杂,且只能保证最终一致性而非强一致性。
  • 工作流的可观测性 (Observability of Workflow):尤其是在代理拓扑中,业务流程被分散到各个独立的处理器中,没有一个集中的地方可以让你直观地看到一个完整的业务流程是如何执行的,这给监控和排错带来了巨大挑战。