QCon 全球软件开发大会(北京站)门票 9 折倒计时 4 天,点击立减 ¥880 了解详情
写点什么

Kafka 实践:到底该不该把不同类型的消息放在同一个主题中

2018 年 8 月 18 日

如果你使用了像 Kafka 这样的流式处理平台,就要搞清楚一件事情:你需要用到哪些主题?特别是如果你要将一堆不同的事件作为消息发布到 Kafka,是将它们放在同一个主题中,还是将它们拆分到不同的主题中?

Kafka 主题最重要的一个功能是可以让消费者指定它们想要消费的消息子集。在极端情况下,将所有数据放在同一个主题中可能不是一个好主意,因为这样消费者就无法选择它们感兴趣的事件——它们需要消费所有的消息。另一种极端情况,拥有数百万个不同的主题也不是一个好主意,因为 Kafka 的每个主题都是有成本的,拥有大量主题会损害性能。

实际上,从性能的角度来看,分区数量才是关键因素。在 Kafka 中,每个主题至少对应一个分区,如果你有 n 个主题,至少会有 n 个分区。不久之前,Jun Rao 写了一篇博文,解释了拥有多个分区的成本(端到端延迟、文件描述符、内存开销、发生故障后的恢复时间)。根据经验,如果你关心延迟,那么每个节点分配几百个分区就可以了。如果每个节点的分区数量超过成千上万个,就会造成较大的延迟。

关于性能的讨论为设计主题结构提供了一些指导:如果你发现自己有数千个主题,那么将一些细粒度、低吞吐量的主题合并到粗粒度主题中可能是个明智之举,这样可以避免分区数量蔓延。

然而,性能并不是我们唯一关心的问题。在我看来,更重要的是主题结构的数据完整性和数据模型。我们将在本文的其余部分讨论这些内容。

主题等于相同类型事件的集合?

人们普遍认为应该将相同类型的事件放在同一主题中,不同的事件类型应该使用不同的主题。这种思路让我们联想到关系型数据库,其中表是相同类型记录的集合,于是我们就有了数据库表和Kafka 主题之间的类比。

Confluent Avro Schema Registry 进一步强化了这种概念,因为它鼓励你对主题的所有消息使用相同的 Avro 模式(schema)。模式可以在保持兼容性的同时进行演化(例如通过添加可选字段),但所有消息都必须符合某种记录类型。稍后我会再回过头来讨论这个问题。

对于某些类型的流式数据,例如活动事件,要求同一主题中所有消息都符合相同的模式,这是合理的。但是,有些人把 Kafka 当成了数据库来用,例如事件溯源,或者在微服务之间交换数据。对于这种情况,我认为是否将主题定义为具有相同模式的消息集合就不那么重要了。这个时候,更重要的是主题分区中的消息必须是有序的。

想象一下这样的场景:你有一个实体(比如客户),这个实体可能会发生许多不同的事情,比如创建客户、客户更改地址、客户向帐户中添加新的信用卡、客户发起客服请求,客户支付账单、客户关闭帐户。

这些事件之间的顺序很重要。例如,我们希望其他事件必须在创建客户之后才能发生,并且在客户关闭帐户之后不能再发生其他事件。在使用 Kafka 时,你可以将它们全部放在同一个主题分区中来保持它们的顺序。在这个示例中,你可以使用客户 ID 作为分区的键,然后将所有事件放在同一个主题中。它们必须位于同一主题中,因为不同的主题对应不同的分区,而 Kafka 是不保证分区之间的顺序的。

顺序问题

如果你为 customerCreated、customerAddressChanged 和 customerInvoicePaid 事件使用了不同的主题,那么这些主题的消费者可能就看不到这些事件之间的顺序。例如,消费者可能会看到一个不存在的客户做出的地址变更(这个客户尚未创建,因为相应的 customerCreated 事件可能发生了延迟)。

如果消费者暂停一段时间(比如进行维护或部署新版本),那么事件出现乱序的可能性就更高了。在消费者停止期间,事件继续发布,并且这些事件被存储在特定定的主题分区中。当消费者再次启动时,它会消费所有积压在分区中的事件。如果消费者只消费一个分区,那就没问题:积压的事件会按照它们存储的顺序依次被处理。但是,如果消费者同时消费几个主题,就会按任意顺序读取主题中数据。它可以先读取积压在一个主题上的所有数据,然后再读取另一个主题上积压的数据,或者交错地读取多个主题的数据。

因此,如果你将 customerCreated、customerAddressChanged 和 customerInvoicePaid 事件放在三个单独的主题中,那么消费者可能会在看到 customerCreated 事件之前先看到 customerAddressChanged 事件。因此,消费者很可能会看到一个客户的 customerAddressChanged 事件,但这个客户却未被创建。

