文章目录

一、前言

Kafka 为什么快?
Kafka 和其他消息队列的区别?
Kafka 这么快,它是如何保证不丢失消息?

二、问题1: Kafka 为什么这么快?

Kafka 的消息是保存或缓存在磁盘上的,一般认为在磁盘上读写数据是会降低性能的,因为寻址会比较消耗时间,但是实际上,Kafka 的特性之一就是高吞吐率。Kafka 之所以能这么快,无非是:「顺序写磁盘、大量使用内存页 、零拷贝技术的使用」…

下面我就从数据写入和读取两方面分析,为大家分析下为什么 Kafka 速度这么快。

对于数据持久化到磁盘中,
数据写入 Kafka 会把收到的消息都写入到硬盘中,它绝对不会丢失数据。为了优化写入速度 Kafka 采用了两个技术, 顺序写入和 Memory Mapped File 。

2.1 对于broker,写磁盘持久化:顺序写入

Kafka 会把收到的消息都写入到硬盘中,它绝对不会丢失数据。为了优化写入速度 Kafka 采用了两个技术, 顺序写入和 MMFile(Memory Mapped File)内存映射文件。

磁盘读写的快慢取决于你怎么使用它,也就是顺序读写或者随机读写。在顺序读写的情况下,磁盘的顺序读写速度和内存持平。因为硬盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”,它是最耗时的。「所以硬盘最讨厌随机 I/O,最喜欢顺序 I/O」。为了提高读写硬盘的速度,Kafka 就是使用顺序 I/O。

而且 Linux 对于磁盘的读写优化也比较多,包括 read-ahead 和 write-behind,磁盘缓存等。

如果在内存做这些操作的时候,一个是 Java 对象的内存开销很大,另一个是随着堆内存数据的增多,Java 的 GC 时间会变得很长。
使用磁盘操作有以下几个好处:
(1)磁盘顺序读写速度超过内存随机读写。
(2)JVM 的 GC 效率低,内存占用大。使用磁盘可以避免这一问题。
(3)系统冷启动后,磁盘缓存依然可用。

下图就展示了 Kafka 是如何写入数据的, 每一个 Partition 其实都是一个文件 ,收到消息后 Kafka 会把数据插入到文件末尾(虚框部分):

解开Kafka神秘的面纱_# (2)消息队列

对于这个图的解释,左边old表示是旧数据,右边new表示是新写入的数据,下面一个箭头很明显,这里表示一个topic下面的三个partition,不断的往里面写数据。

这种方法有一个缺陷——没有办法删除数据 ,所以 Kafka 是不会删除数据的,它会把所有的数据都保留下来,每个 消费者(Consumer)对每个 Topic 都有一个 Offset 用来表示读取到了第几条数据 。

解开Kafka神秘的面纱_# (2)消息队列_02

对于上图的解释:
对于producer,producer生产的数据不断增加,
对于Consumer消费kafka中的数据,提供一个offset,consumer消费的数据不会减少,只是移动offset,offset之前是已经被消费的,offset之后是没有被消费的。
注意,kafka不会删除数据,只是提供一个offset,这个offset可以用zookeeper管理,
特别注意,这个offset只对consumer消费有用,对producer生产没用。

一般情况下 Offset 由客户端 SDK 负责保存 ,会保存到 Zookeeper 里面 。

关于存在硬盘中的消息,Kafka 也有它的解决方法,可以基于时间和 Partition 文件的大小,正常 Kafka 是默认七天的保存,也可以通过命令来修改,以 users topic 为例。
修改kafka 7天 默认保存周期
kafka-topics.sh --zookeeper 6 --alter --topic users --config retention.ms=100000

上面说了,kafka不会删除数据,即使被消费掉也是移动offset,不删除数据,所有的数据都持久化到磁盘中,所以,为了避免磁盘被撑满的情况,Kakfa 提供了两种策略来删除数据:
「基于时间」 (默认七天)
「基于 Partition 文件大小」

2.2 对于broker,写磁盘持久化:Memory Mapped Files

