Skip to content

Commit 4321d7a

Browse files
committed
kafka 入门看这一篇就够了
1 parent 87e52b9 commit 4321d7a

File tree

4 files changed

+232
-0
lines changed

4 files changed

+232
-0
lines changed

docs/system-design/data-communication/Kafka入门看这一篇就够了.md

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
>
2+
>
13
> 本文由 JavaGuide 读者推荐,JavaGuide 对文章进行了整理排版!原文地址:https://www.wmyskxz.com/2019/07/17/kafka-ru-men-jiu-zhe-yi-pian/ , 作者:我没有三颗心脏。
24
35
# 一、Kafka 简介
@@ -79,7 +81,237 @@ Kafka 的一个关键性质是日志保留(retention),我们可以配置
7981

8082
![主题(Topic)与分区(Partition)](./../../../media/pictures/kafaka/kafka存在文件系统上.png)
8183

84+
任何发布到 Partition 的消息都会被追加到 Partition 数据文件的尾部,这样的顺序写磁盘操作让 Kafka 的效率非常高(经验证,顺序写磁盘效率比随机写内存还要高,这是 Kafka 高吞吐率的一个很重要的保证)。
8285

86+
每一条消息被发送到 Broker 中,会根据 Partition 规则选择被存储到哪一个 Partition。如果 Partition 规则设置的合理,所有消息可以均匀分布到不同的 Partition中。
8387

