【Kafka】消息队列学习(二):Topic、Partition与Offset,消息到底存在哪里

一、Topic只是消息的逻辑分类

上一篇介绍 Kafka 时,可以先把整个流程理解成:

复制代码
Producer
   ↓
发送消息
   ↓
Topic
   ↓
Kafka
   ↓
Consumer

例如一个系统中可能存在:

复制代码
order_topic    → 订单消息

payment_topic  → 支付消息

log_topic      → 日志消息

订单系统产生的消息发送到:

复制代码
order_topic

日志系统产生的消息发送到:

复制代码
log_topic

所以刚开始学习时,可以先把 Topic 理解成:

Kafka 对不同类型消息进行分类的逻辑容器。

例如:

复制代码
                    Kafka

            ┌─────────┼─────────┐
            ↓         ↓         ↓

       order_topic payment_topic log_topic

但是这里很容易产生一个误解:

复制代码
Topic = 真正保存消息的地方

实际上并不是。

Kafka 中真正保存消息的是:

复制代码
Partition

一个 Topic 内部通常可以包含多个 Partition。

例如:

复制代码
order_topic

├── Partition 0
├── Partition 1
└── Partition 2

所以真正的消息存储过程应该进一步理解成:

复制代码
Producer
   ↓
发送消息
   ↓
Topic
   ↓
选择Partition
   ↓
消息写入Partition

例如 Producer 连续发送:

复制代码
order1
order2
order3
order4
order5

这些消息可能最终分布成:

复制代码
order_topic

Partition 0:

order1 → order4


Partition 1:

order2 → order5


Partition 2:

order3

所以:

复制代码
Topic
   ↓
负责分类

Partition
   ↓
真正保存消息

理解这一点,是后面继续学习 Kafka 的基础。

二、Partition到底是什么

Partition 中文一般称为:

复制代码
分区

可以把一个 Partition 理解成:

一条不断向后追加消息的有序日志。

例如:

复制代码
Partition 0

msg0 → msg1 → msg2 → msg3 → msg4

当 Producer 再发送一条新的消息:

复制代码
msg5

Kafka 不会把它插入到中间,而是继续追加到末尾:

复制代码
msg0 → msg1 → msg2 → msg3 → msg4 → msg5

这种写入方式可以理解成:

复制代码
已有数据
   ↓
不断向后追加
   ↓
新的消息写到末尾

假设一个 Topic 只有一个 Partition:

复制代码
order_topic

└── Partition 0
       ↓
   order1
       ↓
   order2
       ↓
   order3
       ↓
   order4

那么所有订单消息都需要写入:

复制代码
Partition 0

消息比较少时没有什么问题。

但如果现在系统每秒产生:

复制代码
100000条消息

所有数据都写到一个 Partition:

复制代码
10万条消息
     ↓
Partition 0

这个 Partition 就有可能成为性能瓶颈。

Kafka 的解决方法就是:

复制代码
把一个Topic拆成多个Partition

例如:

复制代码
                    order_topic

            ┌──────────┼──────────┐
            ↓          ↓          ↓

       Partition 0 Partition 1 Partition 2

消息可以分别写入:

复制代码
Partition 0:

order1 → order4 → order7

Partition 1:

order2 → order5 → order8

Partition 2:

order3 → order6 → order9

这样不同 Partition 就可以进行:

复制代码
并行写入
+
并行读取

例如:

复制代码
                   Producer
                       ↓
                  order_topic
                       ↓
          ┌────────────┼────────────┐
          ↓            ↓            ↓
     Partition 0  Partition 1  Partition 2
          ↓            ↓            ↓
       同时处理      同时处理       同时处理

因此 Kafka 使用多个 Partition 的一个重要目的就是:

提高消息的并行处理能力和系统吞吐量。

所以可以先简单记住:

复制代码
一个Topic
    ↓
可以有多个Partition
    ↓
不同Partition可以并行处理
    ↓
提高Kafka吞吐量

三、Offset表示消息在Partition中的位置

消息写入 Partition 以后,还需要解决一个问题:

复制代码
怎样知道一条消息在Partition中的什么位置?

Kafka 为 Partition 中的每条消息分配了一个:

复制代码
Offset

中文一般叫:

复制代码
偏移量

例如:

复制代码
Partition 0

Offset 0 → msgA
Offset 1 → msgB
Offset 2 → msgC
Offset 3 → msgD
Offset 4 → msgE

如果 Producer 再发送:

复制代码
msgF

那么继续追加:

复制代码
Offset 5 → msgF

于是整个 Partition 可以理解成:

复制代码
Offset:

  0      1      2      3      4      5
  ↓      ↓      ↓      ↓      ↓      ↓