你可能会想到为每条消息附加时间戳,并用它来对事件进行排序。如果你将事件导入数据仓库,再对事件进行排序,或许是没有问题的。但在流数据中只使用时间戳是不够的:在你收到一个具有特定时间戳的事件时,你不知道是否需要等待具有较早时间戳的事件,或者所有之前的事件是否已经在当前事情之前到达。依靠时钟进行同步通常会导致噩梦,有关时钟问题的更多详细信息,请参阅“Designing Data-Intensive Applications”的第 8 章。

何时拆分主题,何时合并主题?

基于这个背景,我将给出一些经验之谈,帮你确定哪些数据应该放在同一主题中,以及哪些数据应该放在不同的主题中。

  1. 首先,需要保持固定顺序的事件必须放在同一主题中(并且需要使用相同的分区键)。如果事件属于同一实体,那么事件的顺序就很重要。因此,我们可以说,同一实体的所有事件都应该保存在同一主题中。 如果你使用事件溯源进行数据建模,事件的排序尤为重要。聚合对象的状态是通过以特定的顺序重放事件日志而得出的。因此,即使可能存在不同的事件类型,聚合所需要的所有事件也必须在同一主题中。
  2. 对于不同实体的事件,它们应该保存在相同的主题中还是不同的主题中?我想说,如果一个实体依赖于另一个实体(例如一个地址属于一个客户),或者经常需要同时用到它们,那么它们也应该保存在同一主题中。另一方面,如果它们不相关,并且属于不同的团队,那么最好将它们放在不同的主题中。 另外,这也取决于事件的吞吐量:如果一个实体类型的事件吞吐量比其他实体要高很多,那么最好将它分成几个主题,以免让只想消费低吞吐量实体的消费者不堪重负(参见第 4 点)。不过,可以将多个具有低吞吐量的实体合并起来。
  3. 如果一个事件涉及多个实体该怎么办?例如,订单涉及到产品和客户,转账至少涉及到两个账户。 我建议在一开始将这些事件记录为单个原子消息,而不是将其分成几个属于不同主题的消息。在记录事件时,最好可以保持原封不动,即尽可能保持数据的原始形式。你可以随后使用流式处理器来拆分复合事件,但如果过早进行拆分,想要重建原始事件会难得多。如果能够为初始事件分配一个唯一 ID(例如 UUID)就更好了,之后如果你要拆分原始事件,可以带上这个 ID,从而可以追溯到每个事件的起源。
  4. 看看消费者需要订阅的主题数量。如果几个消费者都订阅了一组特定的主题,这表明可能需要将这些主题合并在一起。 如果将细粒度的主题合并成粗粒度的主题,一些消费者可能会收到他们不需要的事件,需要将其忽略。这不是什么大问题:消费消息的成本非常低,即使最终忽略了一大半的事件,总的成本可能也不会很大。只有当消费者需要忽略绝大多数消息(例如 99.9%是不需要的)时,我才建议将大容量事件流拆分成小容量事件流。
  5. 用作 Kafka Streams 状态存储(KTable)的变更日志主题应该与其他主题分开。在这种情况下,这些主题由 Kafka Streams 流程来管理,所以不应该包含其他类型的事件。 最后,如果基于上述的规则依然无法做出正确的判断,该怎么办?那么就按照类型对事件进行分组,把相同类型的事件放在同一个主题中。不过,我认为这条规则是最不重要的。

模式管理

如果你的数据是普通文本(如 JSON),而且没有使用静态的模式,那么就可以轻松地将不同类型的事件放在同一个主题中。但是,如果你使用了模式编码(如 Avro),那么在单个主题中保存多种类型的事件则需要考虑更多的事情。

如上所述,基于 Avro 的 Kafka Confluent Schema Registry 假设了一个前提,即每个主题都有一个模式(更确切地说,一个模式用于消息的键,一个模式用于消息的值)。你可以注册新版本的模式,注册表会检查模式是否向前和向后兼容。这样设计的一个好处是,你可以让不同的生产者和消费者同时使用不同版本的模式,并且仍然保持彼此的兼容性。

Confluent 的 Avro 序列化器通过 subject 名称在注册表中注册模式。默认情况下,消息键的 subject 为-key,消息值的 subject 为-value。模式注册表会检查在特定 subject 下注册的所有模式的相互兼容性。