这个和Java NIO中的内存映射基本相同,在大学的计算机原理里我们学过(划重点),mmf (Memory Mapped Files)直接利用操作系统的Page来实现文件到物理内存的映射,完成之后对物理内存的操作会直接同步到硬盘。mmf 通过内存映射的方式大大提高了IO速率,省去了用户空间到内核空间的复制。它的缺点显而易见–不可靠,当发生宕机而数据未同步到硬盘时,数据会丢失,Kafka 提供了produce.type参数来控制是否主动的进行刷新

Kafka 提供了一个参数 producer.type 来控制是不是主动 Flush:
如果 Kafka 写入到 mmf 之后就立即 Flush,然后再返回 Producer 叫同步 (Sync)
如果 Kafka 写入 mmf 之后立即返回 Producer 不调用 Flush 叫异步 (Async)。

2.3 对于broker,读磁盘:基于 Sendfile 实现零拷贝(Zero Copy)

作为一个消息系统,不可避免的便是消息的拷贝,常规的操作,一条消息,需要从创建者的socket到应用,再到操作系统内核,然后才能落盘。同样,一条消息发送给消费者也要从磁盘到内核到应用再到接收者的socket,中间经过了多次不是很有必要的拷贝。

传统 Read 方式进行网络文件传输,在传输过程中,文件数据实际上是经过了四次 Copy 操作,其具体流程细节如下:
(1)调用 Read 函数,文件数据被 Copy 到内核缓冲区。
(2)Read 函数返回,文件数据从内核缓冲区 Copy 到用户缓冲区
(3)将文件数据从用户缓冲区 Copy 到内核与 Socket 相关的缓冲区。
(4)数据从 Socket 缓冲区 Copy 到相关协议引擎。
传统Read方式(四次Copy操作):硬盘—>内核 buf—>用户 buf—>Socket buf—>协议引擎

Sendfile 系统调用则提供了一种减少以上多次 Copy,提升文件传输性能的方法。
在内核版本 2.1 中,引入了 Sendfile 系统调用,以简化网络上和两个本地文件之间的数据传输。Sendfile 的引入不仅减少了数据复制,还减少了上下文切换。相较传统 Read/Write 方式,2.1 版本内核引进的 Sendfile 已经减少了内核缓冲区到 User 缓冲区,再由 User 缓冲区到 Socket 相关缓冲区的文件 Copy。减少两次copy。
而在内核版本 2.4 之后,文件描述符结果被改变,Sendfile 实现了更简单的方式,再次减少了一次 Copy 操作。减少了三次copy。

Kafka 把所有的消息都存放在一个一个的文件中,当消费者需要数据的时候 Kafka 直接把文件发送给消费者,配合 mmap 作为文件读写方式,直接把它传给 Sendfile。

传统Read方式(四次Copy操作):硬盘—>内核 buf—>用户 buf—>Socket buf—>协议引擎
SendFile 2.1,硬盘—>内核 buf—>协议引擎
SendFile 2.4,硬盘—>协议引擎

2.4 对于producer,攒一波,批量发送,同步发送+异步发送(两种方式)

producer发送消息两种方式:同步发送 + 异步发送(异步发送解释:Kafka允许进行批量发送消息,producter发送消息的时候,可以将消息缓存在本地,等到了固定条件发送到 Kafka ),同步发送就是马上发送,异步发送就攒一波再发送,那攒一波到什么使用发送呢?两种方式:
(1)等消息条数到固定条数。
(2)一段时间发送一次。

2.5 对于producer,数据压缩,大数据处理上,瓶颈在网络上而不是CPU

Kafka支持对消息集合进行压缩,Producer可以通过GZIP或Snappy格式对消息集合进行压缩。压缩的好处就是减少传输的数据量,减轻对网络传输的压力。

Producer压缩之后,在Consumer需进行解压,虽然增加了CPU的工作,但在对大数据处理上,瓶颈在网络上而不是CPU,所以这个成本很值得。

注意:「批量发送」和「数据压缩」一起使用,单条做数据压缩的话,效果不明显

金手指:数据压缩后发送的优点和缺点
1、优点:压缩包的大小一定比直接发送的大小要小,提高网络传输效率;
2、缺点:producer要压缩,consumer要解压,增加producer和consumer的CPU压力
小结:大数据处理上,瓶颈在网络上而不是CPU,只要优点可以覆盖缺点,就应该使用这种方式 good

2.6 小结以上五点

