基于 C4 模型的事件驱动架构文档
事件驱动架构已经成为构建可扩展、松耦合系统的默认选择。服务在发生事件时发布事件。其他服务订阅这些事件并做出响应。发布者不知道谁在监听。订阅者不知道谁发布了事件。系统是解耦的、有弹性的、灵活的。
但它也几乎是不可见的。
当你查看一个事件驱动系统的传统 C4 图时,你看到的是服务以箭头指向消息中间件。Service A 发布到 Kafka。Service B 从 Kafka 消费。但哪些事件在它们之间流动?Schema 是什么?还有谁在监听?如果消费者失败了会怎样?图表展示了管道设施,却隐藏了行为。
记录事件驱动架构需要让不可见的变得可见的技术——不仅展示基础设施(Broker、Queue、Topic),还展示事件本身、它们的流动、它们的 Schema 和它们的保证。本指南介绍如何使用 C4 模型来实现这一点,以及 Archyl 的 Event Channel 功能如何让事件驱动系统成为架构文档中的一等公民。
为什么事件驱动系统难以记录
事件驱动架构引入了同步请求-响应系统中不存在的几个文档挑战。
不可见的控制流
在同步系统中,你可以通过跟踪调用链来追踪请求从客户端到服务器的路径。Service A 调用 Service B,Service B 调用 Service C。控制流是显式的,在代码中可见。
在事件驱动系统中,控制流是隐式的。Service A 发布了一个 OrderCreated 事件。某处,Service B 对该事件做出响应并预留库存。另一处,Service C 对该事件做出响应并发送确认邮件。Service A 不知道 B 和 C 的存在。控制流由事件订阅定义,而非由发布者的代码定义。
这种间接性意味着你无法通过阅读单个服务的代码来理解系统的行为。你需要一个展示跨服务事件流的更高层级视图——而这正是架构文档应该提供的。
多对多关系
在同步架构中,关系通常是一对一或一对少数。Service A 调用 Service B。关系是直接的,由 API 调用记录。
在事件驱动架构中,关系是多对多的。一个事件类型可能有一个生产者和五个消费者。一个服务可能消费来自十个不同生产者的事件。关系图比同步系统更密集、更复杂。
传统架构图难以应对这种密度。从每个生产者通过每个 topic 到每个消费者画箭头会产生一张看起来像一盘意大利面的图。你需要一种在正确抽象层级展示事件流的文档方法。
Schema 演进
事件 Schema 随时间演进。OrderCreated 事件可能从五个字段开始,在两年内增长到十五个。消费者可能依赖特定字段。如果 Schema 变更不向后兼容,可能会破坏消费者。
记录当前 Schema 是必要的,但还不够。你还需要记录 Schema 版本策略、兼容性保证和破坏性变更的历史。
最终一致性
事件驱动系统本质上是最终一致的。当 Service A 发布事件时,Service B 可能在几毫秒后处理它,也可能在几分钟后(如果消费者落后或需要重试)。系统在这段窗口期内处于不一致状态。
文档应该捕捉这些一致性边界。系统的哪些部分是强一致的?哪些是最终一致的?预期的传播延迟是多少?在不一致窗口期内会发生什么?
死信队列和错误处理
当事件消费者无法处理消息时,事件通常进入死信队列 (DLQ)。但然后呢?谁监控 DLQ?重试策略是什么?毒消息如何处理?
这些错误处理模式对系统行为至关重要,但很少被记录。它们是架构的一部分,应该在文档中可见。
使用 C4 模型建模事件驱动系统
通过对各层级使用方式的一些调整,C4 模型可以有效地表示事件驱动架构。
System Context:关注数据流,而非调用
在 System Context 层级,事件驱动和同步架构看起来类似。你的系统与用户和外部系统交互。关键区别在于你如何标记关系。
不要使用"调用"或"查询",而使用描述数据流的标签:
- "发送订单事件到"
- "接收支付确认从"
- "发布分析事件到"
这些标签暗示了通信的异步特性,而不会在高层视图中塞入实现细节。
Container 图:让 Broker 可见
Container 图是事件驱动架构变得独特的地方。消息中间件(Kafka、RabbitMQ、Amazon SQS/SNS、Google Pub/Sub)应该是图中的一等容器,而不是一个不可见的实现细节。
以下是一个事件驱动电商系统的 Container 图可能的样子:
systems:
- name: E-Commerce Platform
type: software_system
containers:
- name: Order Service
type: service
technologies: [Go, PostgreSQL]
- name: Inventory Service
type: service
technologies: [Java, PostgreSQL]
- name: Notification Service
type: service
technologies: [Python, Redis]
- name: Analytics Service
type: service
technologies: [Python, ClickHouse]
- name: Event Bus
type: queue
technologies: [Apache Kafka]
relationships:
- from: Order Service
to: Event Bus
label: "Publishes OrderCreated, OrderCancelled"
- from: Event Bus
to: Inventory Service
label: "Delivers order events"
- from: Event Bus
to: Notification Service
label: "Delivers order and payment events"
- from: Event Bus
to: Analytics Service
label: "Delivers all domain events"
- from: Inventory Service
to: Event Bus
label: "Publishes InventoryReserved, InventoryReleased"
注意 Event Bus 位于图的中心,关系显式命名了流经它的事件类型。这使事件流可见,而不需要在每个生产者和每个消费者之间创建直接箭头。
Event Channel:一等概念
消息中间件内的各个 topic、queue 和 stream 值得拥有自己的文档。每个 Event Channel 都有对理解系统至关重要的属性:
- Channel 名称:Kafka topic、RabbitMQ queue 或 SQS queue 名称
- 事件类型:通过此 channel 流动的事件
- 生产者:哪些服务向此 channel 发布
- 消费者:哪些服务从此 channel 消费
- 序列化:事件如何编码(JSON、Avro、Protobuf)
- 分区策略:事件如何在分区间分布
- 保留策略:事件保留多长时间
- 顺序保证:顺序是否被保留以及在什么粒度上
在 Archyl 中,Event Channel 是一种专用实体类型。你创建一个 Event Channel,指定其属性,并将其链接到生产和消费它的服务。这创建了一个结构化的、可查询的事件流模型。
例如,一个"orders"Event Channel 可能被记录为:
- 名称:orders
- Broker:Kafka
- 生产者:Order Service
- 消费者:Inventory Service、Notification Service、Analytics Service、Billing Service
- 事件类型:OrderCreated、OrderUpdated、OrderCancelled、OrderCompleted
- 序列化:Avro with Schema Registry
- 分区:按 order ID
- 保留期:7 天
这种详细程度使不可见的基础设施变得可见且可查询。当开发者需要知道谁在消费订单事件时,答案是被记录的且可找到的。
Component 图:Event Handler 和 Publisher
在 Component 层级,事件驱动服务具有值得记录的独特内部结构(针对复杂服务):
- Event Handler:消费和处理特定事件类型的组件
- Event Publisher:产生事件的组件
- Saga / Process Manager:通过事件编排多步骤工作流的组件
- Projection:从事件流构建读模型的组件
Order Service 的 Component 图可能包括:
- Order Controller——处理订单管理的 HTTP 请求
- Order Processor——订单创建和验证的核心业务逻辑
- Event Publisher——将 OrderCreated、OrderUpdated、OrderCancelled 发布到 Kafka
- Payment Event Handler——消费 PaymentProcessed 和 PaymentFailed 事件
- Order Saga——管理跨服务的订单履行工作流
当服务足够复杂时记录这些组件——特别是参与编排或协调模式的服务。
记录常见的事件驱动模式
某些模式在事件驱动系统中反复出现。显式记录它们可以使团队免于从代码中逆向工程这些模式。
Event Sourcing
在 Event Sourcing 系统中,实体的状态从一系列事件中派生,而不是作为快照存储。事件流是事实来源,当前状态是一个投影。
记录 Event Sourcing 的方法:
- 识别哪些实体使用了 Event Sourcing
- 列出每个实体事件流中的事件类型
- 记录从流中派生读模型的投影
- 记录快照策略(如果有的话)
CQRS(命令查询职责分离)
CQRS 将写操作(命令)与读操作(查询)分离,通常使用事件来保持读模型与写模型的同步。
记录 CQRS 的方法:
- 在 C4 模型中清晰地将命令端容器与查询端容器分开
- 记录从写端到读端的事件流
- 记录一致性模型(读模型可以落后多远)
编排 vs. 协调
在编排模式中,服务独立地对事件做出响应。没有中央协调者。在协调模式中,一个中央服务(协调者或 Saga)通过发送命令和监听响应来协调工作流。
记录你的系统在每个工作流中使用的模式。如果使用编排,记录预期的事件序列和参与的服务。如果使用协调,记录 Saga 的状态机和它发出的命令。
死信队列和重试模式
记录你的事件处理错误处理策略:
- 哪些事件有死信队列?
- 重试策略是什么(次数、退避策略)?
- 谁负责监控和重新处理 DLQ 事件?
- DLQ 积压时有什么告警?
在 Archyl 中,你可以将 DLQ 建模为与主 channel 关联的额外 Event Channel。这使错误处理基础设施在架构文档中可见。
记录事件 Schema
事件 Schema 是生产者和消费者之间的契约。它们值得获得与同步系统中 API 契约同等级别的文档记录。
Schema 文档
对于每种事件类型,记录:
- 事件名称:清晰的、面向领域的名称(OrderCreated,而非 GenericEvent)
- Schema 版本:Schema 的当前版本
- 字段:所有字段及其类型、描述,以及是否必填或可选
- 示例负载:具有代表性的 JSON/Avro/Protobuf 示例
- 兼容性:Schema 是前向兼容、后向兼容还是完全兼容
Archyl 的 API Contract 功能可以用来记录事件 Schema,与 REST 和 gRPC 规范放在一起。将契约链接到 Event Channel,在 Schema 和基础设施之间建立直接联系。
Schema 演进策略
记录你的团队对 Schema 演进的方法:
- 你是否使用 Schema Registry(Confluent Schema Registry、AWS Glue)?
- 强制执行什么兼容性模式(后向、前向、完全)?
- 破坏性变更如何通知消费者?
- 旧 Schema 版本的废弃流程是什么?
这些信息应该放在与你的事件基础设施关联的 ADR 中。这是一个影响整个系统的决策,应该被清晰地记录一次,并被所有团队引用。
可视化事件流
静态图表可以展示事件基础设施,但它们难以展示事件流——实现业务流程的事件序列。
使用 Flow 记录业务流程
Archyl 的 Flow 功能让你记录实现业务流程的事件序列。例如,一个"订单下单"流程可能展示:
- 客户通过 Web App 提交订单
- API Gateway 将请求转发到 Order Service
- Order Service 验证并持久化订单
- Order Service 将 OrderCreated 事件发布到 Kafka
- Inventory Service 消费 OrderCreated,预留库存
- Inventory Service 发布 InventoryReserved 事件
- Payment Service 消费 InventoryReserved,处理支付
- Payment Service 发布 PaymentProcessed 事件
- Notification Service 消费 PaymentProcessed,发送确认邮件
这个流程展示了从事件驱动架构中涌现的端到端行为。没有任何单个服务的代码能揭示这个流程——它只存在于架构层面。
使用叠加层展示不同视图
创建叠加层来展示事件驱动架构的不同方面:
- 事件流叠加层:仅突出显示与事件相关的关系,隐藏同步通信
- 生产者/消费者叠加层:根据服务是生产者、消费者还是两者都是来用颜色编码
- 错误处理叠加层:展示 DLQ、重试路径和监控
叠加层让你从一个单一的架构模型创建聚焦的视图,避免了需要多个冗余图表。
最佳实践
以领域操作命名事件,而非技术操作
使用像 OrderCreated、PaymentFailed、InventoryReserved 这样的名称——而不是 DataUpdated、MessageSent 或 RecordInserted。面向领域的名称使事件流在架构层面可读。
记录"为什么不用同步"的决策
对于每个事件驱动的交互,都有一个使用异步而非同步通信的决策。记录这个决策。为什么 Order Service 发布事件而不是直接调用 Inventory Service?答案(解耦、弹性、可扩展性)应该在 ADR 中捕获。
让 Event Channel 文档贴近代码
如果你的事件 Schema 在代码中定义(Protobuf 文件、Avro Schema、JSON Schema),将架构文档链接到这些文件。这在抽象文档和具体实现之间建立了联系。
在架构评审中审查事件流
在季度架构评审中,逐一检查你记录的事件流。提问:
- 是否有新的事件类型未被记录?
- 是否有已记录的事件类型不再使用?
- 是否有新的消费者被添加而未更新文档?
- 已记录的 Schema 是否仍然准确?
Archyl 的漂移检测有助于自动回答这些问题,但定期的人工审查能捕获自动检查遗漏的内容。
结论
事件驱动架构文档需要有意识的努力来让不可见的变得可见。事件在设计上解耦了生产者和消费者。这是架构上的优势,但也是文档上的挑战。
C4 模型提供了框架:System Context 描绘全景,Container 图将消息中间件作为一等元素,Event Channel 提供详细的 topic 和 queue 文档,Component 图记录复杂的 Event Handler 和 Saga。
Archyl 提供了工具:Event Channel 作为一等实体,Flow 记录事件驱动的业务流程,API Contract 记录事件 Schema,叠加层提供聚焦视图,漂移检测捕获未记录的变更。
像你设计事件驱动架构一样来记录它:带着意图、结构,以及这样的认识——系统的行为涌现自服务之间的交互,而非来自任何单个服务本身。
开始使用 Archyl,让你的事件驱动架构变得可见、有文档、被理解。