docs/mq/kafka/消息队列kafka详解:Kafka重要知识点+面试题大全.md
Kafka 消费端确保一个 Partition 在一个消费者组内只能被一个消费者消费。这句话改怎么理解呢?
了解了消费者与消费组的关系后,有朋友会比较疑惑消费者组有啥实际存在的意义呢?或者说消费组的作用是什么?
作者对消费组的作用归结了如下两点。
使用过 Kafka 消费者客户端的同学肯定知道,消费者组内偶尔会触发 Repartition 操作,所谓 Repartition 即 Partition 在某些情况下重新被分配给参与消费的消费者。基本可以分为如下几种情况。
2. 消费组内新增消费者,触发 Repartition 操作,如下图所示。一般这种情况是为了提高消费端的消费能力,从而加快消费进度。
<figure data-size="normal"> <figcaption>新增消费者情况</figcaption> </figure>3.Topic 下的 Partition 增多,触发 Repartition 操作,如下图所示。一般这种调整 Partition 个数的情况也是为了提高消费端消费速度的,因为当消费者个数大于等于 Partition 个数时,在增加消费者个数是没有用的(原因是:在一个消费组内,消费者:Partition = 1:N,当 N 小于 1 时,相当于消费者过剩了),所以一方面增加 Partition 个数同时增加消费者个数可以提高消费端的消费速度。
<figure data-size="normal"> <figcaption>新增Partition个数情况</figcaption> </figure>众所周知,ZK 不仅保存了消费者消费 partition 的进度,同时也保存了消费组的成员列表、partition 的所有者。消费者想要消费 Partition,需要从 ZK 中获取该消费者对应的分区信息及当前分区对应的消费进度,即 OffSert 信息。那么 Partition 应该由那个消费者进行消费,决定因素有哪些呢?从之前的图中不难得出,两个重要因素分别是:消费组中存活的消费者列表和 Topic 对应的 Partition 列表。通过这两个因素结合 Partition 分配算法,即可得出消费者与 Partition 的对应关系,然后将信息存储到 ZK 中。Kafka 有高级 API 和低级 API,如果不需要操作 OffSet 偏移量的提交,可通过高级 API 直接使用,从而降低使用者的难度。对于一些比较特殊的使用场景,比如想要消费特定 Partition 的信息,Kafka 也提供了低级 API 可进行手动操作。
在介绍消费端工作流程前,先来熟悉一下用到的一些组件。
KakfaConsumer:消费端,用于启动消费者进程来消费消息。ConsumerConfig:消费端配置管理,用于给消费端配置相关参数,比如指定 Kafka 集群,设置自动提交和自动提交时间间隔等等参数,都由其来管理。ConsumerConnector:消费者连接器,通过消费者连接器可以获得 Kafka 消息流,然后通过消息流就能获得消息从而使得客户端开始消费消息。以上三者之间的关系可以概括为:消费端使用消费者配置管理创建出了消费者连接器,通过消费者连接器创建队列(这个队列的作用也是为了缓存数据),其中队列中的消息由专门的拉取线程从服务端拉取然后写入,最后由消费者客户端轮询队列中的消息进行消费。具体操作流程如下图所示。
<figure data-size="normal"> <figcaption>消费端工作流程</figcaption> </figure>我们在从消费者与 ZK 的角度来看看其工作流程是什么样的?
<figure data-size="normal"> <figcaption>消费端与ZK之间的工作流程</figcaption> </figure>从上图可以看出,首先拉取线程每拉取一次消息,同步更新一次拉取状态,其作用是为了下一次拉取消息时能够拉取到最新产生的消息;拉取线程将拉取到的消息写入到队列中等待消费消费线程去真正读取处理。消费线程以轮询的方式持续读取队列中的消息,只要发现队列中有消息就开始消费,消费完消息后更新消费进度,此处需要注意的是,消费线程不是每次都和 ZK 同步消费进度,而是将消费进度暂时写入本地。这样做的目的是为了减少消费者与 ZK 的频繁同步消息,从而降低 ZK 的压力。
消费者从服务端的 Partition 上拉取到消息,消费消息有三种情况,分别如下:
消费者读取消息,先处理消息,在保存消费进度。消费者拉取到消息,先消费消息,然后在保存偏移量,当消费者消费消息后还没来得及保存偏移量,则会造成消息被重复消费。如下图所示:
<figure data-size="normal"> <figcaption>先消费后保存消费进度</figcaption> </figure>消费者读取消息,先保存消费进度,在处理消息。消费者拉取到消息,先保存了偏移量,当保存了偏移量后还没消费完消息,消费者挂了,则会造成未消费的消息丢失。如下图所示:
<figure data-size="normal"> <figcaption>先保存消费进度后消费消息</figcaption> </figure>正好消费一次的办法可以通过将消费者的消费进度和消息处理结果保存在一起。只要能保证两个操作是一个原子操作,就能达到正好消费一次的目的。通常可以将两个操作保存在一起,比如 HDFS 中。正好消费一次流程如下图所示。
<figure data-size="normal"> <figcaption>正好消费一次</figcaption> </figure>假设有一个 Kafka 集群,Broker 个数为 3,Topic 个数为 1,Partition 个数为 3,Replica 个数为 2。Partition 的物理分布如下图所示。
<figure data-size="normal"> <figcaption>Partition分布图</figcaption> </figure>从上图可以看出,该 Topic 由三个 Partition 构成,并且每个 Partition 由主从两个副本构成。每个 Partition 的主从副本分布在不同的 Broker 上,通过这点也可以看出,当某个 Broker 宕机时,可以将分布在其他 Broker 上的从副本设置为主副本,因为只有主副本对外提供读写请求,当然在最新的 2.x 版本中从副本也可以对外读请求了。将主从副本分布在不同的 Broker 上从而提高系统的可用性。
Partition 的实际物理存储是以 Log 文件的形式展示的,而每个 Log 文件又以多个 LogSegment 组成。Kafka 为什么要这么设计呢?其实原因比较简单,随着消息的不断写入,Log 文件肯定是越来越大,Kafka 为了方便管理,将一个大文件切割成一个一个的 LogSegment 来进行管理;每个 LogSegment 由数据文件和索引文件构成,数据文件是用来存储实际的消息内容,而索引文件是为了加快消息内容的读取。
可能又有朋友会问,Kafka 本身消费是以 Partition 维度顺序消费消息的,磁盘在顺序读的时候效率很高完全没有必要使用索引啊。其实 Kafka 为了满足一些特殊业务需求,比如要随机消费 Partition 中的消息,此时可以先通过索引文件快速定位到消息的实际存储位置,然后进行处理。
总结一下 Partition、Replica、Log 和 LogSegment 之间的关系。消息是以 Partition 维度进行管理的,为了提高系统的可用性,每个 Partition 都可以设置相应的 Replica 副本数,一般在创建 Topic 的时候同时指定 Replica 的个数;Partition 和 Replica 的实际物理存储形式是通过 Log 文件展现的,为了防止消息不断写入,导致 Log 文件大小持续增长,所以将 Log 切割成一个一个的 LogSegment 文件。
注意: 在同一时刻,每个主 Partition 中有且只有一个 LogSegment 被标识为可写入状态,当一个 LogSegment 文件大小超过一定大小后(比如当文件大小超过 1G,这个就类似于 HDFS 存储的数据文件,HDFS 中数据文件达到 128M 的时候就会被分出一个新的文件来存储数据),就会新创建一个 LogSegment 来继续接收新写入的消息。
流程解析
在第 3 篇文章讲过,生产者客户端对于每个 Partition 一次会发送一批消息到服务端,服务端收到一批消息后写入相应的 Partition 上。上图流程主要分为如下几步:
了解以上过程后,我们在来看看消息的具体构成情况。
<figure data-size="normal"> <figcaption>消息构成细节图</figcaption> </figure>一条消息由如下三部分构成:
通过以上流程可以看出,每条消息在被实际存储到磁盘时都会被分配一个绝对偏移量后才能被写入磁盘。在同一个分区内,消息的绝对偏移量都是从 0 开始,且单调递增;在不同分区内,消息的绝对偏移量是没有任何关系的。接下来讨论下消息的绝对偏移量的计算规则。
确定消息偏移量有两种方式,一种是顺序读取每一条消息来确定,此种方式代价比较大,实际上我们并不想知道消息的内容,而只是想知道消息的偏移量;第二种是读取每条消息的 Size 属性,然后计算出下一条消息的起始偏移量。比如第一条消息内容为 “abc”,写入磁盘后的偏移量为:8(OffSet)+ 4(Message 大小)+ 3(Message 内容的长度)= 15。第二条写入的消息内容为“defg”,其起始偏移量为 15,下一条消息的起始偏移量应该是:15+8+4+4=31,以此类推。
和写入消息流程不同,读取消息流程分为两种情况,分别是消费端消费消息和从副本(备份副本)同步主副本的消息。在开始分析读取流程之前,需要先明白几个用到的变量,不然流程分析可能会看的比较糊涂。
假设消费端从 000000621 位置开始消费消息,关于几个变量的关系如下图所示。
<figure data-size="normal"> <figcaption>位置关系图</figcaption> </figure>消费端和从副本拉取流程如下:
假设拉取消息起始位置为 00000313,消息拉取流程图如下:
<figure data-size="normal"> <figcaption>消息拉取流程图</figcaption> </figure>设置为 1 时代表当 Leader 状态的 Partition 接收到消息并持久化时就认为消息发送成功,如果 ISR 列表的 Replica 还没来得及同步消息,Leader 状态的 Partition 对应的 Broker 宕机,则消息有可能丢失。
<figure data-size="normal"> <figcaption>request.required.acks=0</figcaption> </figure>设置为 0 时代表 Producer 发送消息后就认为成功,消息有可能丢失。
<figure data-size="normal"> <figcaption>request.required.acks=-1</figcaption> </figure>设置为-1 时,代表 ISR 列表中的所有 Replica 将消息同步完成后才认为消息发送成功;但是如果只存在主 Partition 的时候,Broker 异常时同样会导致消息丢失。所以此时就需要min.insync.replicas参数的配合,该参数需要设定值大于等于 2,当 Partition 的个数小于设定的值时,Producer 发送消息会直接报错。
上面这个过程看似已经很完美了,但是假设如果消息在同步到部分从 Partition 上时,主 Partition 宕机,此时消息会重传,虽然消息不会丢失,但是会造成同一条消息会存储多次。在新版本中 Kafka 提出了幂等性的概念,通过给每条消息设置一个唯一 ID,并且该 ID 可以唯一映射到 Partition 的一个固定位置,从而避免消息重复存储的问题(作者到目前还没有使用过该特性,感兴趣的朋友可以自行在深入研究一下)。
从上图可以看出,假设 Consumer 从主 Partition1 上消费消息,由于 Kafka 规定只允许消费 HW 之前的消息,所以最多消费到 Message2。假设当 Partition1 异常后,Partition2 被选举为 Leader,此时依旧可以从 Partition2 上读取到 Message2。其实 HW 的意思利用了木桶效应,始终保持最短板的那个位置。
从上面我们也可以看出,使用 HW 特性后会使得消息只有被所有副本同步后才能被消费,所以在一定程度上降低了消费端的性能,可以通过设置replica.lag.time.max.ms参数来保证消息同步的最大时间。
kafka 使用了顺序写入和“零拷贝”技术,来达到每秒钟 200w(Apache 官方给出的数据) 的磁盘数据写入量,另外 Kafka 通过压缩数据,降低 I/O 的负担。
大家都知道,对于磁盘而已,如果是随机写入数据的话,每次数据在写入时要先进行寻址操作,该操作是通过移动磁头完成的,极其耗费时间,而顺序读写就能够避免该操作。
普通的数据拷贝流程如上图所示,数据由磁盘 copy 到内核态,然后在拷贝到用户态,然后再由用户态拷贝到 socket,然后由 socket 协议引擎,最后由协议引擎将数据发送到网络中。
<figure data-size="normal"> <figcaption>&quot;零拷贝&quot;流程图</figcaption> </figure>采用了“零拷贝”技术后可以看出,数据不在经过用户态传输,而是直接在内核态完成操作,减少了两次 copy 操作。从而大大提高了数据传输速度。
Kafka 官方提供了多种压缩协议,包括 gzip、snappy、lz4 等等,从而降低了数据传输的成本。
这里给大家一个解决重复消费的思路,作者公司使用了 Redis 记录了被消费的 key,并设置了过期时间,在 key 还没有过期内,对于同一个 key 的消息全部当做重复消息直接抛弃掉。 在网上看到过另外一种解决方案,使用 HDFS 存储被消费过的消息,是否具有可行性存疑(需要读者朋友自行探索),读者朋友们可以根据自己的实际情况选择相应的策略,如果朋友们还有其他比较好的方案,欢迎留言交流。
这道题比较主观一些(自认为没有网上其他文章写得话,轻喷),但是都相信大家使用消息队列无非就是为了 解耦、异步、消峰。
<figure data-size="normal"> <figcaption>系统调用图</figcaption> </figure>随着业务的发展,相信有不少朋友公司遇到过如上图所示的情况,系统 A 处理的结构被 B、C、D 系统所依赖,当新增系统 E 时,也需要系统 A 配合进行联调和上线等操作;还有当系统 A 发生变更时同样需要告知 B、C、D、E 系统需要同步升级改造。
<figure data-size="normal"> <figcaption>引入消息队列图</figcaption> </figure>引入消息队列后有两个好处:
引入消息队列的坏处:
万事皆具备两面性,看似引入消息队列这件事情很美好,但是同时也增加了系统的复杂度、系统的维护成本提高(如果 MQ 挂了怎么办)、引入了一致性等等问题需要去解决。
作者认为采用 Kafka 的原因有如下几点:
这个问题有个先决条件,我们只讨论 Kafka0.9 版本的情况。对于高版本,从 Partition 也可以承担读请求了,这里不多赘述。
Kafka 如果支持读写分离的话,有如下几个问题。
2. 从上图可以看出,从从 Partition 上读取数据会有两个问题。一、数据从主 Partition 上同步到从 Partition 有数据延迟问题,因为数据从生产到消费会经历 3 次网络传输才能够被消费,对于时效性要求比较高的场景本身就不适合了。二、数据一致性问题,假设主 Partition 将数据第一次修改成了 A,然后又将该数据修改成了 B,由于从主 Partition 同步到从 Partition 会有延迟问题,所以也就会产生数据一致性问题。
分析得出,通过解决以上问题来换取从 Partition 承担读请求,成本可想而知,而且对于写入压力大,读取压力小的场景,本身也就没有什么意义了。
本文介绍了几个常见的 Kafka 的面试题
ISR:速率和leader相差低于10s的follower的集合
OSR:速率和leader相差大于10s的follwer
AR:所有分区的follower
HW:High Water高水位,根据同一分区中最低的LEO决定(Log End Offset)
LEO:每个分区最大的Offset
在每个分区内,每条消息都有offset,所以消息在同一分区内有序,无法做到全局有序性
分区器Partitioner用来对分区进行处理的,即消息发送到哪一个分区的问题。序列化器,这个是对数据进行序列化和反序列化的工具。拦截器,即对于消息发送进行一个提前处理和收尾处理的类Interceptor,处理顺利首先通过拦截器=>序列化器=>分区器
使用两个线程:main和sender 线程,main线程会一次经过拦截器、序列化器、分区器将数据发送到RecoreAccumulator线程共享变量,再由sender线程从共享变量中拉取数据发送到kafka broker
batch.size达到此规模消息才发送,linger.ms未达到规模,等待当前时长就发送数据。
这句话是对的,超过分区个数的消费者不会在接收数据,主要原因是一个分区的消息只能够被一个消费者组中的一个消费者消费。
生产者发送数据的offset是从0开始的,消费者消费的数据的offset是从1开始,故最新消息是offset+1
先消费后提交offset,如果消费完宕机了,则会造成重复消费
先提交offset,还没消费就宕机了,则会造成漏消费
会在 zookeeper 中的/brokers/topics 节点下创建一个新的 topic 节点,如:/brokers/topics/first 触发 Controller 的监听程序 kafka Controller 负责 topic 的创建工作,并更新 metadata cache
可以增加,修改分区个数--alter可以修改分区个数
不可以减少,减少了分区之后,之前的分区中的数据不好处理
有,__consumer_offsets主要用来在0.9版本以后保存消费者消费的offset
Kafka分区对于Kafka集群来说,分区可以做到负载均衡,对于消费者来说分区可以提高并发度,提高读取效率
每一个分区对应着一个文件夹,命名为topic-0/topic-1…,每个文件夹内有.index和.log文件。
offset表示当前消息的编号,首先可以通过二分法定位当前消息属于哪个.index文件中,随后采用seek定位的方法查找到当前offset在.index中的位置,此时可以拿到初始的偏移量。通过初始的偏移量再通过seek定位到.log中的消息即可找到。
Kafka集群中有一个broker会被选举为Controller,负责管理集群broker的上下线、所有topic的分区副本分配和leader的选举等工作。Controller的工作管理是依赖于zookeeper的。
在ISR中需要选举出Leader,选择策略为先到先得。在分区中需要选举,需要选举出Leader和follower。
失效副本为速率比leader相差大于10s的follower,ISR会将这些失效的follower踢出,等速率接近leader的10s内,会重新加入ISR
https://blog.csdn.net/cao131502 https://zhuanlan.zhihu.com/p/137811719