88+
## 讨论二:Kafka 中的底层存储设计
89+
90+
假设我们现在 Kafka 集群只有一个 Broker,我们创建 2 个 Topic 名称分别为:「topic1」和「topic2」,Partition 数量分别为 1、2,那么我们的根目录下就会创建如下三个文件夹:
91+
92+
```shell
93+
| --topic1-0
94+
| --topic2-0
95+
| --topic2-1
96+
```
97+
98+
在 Kafka 的文件存储中,同一个 Topic 下有多个不同的 Partition,每个 Partition 都为一个目录,而每一个目录又被平均分配成多个大小相等的 **Segment File** 中,Segment File 又由 index file 和 data file 组成,他们总是成对出现,后缀 “.index” 和 “.log” 分表表示 Segment 索引文件和数据文件。
99+
100+
现在假设我们设置每个 Segment 大小为 500 MB,并启动生产者向 topic1 中写入大量数据,topic1-0 文件夹中就会产生类似如下的一些文件:
101+
102+
```shell
103+
| --topic1-0
104+
| --00000000000000000000.index
105+
| --00000000000000000000.log
106+
| --00000000000000368769.index
107+
| --00000000000000368769.log
108+
| --00000000000000737337.index
109+
| --00000000000000737337.log
110+
| --00000000000001105814.index | --00000000000001105814.log
111+
| --topic2-0
112+
| --topic2-1
113+
114+
```
115+
116+
**Segment 是 Kafka 文件存储的最小单位。**Segment 文件命名规则:Partition 全局的第一个 Segment 从 0 开始,后续每个 Segment 文件名为上一个 Segment 文件最后一条消息的 offset 值。数值最大为 64 位 long 大小,19 位数字字符长度,没有数字用0填充。如 00000000000000368769.index 和 00000000000000368769.log。
117+
118+
以上面的一对 Segment File 为例,说明一下索引文件和数据文件对应关系:
119+
120+
![索引文件和数据文件](./../../../media/pictures/kafaka/segment是kafka文件存储的最小单位.png)
121+
122+
123+
124+
其中以索引文件中元数据 `<3, 497>` 为例,依次在数据文件中表示第 3 个 message(在全局 Partition 表示第 368769 + 3 = 368772 个 message)以及该消息的物理偏移地址为 497。
125+
126+
注意该 index 文件并不是从0开始,也不是每次递增1的,这是因为 Kafka 采取稀疏索引存储的方式,每隔一定字节的数据建立一条索引,它减少了索引文件大小,使得能够把 index 映射到内存,降低了查询时的磁盘 IO 开销,同时也并没有给查询带来太多的时间消耗。
127+
128+
因为其文件名为上一个 Segment 最后一条消息的 offset ,所以当需要查找一个指定 offset 的 message 时,通过在所有 segment 的文件名中进行二分查找就能找到它归属的 segment ,再在其 index 文件中找到其对应到文件上的物理位置,就能拿出该 message 。
129+
130+
由于消息在 Partition 的 Segment 数据文件中是顺序读写的,且消息消费后不会删除(删除策略是针对过期的 Segment 文件),这种顺序磁盘 IO 存储设计师 Kafka 高性能很重要的原因。
131+
132+
> Kafka 是如何准确的知道 message 的偏移的呢?这是因为在 Kafka 定义了标准的数据存储结构,在 Partition 中的每一条 message 都包含了以下三个属性:
133+
>
134+
> - offset:表示 message 在当前 Partition 中的偏移量,是一个逻辑上的值,唯一确定了 Partition 中的一条 message,可以简单的认为是一个 id;
135+
> - MessageSize:表示 message 内容 data 的大小;
136+
> - data:message 的具体内容
137+
138+
## 讨论三:生产者设计概要
139+
140+
当我们发送消息之前,先问几个问题:每条消息都是很关键且不能容忍丢失么?偶尔重复消息可以么?我们关注的是消息延迟还是写入消息的吞吐量?
141+
142+
举个例子,有一个信用卡交易处理系统,当交易发生时会发送一条消息到 Kafka,另一个服务来读取消息并根据规则引擎来检查交易是否通过,将结果通过 Kafka 返回。对于这样的业务,消息既不能丢失也不能重复,由于交易量大因此吞吐量需要尽可能大,延迟可以稍微高一点。
143+
144+
再举个例子,假如我们需要收集用户在网页上的点击数据,对于这样的场景,少量消息丢失或者重复是可以容忍的,延迟多大都不重要只要不影响用户体验,吞吐则根据实时用户数来决定。
145+
146+
不同的业务需要使用不同的写入方式和配置。具体的方式我们在这里不做讨论,现在先看下生产者写消息的基本流程:
147+
148+
![生产者设计概要](./../../../media/pictures/kafaka/生产者设计概要.png)
149+
150+
图片来源:[http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/12/kafka-producer.html](http://www.dengshenyu.com/分布式系统/2017/11/12/kafka-producer.html)
151+
152+
流程如下:
153+
154+
1. 首先,我们需要创建一个ProducerRecord,这个对象需要包含消息的主题(topic)和值(value),可以选择性指定一个键值(key)或者分区(partition)。
155+
2. 发送消息时,生产者会对键值和值序列化成字节数组,然后发送到分配器(partitioner)。
156+
3. 如果我们指定了分区,那么分配器返回该分区即可;否则,分配器将会基于键值来选择一个分区并返回。
157+
4. 选择完分区后,生产者知道了消息所属的主题和分区,它将这条记录添加到相同主题和分区的批量消息中,另一个线程负责发送这些批量消息到对应的Kafka broker。
158+
5. 当broker接收到消息后,如果成功写入则返回一个包含消息的主题、分区及位移的RecordMetadata对象,否则返回异常。
159+
6. 生产者接收到结果后,对于异常可能会进行重试。
160+
161+
162+
163+
## 讨论四:消费者设计概要
164+
165+
### 消费者与消费组
166+
167+
假设这么个场景:我们从Kafka中读取消息,并且进行检查,最后产生结果数据。我们可以创建一个消费者实例去做这件事情,但如果生产者写入消息的速度比消费者读取的速度快怎么办呢?这样随着时间增长,消息堆积越来越严重。对于这种场景,我们需要增加多个消费者来进行水平扩展。
168+
169+
Kafka消费者是**消费组**的一部分,当多个消费者形成一个消费组来消费主题时,每个消费者会收到不同分区的消息。假设有一个T1主题,该主题有4个分区;同时我们有一个消费组G1,这个消费组只有一个消费者C1。那么消费者C1将会收到这4个分区的消息,如下所示:
170+
171+
![生产者设计概要](./../../../media/pictures/kafaka/消费者设计概要1.png)
172+
如果我们增加新的消费者C2到消费组G1,那么每个消费者将会分别收到两个分区的消息,如下所示:
173+
174+
![生产者设计概要](./../../../media/pictures/kafaka/消费者设计概要2.png)
175+
176+
如果增加到4个消费者,那么每个消费者将会分别收到一个分区的消息,如下所示:
177+
178+
![生产者设计概要](./../../../media/pictures/kafaka/消费者设计概要3.png)
179+
180+
但如果我们继续增加消费者到这个消费组,剩余的消费者将会空闲,不会收到任何消息:
181+
182+
![生产者设计概要](./../../../media/pictures/kafaka/消费者设计概要4.png)
183+
184+
总而言之,我们可以通过增加消费组的消费者来进行水平扩展提升消费能力。这也是为什么建议创建主题时使用比较多的分区数,这样可以在消费负载高的情况下增加消费者来提升性能。另外,消费者的数量不应该比分区数多,因为多出来的消费者是空闲的,没有任何帮助。
185+
186+
**Kafka一个很重要的特性就是,只需写入一次消息,可以支持任意多的应用读取这个消息。**换句话说,每个应用都可以读到全量的消息。为了使得每个应用都能读到全量消息,应用需要有不同的消费组。对于上面的例子,假如我们新增了一个新的消费组G2,而这个消费组有两个消费者,那么会是这样的:
187+
188+
![生产者设计概要](./../../../media/pictures/kafaka/消费者设计概要5.png)
189+
190+
在这个场景中,消费组G1和消费组G2都能收到T1主题的全量消息,在逻辑意义上来说它们属于不同的应用。
191+
192+
最后,总结起来就是:如果应用需要读取全量消息,那么请为该应用设置一个消费组;如果该应用消费能力不足,那么可以考虑在这个消费组里增加消费者。
193+
194+
### 消费组与分区重平衡
195+
196+
可以看到,当新的消费者加入消费组,它会消费一个或多个分区,而这些分区之前是由其他消费者负责的;另外,当消费者离开消费组(比如重启、宕机等)时,它所消费的分区会分配给其他分区。这种现象称为**重平衡(rebalance)**。重平衡是 Kafka 一个很重要的性质,这个性质保证了高可用和水平扩展。**不过也需要注意到,在重平衡期间,所有消费者都不能消费消息,因此会造成整个消费组短暂的不可用。**而且,将分区进行重平衡也会导致原来的消费者状态过期,从而导致消费者需要重新更新状态,这段期间也会降低消费性能。后面我们会讨论如何安全的进行重平衡以及如何尽可能避免。
197+
198+
消费者通过定期发送心跳(hearbeat)到一个作为组协调者(group coordinator)的 broker 来保持在消费组内存活。这个 broker 不是固定的,每个消费组都可能不同。当消费者拉取消息或者提交时,便会发送心跳。
199+
200+
如果消费者超过一定时间没有发送心跳,那么它的会话(session)就会过期,组协调者会认为该消费者已经宕机,然后触发重平衡。可以看到,从消费者宕机到会话过期是有一定时间的,这段时间内该消费者的分区都不能进行消息消费;通常情况下,我们可以进行优雅关闭,这样消费者会发送离开的消息到组协调者,这样组协调者可以立即进行重平衡而不需要等待会话过期。
201+
202+
在 0.10.1 版本,Kafka 对心跳机制进行了修改,将发送心跳与拉取消息进行分离,这样使得发送心跳的频率不受拉取的频率影响。另外更高版本的 Kafka 支持配置一个消费者多长时间不拉取消息但仍然保持存活,这个配置可以避免活锁(livelock)。活锁,是指应用没有故障但是由于某些原因不能进一步消费。
203+
204+
### Partition 与消费模型
205+
206+
上面提到,Kafka 中一个 topic 中的消息是被打散分配在多个 Partition(分区) 中存储的, Consumer Group 在消费时需要从不同的 Partition 获取消息,那最终如何重建出 Topic 中消息的顺序呢?
207+
208+
答案是:没有办法。Kafka 只会保证在 Partition 内消息是有序的,而不管全局的情况。
209+
210+
下一个问题是:Partition 中的消息可以被(不同的 Consumer Group)多次消费,那 Partition中被消费的消息是何时删除的? Partition 又是如何知道一个 Consumer Group 当前消费的位置呢?
211+
212+
无论消息是否被消费,除非消息到期 Partition 从不删除消息。例如设置保留时间为 2 天,则消息发布 2 天内任何 Group 都可以消费,2 天后,消息自动被删除。
213+
Partition 会为每个 Consumer Group 保存一个偏移量,记录 Group 消费到的位置。 如下图:
214+
![生产者设计概要](./../../../media/pictures/kafaka/Partition与消费模型.png)
215+
216+
217+
218+
219+
### 为什么 Kafka 是 pull 模型
220+
221+
消费者应该向 Broker 要数据(pull)还是 Broker 向消费者推送数据(push)?作为一个消息系统,Kafka 遵循了传统的方式,选择由 Producer 向 broker push 消息并由 Consumer 从 broker pull 消息。一些 logging-centric system,比如 Facebook 的[Scribe](https://github.com/facebookarchive/scribe)和 Cloudera 的[Flume](https://flume.apache.org/),采用 push 模式。事实上,push 模式和 pull 模式各有优劣。
222+
223+
**push 模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的。**push 模式的目标是尽可能以最快速度传递消息,但是这样很容易造成 Consumer 来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。**而 pull 模式则可以根据 Consumer 的消费能力以适当的速率消费消息。**
224+
225+
**对于 Kafka 而言,pull 模式更合适。**pull 模式可简化 broker 的设计,Consumer 可自主控制消费消息的速率,同时 Consumer 可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。
226+
227+
## 讨论五:Kafka 如何保证可靠性
228+
229+
当我们讨论**可靠性**的时候,我们总会提到*保证**这个词语。可靠性保证是基础,我们基于这些基础之上构建我们的应用。比如关系型数据库的可靠性保证是ACID,也就是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
230+
231+
Kafka 中的可靠性保证有如下四点:
232+
233+
- 对于一个分区来说,它的消息是有序的。如果一个生产者向一个分区先写入消息A,然后写入消息B,那么消费者会先读取消息A再读取消息B。
234+
- 当消息写入所有in-sync状态的副本后,消息才会认为**已提交(committed)**。这里的写入有可能只是写入到文件系统的缓存,不一定刷新到磁盘。生产者可以等待不同时机的确认,比如等待分区主副本写入即返回,后者等待所有in-sync状态副本写入才返回。
235+
- 一旦消息已提交,那么只要有一个副本存活,数据不会丢失。
236+
- 消费者只能读取到已提交的消息。
237+
238+
使用这些基础保证,我们构建一个可靠的系统,这时候需要考虑一个问题:究竟我们的应用需要多大程度的可靠性?可靠性不是无偿的,它与系统可用性、吞吐量、延迟和硬件价格息息相关,得此失彼。因此,我们往往需要做权衡,一味的追求可靠性并不实际。
239+
240+
> 想了解更多戳这里:http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/21/kafka-data-delivery.html
241+
242+
243+
三、动手搭一个 Kafka
244+
245+
通过上面的描述,我们已经大致了解到了「Kafka」是何方神圣了,现在我们开始尝试自己动手本地搭一个来实际体验一把。
246+
247+
## 第一步:下载 Kafka
248+
249+
这里以 Mac OS 为例,在安装了 Homebrew 的情况下执行下列代码:
250+
251+
```shell
252+
brew install kafka
253+
```
254+
255+
由于 Kafka 依赖了 Zookeeper,所以在下载的时候会自动下载。
256+
257+
## 第二步:启动服务
258+
259+
我们在启动之前首先需要修改 Kafka 的监听地址和端口为 `localhost:9092`
260+
261+
```shell
262+
vi /usr/local/etc/kafka/server.properties
263+
```
264+
265+
266+
然后修改成下图的样子:
267+
268+
![启动服务](./../../../media/pictures/kafaka/启动服务.png)
269+
依次启动 Zookeeper 和 Kafka:
270+
271+
```shell
272+
brew services start zookeeper
273+
brew services start kafka
274+
```
275+
276+
然后执行下列语句来创建一个名字为 “test” 的 Topic:
277+
278+
```shell
279+
kafka-topics --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
280+
```
281+
282+
我们可以通过下列的命令查看我们的 Topic 列表:
283+
284+
```shell
285+
kafka-topics --list --zookeeper localhost:2181
286+
```
287+
288+
## 第三步:发送消息
289+
290+
然后我们新建一个控制台,运行下列命令创建一个消费者关注刚才创建的 Topic:
291+
292+
```shell
293+
kafka-console-consumer --bootstrap-server localhost:9092 --topic test --from-beginning
294+
```
295+
296+
用控制台往刚才创建的 Topic 中添加消息,并观察刚才创建的消费者窗口:
297+
298+
```shel
299+
kafka-console-producer --broker-list localhost:9092 --topic test
300+
```
301+
302+
能通过消费者窗口观察到正确的消息:
303+
304+
![发送消息](./../../../media/pictures/kafaka/发送消息.png)
305+
306+
# 参考资料
307+
308+
------
309+
310+
1. https://www.infoq.cn/article/kafka-analysis-part-1 - Kafka 设计解析(一):Kafka 背景及架构介绍
311+
2. [http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/06/kafka-Meet-Kafka.html](http://www.dengshenyu.com/分布式系统/2017/11/06/kafka-Meet-Kafka.html) - Kafka系列(一)初识Kafka
312+
3. https://lotabout.me/2018/kafka-introduction/ - Kafka 入门介绍
313+
4. https://www.zhihu.com/question/28925721 - Kafka 中的 Topic 为什么要进行分区? - 知乎
314+
5. https://blog.joway.io/posts/kafka-design-practice/ - Kafka 的设计与实践思考
315+
6. [http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/21/kafka-data-delivery.html](http://www.dengshenyu.com/分布式系统/2017/11/21/kafka-data-delivery.html) - Kafka系列(六)可靠的数据传输
84316

85317

Loading
135 KB
Loading
496 KB
Loading

0 commit comments

Comments
 (0)