问题:为什么 Apache Kafka虽然使用了硬盘存储,但是仍然可以速度很快?
标准答案:原因五点:
1、对于producer:把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络 IO 损耗,
2、对于broker:写磁盘两招:通过 mmap 提高 I/O 速度;写入数据的时候由于单个 Partion 是末尾添加,所以速度最优;读磁盘一招:读取数据的时候配合 Sendfile 直接暴力输出。

写磁盘持久化的是broker,从磁盘中读取发送给consumer的,也是broker,producer和consumer只有内存操作。

三、关于 Kafka 和其他消息队列的区别(为了达到这个高吞吐量的目标)

Kafka的设计目标是高吞吐量,为了达到这个高吞吐量的目标,那它与其它消息队列的区别就显而易见了:

1、对于broker,顺序写磁盘,内存映射文件写磁盘:Kafka操作的是序列文件 I/O(序列文件的特征是按顺序写,按顺序读),为保证顺序,Kafka强制点对点的按顺序传递消息,这意味着,一个consumer在消息流(或分区)中只有一个位置。

2、对于broker,Kafka的消息存储在OS pagecache(页缓存,page cache的大小为一页,通常为4K,在Linux读写文件时,它用于缓存文件的逻辑内容,从而加快对磁盘上映像和数据的访问)。

3、对于broker-consumer,基于SendFile零拷贝读磁盘,并且,不保存消息状态(是否被消费),使用offset记录消费位置:Kafka不保存消息的状态,即消息是否被“消费”。一般的消息系统需要保存消息的状态,并且还需要以随机访问的形式更新消息的状态。而Kafka 的做法是保存Consumer在Topic分区中的位置offset,在offset之前的消息是已被“消费”的,在offset之后则为未“消费”的,并且offset是可以任意移动的,这样就消除了大部分的随机IO。

4、对于broker,批量发送、压缩后发送,kafka在producer端加速/高吞吐量的措施:Kafka支持点对点的批量消息传递。

RabbitMQ:分布式,支持多种MQ协议,重量级
ActiveMQ:与RabbitMQ类似
ZeroMQ:以库的形式提供,使用复杂,无持久化
Redis:单机、纯内存性好,持久化较差
Kafka:分布式,消息不是使用完就丢失【较长时间持久化】,吞吐量高【高性能】,轻量灵活

四、Kafka 如何保证消息队列不丢失?

问题:Kafka 为什么这么快,又能保证不丢失消息?

4.1 对于producer,攒一波批量发送消息,同步发送/异步发送

为了得到更好的性能,Kafka 支持在生产者一侧进行本地buffer,也就是累积到一定的条数才发送,如果这里设置不当是会丢消息的。

生产者端设置:producer.type=async, sync,默认是 sync。

当设置为 async,会大幅提升性能,因为生产者会在本地缓冲消息,并适时批量发送。

如果对可靠性要求高,那么这里可以设置为 sync 同步发送。

一般时候我们还需要设置:min.insync.replicas> 1 ,消息至少要被写入到这么多副本才算成功,也是提升数据持久性的一个参数,与acks配合使用。

但如果出现两者相等,我们还需要设置 replication.factor = min.insync.replicas + 1 ,避免在一个副本挂掉,整个分区无法工作的情况!

4.2 对于broker,正常运行时:kafka本身就是分布式,partition数据设置至少3个副本分区,保证存储在 broker/partition 中的消息不丢失

为了保证 leader 副本能有 follower 副本能同步消息,我们一般会为 topic 设置 replication.factor >= 3。这样就可以保证每个 分区(partition) 至少有 3 个副本,以确保消息队列的安全性。

4.3 对于broker,leader partition宕机时:关闭 unclean leader 选举,减低数据丢失可能性

我们最开始也说了我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。多个 follower 副本之间的消息同步情况不一样,当我们配置了 unclean.leader.election.enable = false 的话,当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。

4.4 对于broker-consumer,消费消息:关于 ACK 机制,添加 acks=all 这个条件,保证broker发送给consumer的消息不会被丢

Kafka 架构深入 ,通过 ACK 机制保证消息送达。Kafka 采用的是至少一次(At least once),消息不会丢,但是可能会重复传输。

