事件驱动架构图:如何画出 Kafka 的生产者、topic 和消费者

在事件驱动系统中,调用关系隐没在 topic 里;本文给出一套记法,让生产者、topic、消费者组和失败路径都清晰可读。

2026/10/22

事件驱动架构图:如何画出 Kafka 的生产者、topic 和消费者

在请求-响应式的系统中,图上的箭头就是调用图。在事件驱动系统中却不是这样。生产者把事件写入 topic,并不知道谁会读取它。如果简单粗暴地画,把每个服务都连到一个大大的 Kafka 方框上,图就会把你需要的一切都藏起来:有哪些事件、谁对它们做出响应、按什么顺序,以及消费者失败时会发生什么。

本文给出一套能让流程保持可读的事件驱动架构图记法,以 Kafka 上的订单处理为例逐步讲解,并介绍大家常常忘记画的细节:key、消费者组、outbox 和死信路径。

图上必须表达什么

一张事件驱动架构图应当无需另附文档,就能回答以下问题:

  • 有哪些事件? 用过去时命名,例如 OrderPlaced 或 PaymentFailed。
  • 每个事件由谁产生? 健康的默认做法是:每种事件类型恰好只有一个所有者。
  • 谁消费它,以哪个消费者组的身份? 在 Kafka 中,每个消费者组都会收到每一条事件,所以每个组都是一个独立的读者。
  • 由什么决定顺序? Kafka 只在分区内保持顺序,而由 key 决定消息进入哪个分区。
  • 失败时会发生什么? 重试、死信 topic,以及由谁来重放。

一套好用的记法

元素 画法
生产者 一个服务节点,用虚线连到 topic,连线上标注事件类型
Topic 单独的节点,名称与集群中一致(orders.v1),并在标签中写上 key
消费者组 一条从 topic 指向消费者的虚线,标注 group id
事件与命令 事件用过去时(OrderPlaced),命令用祈使语气(ReserveStock)
死信 topic 一个单独的 topic 节点,由放弃处理的消费者写入
连接器 位于 topic 与外部存储之间的节点,例如写入数据仓库的 sink
Broker 集群 围住各个 topic 的框;broker 和分区放到运维图上

实线只用于同步调用,例如向支付服务商发起的 HTTP 请求。这样读者一眼就能看出,流程中哪些部分发生在同一个请求里,哪些是之后才发生的。

示例:Kafka 上的订单事件

一家网店用事件来处理订单:

  1. orders 服务在同一个数据库事务中写入订单和一行 outbox 记录。
  2. outbox relay(运行在 Kafka Connect 上的 Debezium)通过数据库日志读取 outbox,并以 order_id 为 key,把 OrderPlaced 发布到 orders.v1。
  3. 三个消费者组读取 orders.v1:payments 从卡上扣款,inventory 预留库存,warehouse sink 把每一条事件加载到数据仓库。
  4. payments 把 PaymentSucceeded 或 PaymentFailed 发布到 payments.v1,同样以 order_id 为 key;orders 服务消费这些事件来更新订单状态。
  5. 如果 inventory 尝试三次后仍无法处理某条消息,就把它写入 inventory.dlq,然后继续处理后面的消息。

下面是画在同一张画布上的完整流程:

事件驱动架构图:orders 把订单行和 outbox 行写入自己的数据库,Debezium 中继将它们发布到 orders.v1。payments、inventory 和一个写入数据仓库的 sink 消费该 topic;payments 通过 payments.v1 回复,由 orders 读取。

11 个组件 · 10 个连接

在 Archboard 中打开
图表内容
  • Orders
  • Orders DB (orders + outbox)
  • Outbox relay
  • Kafka cluster
  • payments.v1 · key order_id
  • orders.v1 · key order_id
  • inventory.dlq
  • Payments
  • Warehouse sink
  • Inventory
  • Data warehouse
  • order + outbox row, one transaction
  • reads WAL
  • OrderPlaced
  • group: payments
  • group: warehouse-sink
  • group: inventory
  • PaymentSucceeded/Failed
  • group: orders
  • load
  • after 3 failed attempts
Kafka 上的订单事件:outbox 中继发布 OrderPlaced,三个消费者组做出响应,payments 在自己的 topic 上回复,处理失败的 inventory 消息进入死信 topic。

注意这张图让哪些事一目了然:没有谁调用 payments;新增第四个消费者组不会改变上游的任何东西;由于两个 topic 都以 order_id 为 key,同一订单的事件在每个 topic 内都保持顺序(Kafka 不保证跨 topic 的顺序)。

事件驱动架构图的五条规则

1. Topic 是节点,不是箭头