msgA → msgB → msgC → msgD → msgE → msgF

所以 Offset 可以先简单理解成:

一条消息在某个 Partition 中的位置编号。

不过这里有一个非常重要的点:

Offset 是属于 Partition 的,不是属于整个 Topic 的。

例如:

复制代码
order_topic

有三个 Partition:

复制代码
Partition 0
Partition 1
Partition 2

那么每个 Partition 都有自己独立的 Offset。

例如:

复制代码
Partition 0

Offset 0 → orderA
Offset 1 → orderD
Offset 2 → orderG

同时:

复制代码
Partition 1

Offset 0 → orderB
Offset 1 → orderE
Offset 2 → orderH

另外:

复制代码
Partition 2

Offset 0 → orderC
Offset 1 → orderF
Offset 2 → orderI

所以三个 Partition 中都可以存在:

复制代码
Offset 0

但是它们表示的不是同一条消息。

因此如果想准确描述 Kafka 中的一条消息,可以理解成需要三个信息:

复制代码
Topic
+
Partition
+
Offset

例如:

复制代码
Topic     = order_topic

Partition = 1

Offset    = 3

表示:

复制代码
order_topic
      ↓
Partition 1
      ↓
Offset 3

这三个信息组合起来,才能比较准确地描述一条消息的位置。

Offset 对 Consumer 同样非常重要。

假设 Consumer 已经消费:

复制代码
Offset 0
Offset 1
Offset 2

那么下一次就可以继续读取:

复制代码
Offset 3

可以简单理解成:

复制代码
Consumer
    ↓
消费消息
    ↓
记录当前Offset
    ↓
下次继续消费

所以 Offset 不仅表示:

复制代码
消息存在哪里

后面还会和:

复制代码
Consumer消费到哪里了

有很大的关系。

四、为什么Kafka只能保证Partition内部有序

理解 Partition 后,就可以理解 Kafka 一个很重要的特点:

Kafka 可以保证同一个 Partition 内部的消息顺序,但是不能天然保证多个 Partition 之间的全局顺序。

例如一个 Partition:

复制代码
Partition 0

A → B → C → D

写入顺序:

复制代码
A
B
C
D

读取时也可以按照:

复制代码
A
B
C
D

进行读取。

所以:

复制代码
同一个Partition内部

消息是有顺序的。

但是现在如果有三个 Partition:

复制代码
Partition 0:

A → D

Partition 1:

B → E

Partition 2:

C → F

Producer 原来发送的顺序可能是:

复制代码
A → B → C → D → E → F

但是因为:

复制代码
Partition 0
Partition 1
Partition 2

可以并行处理,所以 Consumer 最终处理消息的时间可能不同。

例如可能出现:

复制代码
A
B
D
C
E
F

Kafka 能保证的是:

复制代码
Partition 0:

A一定在D之前

以及:

复制代码
Partition 1:

B一定在E之前

还有:

复制代码
Partition 2:

C一定在F之前

但是 Kafka 不直接保证:

复制代码
A → B → C → D → E → F

这种跨 Partition 的全局顺序。

那么如果有些消息必须保证顺序怎么办?

例如同一个订单可能产生:

复制代码
订单创建
   ↓
订单支付
   ↓
订单完成

如果三个消息分别进入:

复制代码
Partition 0
Partition 1
Partition 2

就有可能出现:

复制代码
支付消息先被处理
创建消息后被处理

这样的顺序问题。

一种常见方式就是:

让相关消息进入同一个 Partition。

Producer 发送消息时可以指定:

复制代码
Key

例如使用:

复制代码
orderId

作为 Key。

假设:

复制代码
orderId = 10001

那么这个订单相关的:

复制代码
订单创建
订单支付
订单完成

都使用相同 Key:

复制代码
key = 10001

它们通常会被路由到同一个 Partition:

复制代码
orderId = 10001
       ↓
相同Key
       ↓
Partition 1
       ↓

创建 → 支付 → 完成

这样就可以利用:

复制代码
Partition内部有序

保证同一个业务对象相关消息的顺序。

所以多个 Partition 实际上是在:

复制代码
并发能力

和:

复制代码
全局顺序

之间进行了一定取舍。

只有一个 Partition:

复制代码
顺序更加容易保证
但是并行能力有限

多个 Partition:

复制代码
并行处理能力更强
但是只能保证Partition内部有序

这也是 Kafka Partition 设计中非常重要的一点。

五、通过命令实际观察Topic和Partition

理解概念以后,可以通过 Kafka 命令实际创建一个带多个 Partition 的 Topic。

例如创建:

复制代码
order_topic