acks 的默认值即为1,代表我们的消息被leader副本接收之后就算被成功发送。我们可以配置 acks = all ,代表则所有副本都要接收到该消息之后该消息才算真正成功被发送。

4.5 对于consumer,关闭自动提交位移,在消息被完整处理之后再手动提交位移。

consumer端丢失消息的情形比较简单:

如果在消息处理完成前就提交了offset,那么就有可能造成数据的丢失。

由于Kafka consumer默认是自动提交位移的,所以在后台提交位移前一定要保证消息被正常处理了,因此不建议采用很重的处理逻辑,如果处理耗时很长,则建议把逻辑放到另一个线程中去做。

为了避免数据丢失,现给出几点建议:设置 enable.auto.commit=false

关闭自动提交位移,在消息被完整处理之后再手动提交位移。

五、面试金手指

5.1 为什么kafka这么快?(从producer broker/partition consumer 三个方面来回答,拉高逼格)

从producer broker/partition consumer 三个方面来回答,拉高逼格

问题:为什么 Apache Kafka虽然使用了硬盘存储,但是仍然可以速度很快?
标准答案:原因五点:
1、对于producer:把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络 IO 损耗,
2、对于broker:写磁盘两招:通过 mmap 提高 I/O 速度;写入数据的时候由于单个 Partion 是末尾添加,所以速度最优;读磁盘一招:读取数据的时候配合 Sendfile 直接暴力输出。

写磁盘持久化的是broker,从磁盘中读取发送给consumer的,也是broker,producer和consumer只有内存操作。

重要的点:
第一,producer批量发送

producer发送消息两种方式:同步发送 + 异步发送(异步发送解释:Kafka允许进行批量发送消息,producter发送消息的时候,可以将消息缓存在本地,等到了固定条件发送到 Kafka ),同步发送就是马上发送,异步发送就攒一波再发送,那攒一波到什么使用发送呢?两种方式:
(1)等消息条数到固定条数。
(2)一段时间发送一次。
第二,producer压缩发送
金手指:数据压缩后发送的优点和缺点
1、优点:压缩包的大小一定比直接发送的大小要小,提高网络传输效率;
2、缺点:producer要压缩,consumer要解压,增加producer和consumer的CPU压力
小结:大数据处理上,瓶颈在网络上而不是CPU,只要优点可以覆盖缺点,就应该使用这种方式 good
第三,broker写磁盘,顺序写入(磁盘寻址 + 磁盘写入,磁盘寻址是机械运动,速度慢,去掉前者,磁盘写入和内存写入速度相差无几)
对于producer,producer生产的数据不断增加;
对于consumer,消费kafka中的数据,提供一个offset,consumer消费的数据不会减少,只是移动offset,offset之前是已经被消费的,offset之后是没有被消费的。
注意,kafka不会删除数据,只是提供一个offset,这个offset可以用zookeeper管理,
特别注意,这个offset只对consumer消费有用,对producer生产没用。
上面说了,kafka不会删除数据,即使被消费掉也是移动offset,不删除数据,所有的数据都持久化到磁盘中,所以,为了避免磁盘被撑满的情况,Kakfa 提供了两种策略来删除数据:
「基于时间」 (默认七天)
「基于 Partition 文件大小」
第四,broker写磁盘
优点:mmf (Memory Mapped Files)直接利用操作系统的Page来实现文件到物理内存的映射,完成之后对物理内存的操作会直接同步到硬盘。mmf 通过内存映射的方式大大提高了IO速率,省去了用户空间到内核空间的复制。
缺点:不可靠,当发生宕机而数据未同步到硬盘时,数据会丢失,Kafka 提供了produce.type参数来控制是否主动的进行刷新,如果 Kafka 写入到 mmf 后立即flush再返回给生产者则为同步模式,反之为异步模式。
第五,读磁盘,零复制
传统Read/Write方式(四次Copy操作):硬盘—>内核 buf—>用户 buf—>Socket 相关缓冲区—>协议引擎
基于 Sendfile 实现零拷贝(Zero Copy)

5.2 技术选型/技术对比:kafka为什么吞吐量大?kafka和其他消息队列的不同?(从producer broker/partition consumer 三个方面来回答,拉高逼格)

从producer broker/partition consumer 三个方面来回答,拉高逼格