最近,我为 Avro 序列化器提供了一个补丁( https://github.com/confluentinc/schema-registry/pull/680 ),让兼容性检查变得更加灵活。这个补丁添加了两个新的配置选项:key.subject.name.strategy(用于定义如何构造消息键的 subject 名称)和 value.subject.name.strategy(用于定义如何构造消息值的 subject 名称)。它们的值可以是如下几个:

  • io.confluent.kafka.serializers.subject.TopicNameStrategy(默认):消息键的 subject 名称为-key,消息值为-value。这意味着主题中所有消息的模式必须相互兼容。
  • io.confluent.kafka.serializers.subject.RecordNameStrategy:subject 名称是 Avro 记录类型的完全限定名。因此,模式注册表会检查特定记录类型的兼容性,而不管是哪个主题。这个设置允许同一主题包含不同类型的事件。
  • io.confluent.kafka.serializers.subject.TopicRecordNameStrategy:subject 名称是-,其中是 Kafka 主题名,是 Avro 记录类型的完全限定名。这个设置允许同一主题包含不同类型的事件,并进一步对当前主题进行兼容性检查。

有了这个新特性,你就可以轻松地将属于特定实体的所有不同类型的事件放在同一个主题中。现在,你可以自由选择主题的粒度,而不仅限于一个类型对应一个主题。

英文原文: http://martin.kleppmann.com/2018/01/18/event-types-in-kafka-topic.html

感谢蔡芳芳对本文的审校。

2018 年 8 月 18 日 17:554384
用户头像

发布了 731 篇内容, 共 370.2 次阅读, 收获喜欢 1865 次。

关注

评论

发布
暂无评论
发现更多内容

TRX波场智能合约APP系统开发|TRX波场智能合约软件开发

开發I852946OIIO

系统开发

Nginx做web服务器linux内核参数优化

赖猫

c++ Linux 编程 后台开发 内核

阿里P8大牛的建议,工作1-5年的Java工程师如何让自己变得更值钱

Java成神之路

Java 程序员 架构 面试 编程语言

金九银十Android热点知识!架构师花费近一年时间整理出来的安卓核心知识,送大厂面经一份!

欢喜学安卓

程序员 面试 编程开发 Android进阶 Android开发

你敢信?就是这个Netty的网络框架差点把我整疯了,哭jj

996小迁

Java 架构 面试 Netty 面试程序人生

安卓开发不得不会!啃下这些Framework技术笔记,成功入职阿里

欢喜学安卓

android 程序员 编程开发 Android进阶

Alibaba Java面试题大揭秘,把这些知识点吃透去面试成功率高达100%

Java架构之路

Java 程序员 架构 面试 编程语言

Swift性能优化分析

ios swift

3种缓存读写策略都不了解?面试很难让你通过啊兄弟!

Java架构师迁哥

二层交换机和三层交换机之间VLAN的区别

刷完这两份pdf轻松拿下了蚂蚁金服、头条、小米等大厂的offer。

Java架构之路

Java 程序员 架构 面试 编程语言

如何通过易观Argo,免费快速搭起精细化运营平台?

易观大数据

iOS逆向学习(arm64汇编入门)

ios 汇编

ChaosBlade x SkyWalking 微服务高可用实践

阿里巴巴云原生

微服务 云原生 监控 混沌工程 CloudNative

不为人知的网络编程(十):深入操作系统,从内核理解网络包的接收过程(Linux篇)

JackJiang

Linux 网络编程 TCP/IP

足不出户带你体验专业实验室,技术实现不在话下

华为云开发者社区

体验 平台 实验

mybatis快速入门,so easy啦

田维常

mybatis

《算法问题整理》​.pdf

田维常

浅谈ToB市场精细化运营

Linkflow

营销数字化 客户数据平台 CDP 精细化运营

可以秒杀全场的SpringCloud微服务电商实战项目,文档贼全

Java成神之路

Java 程序员 架构 面试 编程语言

性能之巅:定位和优化程序CPU、内存、IO瓶颈

华为云开发者社区

性能 cpu 优化

一个优秀的程序员,不仅要会编写程序,更要会编写高质量的程序

Java成神之路

Java 程序员 架构 面试 编程语言

架构师训练营 1 期 -- 第十二周总结

曾彪彪

极客大学架构师训练营

Kubernetes 1.20发布,Release Logo变成一只猫

晓川

大厂iOS面试题全面讲解(上)

ios 面试

架构师训练营第一期 - 第十一周学习总结

卖猪肉的大叔

极客大学架构师训练营

六度空间软件系统开发|六度空间APP开发

开發I852946OIIO

系统开发

MySQL最全整理(面试题+笔记+导图),面试大厂不再被MySql难倒

Java成神之路

Java 程序员 架构 面试 编程语言

GitHub标星17K,超火的SpringBoot +Vue实战项目,文档视频贼全

Java成神之路

Java 程序员 架构 面试 编程语言

linux命令:查询占用端口文件所在位置

梁小航航

Linux

在阿里巴巴内网找到的一份《Java核心宝典》简直太细了,如获至宝!

Java架构之路

Java 程序员 架构 面试 编程语言

边缘计算隔离技术的挑战与实践

边缘计算隔离技术的挑战与实践

Kafka实践:到底该不该把不同类型的消息放在同一个主题中-InfoQ