Kafka 不难,只是你用得不对

Kafka 难用往往不是工具本身的问题,而是主题建模、消费者组、状态流转、发件箱、重试队列和 Schema 演进没有设计好。本文用 6 个模式说明 Kafka 如何支撑可靠的事件驱动架构。

作者 Abhinav

Kafka 使用模式

本文分享使用 Kafka 的一些经典模式。有时你感觉 Kafka 好难搞,可能不是因为 Kafka 本身复杂,而是主题、消费者组、状态、重试和 Schema 演进这些基础模式没有设计好。

核心要点

  • Kafka topic 应该围绕业务事件命名,而不是围绕服务命名;否则很容易把事件流做成“绕远路的 RPC”。
  • consumer group 代表一个独立的业务消费目的,同一事件可以被库存、邮件、分析等多个逻辑工作单元独立消费。
  • 事件驱动不等于无状态串联事件,关键流程仍然需要由有状态服务判断是否发出下一个事件。
  • 发件箱模式、退避重试、DLQ 和 Schema Registry 是 Kafka 生产可用性的基础设施,不是可选装饰。

什么时候会觉得 Kafka 难用?

Kafka 适合承载事件驱动架构,但它不会自动替你完成领域建模、失败恢复和兼容性治理。很多团队踩坑的根因不是“不会写生产者和消费者”,而是把 topic 当服务队列、把消费者组当实例编号、把失败消息当作可以无限重放的普通消息。

让我们从基础开始:

1. 每个事件类型一个主题

反模式:

orders-service-topic
shipping-service-topic
analytics-service-topic

每个服务都有自己的主题?不不不,你要这么搞,那就不是事件驱动设计了,这种设计只能看做是多了些步骤的远程过程调用(RPC)。

正模式:

order.created
order.shipped
order.cancelled

主题应该表示一个什么什么事件,多个服务可以对同一事件做出反应,这样你的架构才能保持松耦合。

2. 消费者组 = 一个逻辑工作单元

消费者组很简单:一个组 = 一个合理的业务目的。

但很多团队都把这一点搞砸了。他们为每个实例创建一个消费者组,更糟糕的是,还会为不同的任务重复使用同一个组。

正模式:

  • inventory-updater 仓库更新器
  • email-sender 邮件发送器
  • analytics-processor 分析处理器

这些中的每一个都是独立的组。它们都使用 order.created 并独立完成自己的工作。

这将所有内容解耦。它还具有出色的扩展性——Kafka 会在同一组内的消费者之间对分区进行负载均衡。

3. 避免无状态的链式事件

假设你收到一个 order.created 的消息,然后你甚至没有检查任何内容就立即发布一个 order.verified 的消息。这很危险。不要把 Kafka 变成一个愚蠢的消息传递者。

正模式:事件 + 状态

不要为了模拟工作流程而将五个事件串联在一起,而是让事件触发一个拥有逻辑和状态的服务,并且仅在必要时发出下一个事件。

这样可以避免意外的无限循环、幽灵事件和令人困惑的重放错误。

4. 使用发件箱模式

有没有试过在插入数据库后向 Kafka 发送消息,结果 Kafka 调用失败?现在你的数据库已更新……但 Kafka 从未收到该消息。恭喜你——你刚刚制造了一个不一致性!

发件箱模式来解决这个问题

将事件作为同一事务的一部分存储在你的数据库中,如下所示:

BEGIN;
INSERT INTO orders (...) VALUES (...);
INSERT INTO outbox (event_type, payload) VALUES ('order.created', '{...}');
COMMIT;

然后运行一个后台任务,读取 outbox 表并将事件发送到 Kafka。

原子!可靠!可重放!

5. 采用退避重试

人们认为“Kafka 让一切都可重放”,所以他们滥用它。他们只是不假思索地重新消费失败的事件。

但如果错误是由于下游系统宕机导致的呢?(比如A服务消费Kafka消息时要调用B服务做一些逻辑,此时B服务挂了,进而导致最终未能成功消费)

使用带有指数退避的重试队列,或者使用像Kafka Streams / Debezium这样内置错误处理功能的框架。