Kafka的设计目标是高吞吐量,为了达到这个高吞吐量的目标,那它与其它消息队列的区别就显而易见了:
1、对于broker,顺序写磁盘:Kafka操作的是序列文件I / O(序列文件的特征是按顺序写,按顺序读),为保证顺序,Kafka强制点对点的按顺序传递消息,这意味着,一个consumer在消息流(或分区)中只有一个位置。
2、对于broker-consumer,不保存消息状态(是否被消费),使用offset记录消费位置:Kafka不保存消息的状态,即消息是否被“消费”。一般的消息系统需要保存消息的状态,并且还需要以随机访问的形式更新消息的状态。而Kafka 的做法是保存Consumer在Topic分区中的位置offset,在offset之前的消息是已被“消费”的,在offset之后则为未“消费”的,并且offset是可以任意移动的,这样就消除了大部分的随机IO。
3、对于broker,批量发送、压缩后发送,kafka在producer端加速/高吞吐量的措施:Kafka支持点对点的批量消息传递。
4、对于broker,Kafka的消息存储在OS pagecache(页缓存,page cache的大小为一页,通常为4K,在Linux读写文件时,它用于缓存文件的逻辑内容,从而加快对磁盘上映像和数据的访问)。

5.3 kafka如何保证消息不丢失?(从producer broker/partition consumer 三个方面来回答,拉高逼格)

从producer broker/partition consumer 三个方面来回答,拉高逼格

第一,对于broker-consumer,关于 ACK 机制,添加 acks=all 这个条件,保证broker发送给consumer的消息不会被丢
Kafka 通过 ACK 机制保证消息送达,Kafka 采用的是至少一次(At least once),消息不会丢,但是可能会重复传输。
acks 的默认值即为1,代表我们的消息被leader副本接收之后就算被成功发送。我们可以配置 acks = all ,代表则所有副本都要接收到该消息之后该消息才算真正成功被发送。
所以,添加 acks=all 这个条件,保证broker发送给consumer的消息不会被丢。

**第二,对于broker,partition数据设置至少3个副本分区,保证存储在 broker/partition 中的消息不丢失 **
为了保证 leader 副本能有 follower 副本能同步消息,我们一般会为 topic 设置 replication.factor >= 3。这样就可以保证每个 分区(partition) 至少有 3 个副本,以确保消息队列的安全性。

第三,对于broker,leader partition宕机后,关闭 unclean leader 选举,减低数据丢失可能性
我们最开始也说了我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。多个 follower 副本之间的消息同步情况不一样,当我们配置了 unclean.leader.election.enable = false 的话,当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。

第四,对于producer,攒一波批量发送消息,同步发送/异步发送
异步发送定义:为了得到更好的性能,Kafka 支持在生产者一侧进行本地buffer,也就是累积到一定的条数才发送,如果这里设置不当是会丢消息的。
同步发送/异步发送设置:生产者端设置:producer.type=async, sync,默认是 sync。
异步消息:当设置为 async,会大幅提升性能,因为生产者会在本地缓冲消息,并适时批量发送。
同步消息:如果对可靠性要求高,那么这里可以设置为 sync 同步发送。

第四2,对于producer-broker,消息至少要被写入到这么多副本才算成功
一般时候我们还需要设置:min.insync.replicas> 1 ,消息至少要被写入到这么多副本才算成功,也是提升数据持久性的一个参数,与acks配合使用。
但如果出现两者相等,我们还需要设置 replication.factor = min.insync.replicas + 1 ,避免在一个副本挂掉,整个分区无法工作的情况!

第五,对于consumer,关闭自动提交位移,在消息被完整处理之后再手动提交位移。
consumer端丢失消息的情形比较简单:如果在消息处理完成前就提交了offset,那么就有可能造成数据的丢失。
由于Kafka consumer默认是自动提交位移的,所以在后台提交位移前一定要保证消息被正常处理了,因此不建议采用很重的处理逻辑,如果处理耗时很长,则建议把逻辑放到另一个线程中去做。
为了避免数据丢失,现给出几点建议:设置 enable.auto.commit=false
关闭自动提交位移,在消息被完整处理之后再手动提交位移。

六、小结

Kafka面试三个问题,完成了。

天天打码,天天进步!!!