并设置三个 Partition:

复制代码
kafka-topics.sh --create --topic order_topic --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1

其中:

复制代码
--topic order_topic

表示:

复制代码
创建名为order_topic的Topic

而:

复制代码
--partitions 3

表示:

复制代码
创建3个Partition

最终结构:

复制代码
order_topic

├── Partition 0
├── Partition 1
└── Partition 2

另外:

复制代码
--replication-factor 1

表示:

复制代码
每个Partition目前只有1份数据

这个参数会涉及后面要学习的:

复制代码
Replica
Leader
Follower
ISR

这里暂时先不展开。

创建完成后,可以查看 Topic 的详细信息:

复制代码
kafka-topics.sh --describe --topic order_topic --bootstrap-server localhost:9092

可以看到类似:

复制代码
Topic: order_topic
PartitionCount: 3
ReplicationFactor: 1

以及不同 Partition:

复制代码
Partition: 0
Partition: 1
Partition: 2

接下来启动 Producer:

复制代码
kafka-console-producer.sh --bootstrap-server localhost:9092 --topic order_topic

然后输入:

复制代码
order1
order2
order3
order4

这些消息发送给:

复制代码
order_topic

以后,Kafka 会进一步决定:

复制代码
到底写入哪个Partition

所以一条消息真正进入 Kafka 后的流程可以整理成:

复制代码
Producer
    ↓
产生Message
    ↓
指定Topic
    ↓
order_topic
    ↓
选择Partition
    ↓
Partition 0 / 1 / 2
    ↓
追加到Partition末尾
    ↓
获得对应Offset

例如最终某条消息可能位于:

复制代码
Topic:

order_topic

Partition:

1

Offset:

5

所以相比上一篇:

复制代码
Producer
   ↓
Topic
   ↓
Kafka
   ↓
Consumer

现在已经可以把 Kafka 内部结构进一步展开:

复制代码
                       Producer
                           ↓
                        Message
                           ↓
                         Topic
                           ↓
               ┌───────────┼───────────┐
               ↓           ↓           ↓
          Partition 0 Partition 1 Partition 2
               ↓           ↓           ↓
             Offset      Offset      Offset

这一篇最重要的三个概念可以总结成:

复制代码
Topic
   ↓
负责消息的逻辑分类

Partition
   ↓
真正存储消息的数据分区

Offset
   ↓
消息在某个Partition中的位置

而 Kafka 将一个 Topic 拆成多个 Partition,一个非常重要的目的就是:

复制代码
数据拆分
   ↓
多个Partition
   ↓
并行读写
   ↓
提高吞吐量

但是多个 Partition 同时也意味着:

复制代码
只能保证Partition内部有序

如果业务消息存在严格的先后关系,则通常需要通过:

复制代码
相同Key
   ↓
进入同一个Partition

来保证相关消息的顺序。

理解了:

复制代码
Topic
+
Partition
+
Offset

之后,下一个问题就很自然了:

复制代码
Producer发送一条消息以后,
Kafka到底怎么决定它进入哪个Partition?

以及:

复制代码
多个Consumer同时存在时,
不同Partition又应该由谁负责消费?

这就会引出 Kafka 中另外几个非常重要的概念:

复制代码
Producer分区策略
+
Consumer
+
Consumer Group

0voice · GitHub

相关推荐
彧azz1 小时前
Git 学习指南:从入门到进阶的完整实践手册
git·学习
彧azz10 小时前
图的存储结构详解:邻接矩阵的原理、实现与应用
开发语言·数据结构·学习·php
平头哥AI10 小时前
Day 22 _ 包装错误别丢链_%w、errors.Is 与 errors.As
android·服务器·学习·golang·go
一阵寒风10 小时前
CAM智能化助力PCB 智能制造-培训体系第六章.项目开发学习
学习·制造
传奇开心果编程11 小时前
【Xilem 0.4 基础语法学与练】第15课:状态管理与 memoize 性能优化
学习·rust·前端框架
具身AGI11 小时前
视频即仿真,物理AI 人类学习路线 的下一步
人工智能·学习
wuyk55512 小时前
从零吃透 MQTT 通信|第 8 章 FreeRTOS 多任务架构下 MQTT 工程架构,任务拆分、队列解耦、临界区保护
c语言·开发语言·stm32·学习·架构
陈年老古董12 小时前
PyTorch食物图像分类实战:从数据集制作到CNN模型训练全流程详解
pytorch·深度学习·学习·机器学习·分类·cnn
明志数科13 小时前
从300克Ego头环看第一人称数据采集趋势:设备轻量化之后,场景端壁垒在哪
数码相机·学习