或者手动创建如下主题存放消费失败的消息,延迟重试:

  • order.created.retry.1m
  • order.created.retry.10m
  • order.created.dlq

巴辉特提醒:这种 retry 队列的模式很有意思,之前我也未曾关注过。通常来讲,建议一个 topic + 一个 consumer group 作为颗粒度创建一套 retry 队列,因为事件可能有多个 consumer group 来消费。这个知识有点绕,大家可以通过 GPT 了解更多细节。

举例,比如 100 条事件里只有 1 条消费失败,如果持续重试这一条,持续失败,就会影响后面的事件消费。

当然,如果你们的业务逻辑就必须得严格要求顺序,那另当别论,case by case 来看哈。

6. Schema 要全局统一

如果您的事件看起来像这样:

{
  "id": 123,
  "user": "john",
  "total": 100
}

这样没问题……直到有人添加了一个新字段:

{
  "id": 123,
  "user": "john",
  "total": 100,
  "vip": true
}

现在,你的使用者程序出现故障。或者更糟的是—— 悄无声息地出现异常行为。

巴辉特提醒:这个意思是说,consumer 不知道事件格式发生变化,可能会引起故障,需要一个机制,让所有人知道消息格式变了,而且消息格式需要兼容性。

使用 Avro/Protobuf + Schema Registry,你会得到:

  • 向前/向后兼容性
  • 严格类型标注
  • 演进支持

当团队壮大时,你会感谢自己的这种做法。

一张简单的 Kafka 架构图

          +------------------+
          |  Order Service   |
          |------------------|
          | Emits: order.created |
          +--------+---------+
                   |
                   v
          +--------+---------+
          |     Kafka         |
          |-------------------|
          | Topics:            |
          | - order.created    |
          | - order.retry      |
          | - order.dlq        |
          +--------+----------+
                   |
       +-----------+------------+
       |           |            |
       v           v            v
+-----------+ +--------------+ +------------------+
|Inventory  | |Email Service | |Analytics Service |
|Updater    | |(Consumer)    | |(Consumer)        |
+-----------+ +--------------+ +------------------+

每个服务都是一个独立的消费者组,对相同的事件做出反应。发件箱模式(未显示)在生产者端实现。

Kafka 设计检查表

设计点 推荐做法 常见风险
Topic 命名 使用 order.created 这类事件名 用服务名建 topic,耦合生产者和消费者
消费者组 一个 consumer group 对应一个业务目的 每个实例一个组,或者多个任务复用同一组
流程推进 由有状态服务判断是否发出后续事件 无条件链式发布,制造循环和重放混乱
写库与发消息 使用 outbox 表保证同一事务内记录事件 数据库已提交但 Kafka 发送失败
失败处理 退避重试、重试 topic、DLQ 分层处理 失败消息原地反复阻塞后续消费
Schema 演进 Avro/Protobuf + Schema Registry 字段变化导致消费者故障或静默异常

常见问题

Kafka topic 应该按服务建,还是按事件建?

优先按事件建。按服务建 topic 会让消费者感知生产服务边界,系统很容易退化成异步 RPC;按事件建 topic,多个消费者可以围绕同一个业务事实独立响应。

Kafka 消费失败后直接重放可以吗?

不建议无脑重放。下游不可用、数据格式不兼容、业务校验失败都可能导致持续失败。更稳妥的方式是使用退避重试队列和 DLQ,把短暂故障、长时间故障和需要人工处理的消息分开。

为什么 Schema Registry 很重要?

事件一旦被多个团队消费,字段新增、删除和类型变化都会影响消费者。Schema Registry 的价值在于把兼容性检查前置,避免格式演进变成线上故障。

总结

Kafka 并不难。你跳过了那些让事情变得易于处理的模式,结果反而把事情弄复杂了。

你越早遵循这些原则,Kafka就越早成为一个健壮、可扩展的事件驱动系统的支柱,而不是凌晨3点令人头疼的调试难题。

原文:https://codingplainenglish.medium.com/kafka-is-hard-because-you-keep-ignoring-these-patterns-588b2ebac3c0

延伸路径

继续看解决方案和产品对比

如果你正在做监控、可观测性或故障定位相关选型,建议从解决方案和产品对比继续往下看。

标签 Kafka
快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云