一、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