Kafka 不难,只是你用得不对

本文分享使用 Kafka 的一些经典模式。有时你感觉 Kafka 好难搞,可能是因为不了解这些模式。

让我们从基础开始:

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 并不难。你跳过了那些让事情变得易于处理的模式,结果反而把事情弄复杂了。

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

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

相关推荐
linux修理工2 小时前
kafka积压
数据库·分布式·kafka
杰克逊的日记2 小时前
kafka消息堆积了怎么处理
大数据·分布式·kafka
linux修理工2 小时前
使用codebuddy调优kafka等
分布式·kafka
functionflux20 小时前
kafka-python:Python 生态中最成熟的 Kafka 客户端
分布式·python·其他·kafka
q21030633721 天前
kafka启动几秒后挂了,重启多次无果
分布式·kafka
abcy0712131 天前
在Python 中使用Celery和Kafka进行消息队列的生产者和消费者实现
python·kafka
阿坤带你走近大数据2 天前
如何保证kafka中的数据一致性
分布式·kafka
阿坤带你走近大数据2 天前
Kafka中的分区概念
分布式·kafka
爱吃牛肉的大老虎2 天前
Kafka集群之抛弃 Zookeeper
分布式·zookeeper·kafka
Solis程序员2 天前
Kafka 灾难回放机制:基于事件事实流的计数全量恢复方案
分布式·kafka