从 orders 指向 payments、标着“Kafka”的箭头,暗示了一个并不存在的调用。把 topic 画成两者之间的节点。它是真实存在的东西,有名称、所有者、保留期和 schema。

2. 箭头跟着数据走

按数据流动的方向画:从生产者到 topic,再到消费者。Kafka 消费者是主动拉取消息的,但箭头从消费者指向 topic 的图,几乎所有人都会读反。如果拉取语义对讨论很重要,就加一条注释。

3. 标注事件和消费者组

在生产者的连线上写事件类型,在每条消费者连线上写消费者组。当有人问“还有谁在读 OrderPlaced?”时,答案就是从这个 topic 出发的那些带标签的连线。

4. 顺序重要时,标出 key

Kafka 保证分区内的顺序,而 key 相同的事件会进入同一个分区。如果你的消费者依赖于先看到 OrderPlaced、再看到 OrderCancelled,就把 key 写在 topic 上:orders.v1 · key order_id。标签里没有 key 的 topic,难免会招来这个问题。

5. 画出异常路径

Kafka 的消费者组没有内置的死信队列:重试几次、无法处理的消息放到哪里,都由你的消费者代码或框架决定(Kafka Connect 的 sink 连接器倒是有死信 topic 的配置项)。把这个决定画出来。一个由消费者连过去的死信 topic,再加一条注释说明谁在监控它、消息如何重放,就能省掉一次事故。

画出 outbox,而不是捷径

如果一个服务分两步走,先写数据库、再发布到 Kafka,两步之间一旦崩溃,要么丢失一条事件,要么为一个从未提交的变更发布了事件。Outbox 模式避免了这种双写:服务在同一个事务中把事件写入 outbox 表,再由中继负责发布。

在图上画出真实路径:服务 → 数据库 → 中继 → topic。从服务直接指向 topic 的箭头描述的是另一个可靠性更差的系统,而人们会据此做出决策。

协同(choreography)还是编排(orchestration)

示例采用的是协同方式:各服务对彼此的事件做出响应,没有谁统管整个流程。如果你改用编排器,也就是一个发送 ReserveStock 之类的命令并等待回复的工作流引擎,就把编排器画成位于中间的服务,并把它的连线标注为命令。无论哪种方式,都要在连线上为主流程的步骤编号,方便读者按顺序跟踪整个 Saga。

别把集群画进逻辑视图

Broker、分区、副本因子和控制器节点关乎容量和运维,与理解流程无关。把它们放到单独的运维图上。在逻辑视图中,用一个标着“Kafka cluster”的框把各个 topic 围起来就够了。

在 Archboard 中构建

  1. 使用 Google 或 GitHub 登录,并新建一张图。
  2. 从 C4 模型节点包中添加 容器 节点来表示各个服务,再从数据节点包中添加 orders 的数据库(Postgres)。
  3. 添加一个基础节点包中的 框架,标签设为 Kafka cluster,再把数据节点包中的 主题 节点放进去。
  4. 把 outbox relay 和 warehouse sink 添加为容器,在“技术”字段中填写 Kafka Connect,再从数据节点包中添加一个 数据仓库 节点。
  5. 连接端口,把事件连线设为虚线,并标注事件类型和消费者组。
  6. 评审时用演示模式打开,分享实时链接,或导出 SVG 和 PNG。

这个示例用了 11 个图表节点,在 Free 方案的 15 个之内。另一种流式处理布局见 /examples/streaming-data-platform;类似网店的同步部分,可参阅微服务架构图指南。

常见问题

要画分区吗? 逻辑视图上不画。如果分区数很重要,就写在 topic 的标签里,再在运维图上画出分区和 broker。

在图上,队列和 topic 有什么区别? 在队列中,每条消息只交给一个消费者;在 Kafka topic 中,每个消费者组都会收到每一条消息。SQS 或 RabbitMQ 这类队列用队列节点表示,并写明你依赖的是哪种语义。

事件 schema 放在哪里? 放在注释或 topic 标签中,并附上指向 schema registry 或拥有该 schema 的代码仓库的链接。图应当表明契约存在,以及它在哪里。

如何表示重放? 在 topic 上写明保留期(例如 retention 7d),并加一条注释说明消费者组如何重置 offset。重放是一项设计特性,要让它可见。

每个事件都需要一张图吗? 不需要。每个业务流程一张图就很好用。在连线上列出事件,完整列表则维护在事件目录里。

免费开始 — 1 个席位,无需信用卡。

相关文章

← 返回博客

一分钟打开画布

使用 Google 或 GitHub 登录。单席位免费,团队加入后再升级。