文章目录
-
- 一、初识Kafka
-
- [1.1 Kafka架构](#1.1 Kafka架构)
- [1.2 生产者](#1.2 生产者)
-
- [1.2.1 消息发送方式](#1.2.1 消息发送方式)
- [1.2.2 分区](#1.2.2 分区)
- [1.2.3 压缩机制](#1.2.3 压缩机制)
- [1.3 消费者](#1.3 消费者)
-
- [1.3.1 分区重平衡](#1.3.1 分区重平衡)
- [1.3.2 分区重平衡流程](#1.3.2 分区重平衡流程)
- [1.4 Kafka的用途](#1.4 Kafka的用途)
- [1.5 消费者是推还是拉](#1.5 消费者是推还是拉)
- [1.6 Kafka与传统MQ之间的区别](#1.6 Kafka与传统MQ之间的区别)
- 二、Kafka集群
-
- [2.1 副本机制](#2.1 副本机制)
- [2.2 控制器机制](#2.2 控制器机制)
- 三、Kafka效率高的原因
-
- [3.1 利用Partition实现并行处理](#3.1 利用Partition实现并行处理)
- [3.2 顺序写磁盘](#3.2 顺序写磁盘)
- [3.3 充分利用 Page Cache](#3.3 充分利用 Page Cache)
- [3.4 零拷贝技术](#3.4 零拷贝技术)
- [3.5 数据压缩](#3.5 数据压缩)
- 四、相关问题
-
- [4.1 Kafka 中的分区器、序列化器、拦截器,它们之间的处理顺序是什么](#4.1 Kafka 中的分区器、序列化器、拦截器,它们之间的处理顺序是什么)
- [4.2 有哪些情形会造成重复消费](#4.2 有哪些情形会造成重复消费)
- [4.3 有哪些情形会造成消息漏消费](#4.3 有哪些情形会造成消息漏消费)
- [4.4 Kafka消息是采用Pull模式,还是Push模式](#4.4 Kafka消息是采用Pull模式,还是Push模式)
- [4.5 Kafka的那些设计让它有如此高的性能?](#4.5 Kafka的那些设计让它有如此高的性能?)
- [4.6 Kafka为什么要抛弃Zookeeper](#4.6 Kafka为什么要抛弃Zookeeper)
- [4.7 Kafka 如何保证可靠性](#4.7 Kafka 如何保证可靠性)
- [4.8 Kafka中的延迟队列怎么实现](#4.8 Kafka中的延迟队列怎么实现)
- [4.9 简述Kafka的日志目录结构](#4.9 简述Kafka的日志目录结构)
- [4.10 Kafka中有哪些索引文件](#4.10 Kafka中有哪些索引文件)
一、初识Kafka
Kafka是一种发布订阅消息系统,主要用于处理活跃的流式数据。
Kafka的特点:
同时为发布和订阅提供高吞吐量
。Kafka每秒可以生产约25万消息(50MB),每秒处理55万消息(110MB)。可进行持久化操作
。将消息持久化到磁盘,因此可用于批量消费,例如ETL,以及实时应用程序。通过将数据持久化到硬盘以及replication防止数据丢失。分布式系统,易于向外扩展
。所有的producer、broker和consumer都会有多个,均为分布式的。无需停机即可扩展机器。它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性。消息被处理的状态是在consumer端维护,而不是由server端维护
。当失败时能自动平衡。- 支持online和offline的场景。
- Kafka支持实时的流式处理。
相比传统的消息系统,Kafka可以很好的保证有序性,也可以在多个消费者组并发的情况下提供较好的负载均衡。
1.1 Kafka架构
Kafka是分布式架构,Producer、Broker和Consumer都可以有多个。
Kafka将消息以Topic为单位进行归纳,向Topic发布消息的程序就是Producer,预订Topic并消费消息的程序是Consumer。
Kafka以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个 Broker。Producer通过网络将消息发送到Kafka集群,集群向消费者提供消息。
Broker :负责消息存储和转发,Kafka集群中的一台或多台服务器统称为Broker。
Topic :消息类别,Kafka按照Topic来分类消息。
Partition :Topic物理上的分组,一个Topic可以分为多个Partition,每个Partition是一个有序的队列。Partition中的每条消息都会被分配一个有序的id(offset)。每个分区都由一系列有序的、不可变的消息组成,这些消息被连续的追加到分区中。分区中的每个消息都有一个连续的序列号叫做offset,用来在分区中唯一的标识这个消息。
offset :消息在日志中的位置,可以理解是消息在Partition上的偏移量,也是代表该消息的唯一序号。
Segment :一个partition当中存在多个segment文件段,每个segment分为两部分,.log文件和.index文件,其中.index文件是索引文件,主要用于快速查询,.log文件当中数据的偏移量位置。
Producer :消息生产者,向Kafka的Topic发布消息。Producer将消息发布到它指定的Topic中,并负责决定发布到哪个分区。通常简单的由负载均衡机制随机选择分区,但也可以通过特定的分区函数选择分区。
Consumer :消息消费者,订阅Topic并处理发布的消息。实际上每个Consumer唯一需要维护的数据是消息在日志中的位置,也就是offset。这个offset有Consumer来维护:一般情况下随着Consumer不断的读取消息,这offset的值不断增加,但其实Consumer可以以任意的顺序读取消息,比如它可以将offset设置成为一个旧的值来重读之前的消息。
Consumer Group :消费者组,每一个 consumer 属于一个特定的 consumer group(可以为每个consumer指定 groupName).。
Zookeeper:保存着集群Broker、Topic、Partition等meta数据;另外,还负责Broker故障发现,partition leader选举,负载均衡等功能。
- 1、Producer
Producer主要是用于生产消息,是Kafka当中的消息生产者,生产的消息通过Topic进行归类,保存到Kafka的Broker里面去。
- 2、Topic
Kafka将消息以Topic为单位进行归类;
Topic是一种分类或者发布的一些列记录的名义上的名字。Kafka主题始终是支持多用户订阅的;也就是说,一 个主题可以有零个、一个或者多个消费者订阅写入的数据;
在Kafka集群中,可以有无数的主题;
生产者和消费者消费数据一般以主题为单位。更细粒度可以到分区级别。 - 3、Partition
Topic是消息的归类,一个Topic可以有多个分区(Partition),每个分区保存部分Topic的数据。
一个Broker服务下,可以创建多个分区,Broker数与分区数没有关系; 在Kafka中,每一个分区会有一个编号:编号从0开始。 每一个分区内的数据是有序的,但全局的数据不能保证是有序的。 - 4、Consumer
Consumer是Kafka当中的消费者,主要用于消费Kafka当中的数据,消费者一定是归属于某个消费组中的。 - 5、Consumer Group
消费者组由一个或者多个消费者组成,同一个组中的消费者对于同一条消息只消费一次。每个消费者都属于某个消费者组,如果不指定,那么所有的消费者都属于默认的组。
每个消费者组都有一个ID,即group ID。组内的所有消费者协调在一起来消费一个订阅主题( Topic)的所有分区(Partition)。当然,每个分区只能由同一个消费组内的一个消费者(Consumer)来消费,可以由不同的消费组来消费。
Partition数量决定了每个Consumer Group中并发消费者的最大数量。
如果只有两个分区,即使一个组内的消费者有4个,也会有两个空闲的。 右图所示,有4个分区,每个消费者消费一个分区,并发量达到最大4。
上图所示,不同的消费者组消费同一个Topic,这个Topic有4个分区,分布在两个节点上。左边的消费组1有两个消费者,每个消费者就要消费两个分区才能把消息完整的消费完,右边的消费组2有四个消费者,每个消费者消费一个分区即可。
【分区与消费组的关系】
消费组: 由一个或者多个消费者组成,同一个组中的消费者对于同一条消息只消费一次。 某一个主题下的分区数,对于消费该主题的同一个消费组下的消费者数量,应该小于等于该主题下的分区数。
如:某一个主题有4个分区,那么消费组中的消费者应该小于等于4,而且最好与分区数成整数倍1/2/4这样。同一个分区下的数据,在同一时刻,不能同一个消费组的不同消费者消费。
分区数越多,同一时间可以有越多的消费者来进行消费,消费数据的速度就会越快,提高消费的性能。
- 6、分区副本
副本数:控制消息保存在几个Broker(服务器)上,一般情况下副本数等于bBrokeroker的个数。
一个Broker服务下,不可以创建多个副本因子。创建主题时,副本因子应该小于等于可用的Broker数。
副本因子操作以分区为单位的。每个分区都有各自的主副本和从副本。
主副本叫做Leader,从副本叫做Follower(在有多个副本的情况下,Kafka会为同一个分区下的所有分区,设定角色关系:一个Leader和N个Follower),处于同步状态的副本叫做in-sync-replicas(ISR)。
Follower通过拉的方式从Leader同步数据。 消费者和生产者都是从Leader读写数据,不与Follower交互。
副本因子的作用:让Kafka读取数据和写入数据时的可靠性。
副本因子是包含本身,同一个副本因子不能放在同一个Broker中。
如果某一个分区有三个副本因子,就算其中一个挂掉,那么只会剩下的两个中,选择一个Leader,但不会在其他的Broker中,另启动一个副本(因为在另一台启动的话,存在数据传递,只要在机器之间有数据传递,就会长时间占用网络IO,Kafka是一个高吞吐量的消息系统,这个情况不允许发生)所以不会在另一个broker中启动。
如果所有的副本都挂了,生产者如果生产数据到指定分区的话,将写入不成功。
lsr:当前可用的副本。 - 7、Segment文件
Topic是逻辑上的概念,物理上存储的其实是Partition,每一个Partition最终对应一个目录,里面存储所有的消息和索引文件。默认情况下,每一个Topic在创建时如果不指定Partition数量时只会创建1个Partition。比如,创建了一个Topic名字为 test ,没有指定Partition的数量,那么会默认创建一个test-0的文件夹,这里的命名规则是: <topic_name>-<partition_id> 。
任何发布到Partition的消息,都会被追加到Partition数据文件的尾部,这样的顺序写磁盘操作让Kafka的效率非常高。
每一条消息被发送到Broker 中,会根据Partition规则选择被存储到哪一个Partition。如果Partition规则设置的合理,所有消息可以均匀分布到不同的Partition中。
假设现在Kafka集群只有一个 Broker,创建2个Topic名称分别为:topic1、topic2,Partition数量分别为1、2,根目录下就会创建如下三个文件夹:
shell
| --topic1-0
| --topic2-0
| --topic2-1
在Kafka的文件存储中,同一个Topic下有多个不同的Partition,每个Partition都为一个目录,而每一个目录又被平均分配成多个大小相等的Segment File中,Segment File又由index file和data file组成,他们总是成对出现,后缀 ".index" 和 ".log" 分表表示Segment索引文件和数据文件。
假设设置每个Segment大小为500MB,并启动生产者向topic1中写入大量数据,topic1-0文件夹中就会产生类似如下的一些文件:
shell
| --topic1-0
| --00000000000000000000.index
| --00000000000000000000.log
| --00000000000000368769.index
| --00000000000000368769.log
| --00000000000000737337.index
| --00000000000000737337.log
| --00000000000001105814.index | --00000000000001105814.log
| --topic2-0
| --topic2-1
Segment是Kafka文件存储的最小单位。Segment文件命名规则:Partition全局的第一个Segment从0开始,后续每个Segment文件名为上一个Segment文件最后一条消息的offset值。数值最大为64位long大小,19 位数字字符长度,没有数字用0填充。如 00000000000000368769.index和00000000000000368769.log。
segment index file 采取稀疏索引存储方式,减少索引文件大小,通过mmap(内存映射)可以直接内存操作,稀疏索引为数据文件的每个对应message设置一个元数据指针,它比稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间。
一个Partition当中由多个Segment文件组成,每个Segment文件,包含两部分,一个是.log文件,另外一个是.index文件,其中 .log 文件包含了我们发送的数据存储,.index 文件,记录的是我们.log文件的数据索引值,以便于我们加快数据的查询速度。
索引文件中元数据指向对应数据文件中message的物理偏移地址。
其中以索引文件中元数据 ❤️, 497> 为例,依次在数据文件中表示第 3 个 message(在全局 Partition表示第 368769 + 3 = 368772 个 message)以及该消息的物理偏移地址为 497。
上图左半部分是索引文件,里面存储的是一对一对的key-value,其中key是消息在数据文件(对应的log文件)中的编号,比如"1,3,6,8......", 分别表示在log文件中的第1条消息、第3条消息、第6条消息、第8条消息......
为什么在index文件中这些编号不是连续的呢? 这是因为index文件中并没有为数据文件中的每条消息都建立索引,而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。 这样避免了索引文件占用过多的空间,从而可以将索引文件保留在内存中。 但缺点是没有建立索引的Message也不能一次定位到其在数据文件的位置,从而需要做一次顺序扫描,但是这次顺序扫描的范围就很小了。
value代表的是在全局partiton中的第几个消息。以索引文件中元数据 3,497 为例,其中3代表在右边log数据文件中从上到下第3个消息, 497表示该消息的物理偏移地址(位置)为497(也表示在全局partiton表示第497个消息-顺序写入特性)。
注意该 index 文件并不是从0开始,也不是每次递增1的,这是因为 Kafka 采取稀疏索引存储的方式,每隔一定字节的数据建立一条索引,它减少了索引文件大小,使得能够把 index 映射到内存,降低了查询时的磁盘 IO 开销,同时也并没有给查询带来太多的时间消耗。
因为其文件名为上一个 Segment 最后一条消息的 offset ,所以当需要查找一个指定 offset 的message 时,通过在所有 segment 的文件名中进行二分查找就能找到它归属的 segment ,再在其index 文件中找到其对应到文件上的物理位置,就能拿出该 message 。
由于消息在 Partition 的 Segment 数据文件中是顺序读写的,且消息消费后不会删除(删除策略是针对过期的 Segment 文件),这种顺序磁盘 IO 存储设计师 Kafka 高性能很重要的原因。
Kafka 是如何准确的知道 message 的偏移的呢?这是因为在 Kafka 定义了标准的数据存储结构,在 Partition 中的每一条 message 都包含了以下三个属性:
offset:表示 message 在当前 Partition 中的偏移量,是一个逻辑上的值,唯一确定了Partition 中的一条 message,可以简单的认为是一个 id;
MessageSize:表示 message 内容 data 的大小;
data:message 的具体内容。
- 8、log日志目录
Kafka在指定的log.dir目录下,会创建一些文件夹;名字是 (主题名字-分区名) 所组成的文件夹。 在(主题名字-分区名)的目录下,会有两个文件存在,示例:
shell
#索引文件
00000000000000000000.index
#日志内容
00000000000000000000.log
在目录下的文件,会根据log日志的大小进行切分,.log文件的大小为1G的时候,就会进行切分文件。示例:
shell
-rw-r--r--. 1 root root 389k 1月 17 18:03 00000000000000000000.index
-rw-r--r--. 1 root root 1.0G 1月 17 18:03 00000000000000000000.log
-rw-r--r--. 1 root root 10M 1月 17 18:03 00000000000000077894.index
-rw-r--r--. 1 root root 127M 1月 17 18:03 00000000000000077894.log
在kafka的设计中,将offset值作为了文件名的一部分。
segment文件命名规则 :partion全局的第一个segment从0开始,后续每个segment文件名为上一个全局 partion的最大offset(偏移message数)。数值最大为64位long大小,20位数字字符长度,没有数字就用 0 填充。
通过索引信息可以快速定位到message。通过index元数据全部映射到内存,可以避免segment File的IO磁盘操作;
通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。
稀疏索引:为了数据创建索引,但范围并不是为每一条创建,而是为某一个区间创建; 好处:就是可以减少索引值的数量。 不好的地方:找到索引区间之后,要得进行第二次处理。
- 9、message的物理结构
生产者发送到Kafka的每条消息,都被Kafka包装成了一个message,message的物理结构:
1.2 生产者
1.2.1 消息发送方式
生产者发送给Kafka数据,可以采用同步方式或异步方式。
- 同步方式
发送一批数据给Kafka后,等待kafkaKafka结果:
生产者等待10s,如果broker没有给出ack响应,就认为失败。
生产者重试3次,如果还没有响应,就报错。
- ack机制(确认机制)
生产者数据发送出去,需要服务端返回一个确认码,即ack响应码;ack的响应有三个状态值0、1、-1:
0:生产者只负责发送数据,不关心数据是否丢失,丢失的数据,需要再次发送。
1:partition的leader收到数据,不管follow是否同步完数据,响应的状态码为1。
-1:所有的从节点都收到数据,响应的状态码为-1。
如果broker端一直不返回ack状态,producer永远不知道是否成功;producer可以设置一个超时时间10s,超过时间认为失败。
- 批量发送
提高消息吞吐量重要的方式,Producer端可以在内存中合并多条消息后,以一次请求的方式发送了批量的消息给broker,从而大大减少broker存储消息的IO操作次数。但也一定程度上影响了消息的实时性,相当于以时延代价,换取更好的吞吐量。
1.2.2 分区
- 负载均衡(Partition会均衡分布到不同Broker上)
由于消息topic由多个Partition组成,且Partition会均衡分布到不同Broker上。因此,为了有效利用Broker集群的性能,提高消息的吞吐量,Producer可以通过随机或者hash等方式,将消息平均发送到多个Partition上,以实现负载均衡。
Kafka对于数据的读写是以分区为粒度的,分区可以分布在多个主机(Broker)中,这样每个节点能够实现独立的数据写入和读取,并且能够通过增加新的节点来增加Kafka集群的吞吐量,通过分区部署在多个Broker来实现负载均衡的效果。
Kafka的分区策略指的就是将生产者锁产生的数据发送到哪个分区的算法。Kafka 为我们提供了默认的分区策略,同时它也支持自定义分区策略。分区策略有这几种:
- 1、顺序轮询
顺序分配,消息是均匀的分配给每个partition,即每个分区存储一次消息。
轮训策略是Kafka Producer提供的默认策略。 - 2、随机轮询
就是随机地向partition中保存消息。
- 3、按照key进行消息保存
也叫做key-ordering策略,Kafka中每条消息都会有自己的key,一旦消息被定义了Key,那么你就可以保证同一个Key的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略。
1.2.3 压缩机制
Producer端可以通过GZIP或Snappy格式对消息集合进行压缩。Producer端进行压缩之后,在Consumer端需进行解压。压缩的好处就是减少传输的数据量,减轻对网络传输的压力,在对大数据处理上,瓶颈往往体现在网络上而不是CPU(压缩和解压会耗掉部分CPU资源)。
Kafka的消息分为两层:消息集合 和 消息。一个消息集合中包含若干条日志项,而日志项才是真正封装消息的地方。Kafka底层的消息日志由一系列消息集合日志项组成。Kafka通常不会直接操作具体的一条条消息,它总是在消息集合这个层面上进行写入操作。Kafka Producer中使用compression.type来开启压缩。示例:
java
private Properties properties = new Properties();
properties.put("bootstrap.servers","192.168.1.9:9092");
properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
//使用GZIP压缩算法
properties.put("compression.type", "gzip");
Producer<String,String> producer = new KafkaProducer<String, String>(properties);
ProducerRecord<String,String> record = new ProducerRecord<String, String>("CustomerCountry","Precision Products","France");
1.3 消费者
在消费消息时,有两种模式:队列模式和发布-订阅模式。队列模式中,消费者可以同时从服务端读取消息,每个消息只被其中一个消费者读到。发布-订阅模式中消息被广播到所有的消费者中。
消费者可以加入一个消费者组,共同竞争一个Topic,Topic中的消息将被分发到组中的一个成员中。
如果所有的消费者都在一个组中,这就成为了传统的队列模式,在各消费者中实现负载均衡。
如果所有的消费者都不在不同的组中,这就成为了发布-订阅模式,所有的消息都被分发到所有的消费者中。
- 消费者的数据有序性
一个消费者组里它的内部是有序的,消费者组与消费者组之间是无序的。 - 如何消费数据
消费者每次消费数据的时候,消费者都会记录消费的物理偏移量(offset)的位置等到下次消费时,他会接着上次位置继续消费。
1.3.1 分区重平衡
消费者演变过程大致:最初是一个消费者订阅一个主题并消费其全部分区的消息,后来有一个消费者加入群组,随后又有更多的消费者加入群组,而新加入的消费者实例分摊了最初消费者的部分消息,这种把分区的所有权通过一个消费者转到其他消费者的行为称为重平衡(Rebalance)。
重平衡非常重要,它为消费者群组带来了高可用性 和 伸缩性,我们可以放心的添加消费者或移除消费者,不过在正常情况下我们并不希望发生这样的行为。在重平衡期间,消费者无法读取消息,造成整个消费者组在重平衡的期间都不可用。另外,当分区被重新分配给另一个消费者时,消息当前的读取状态会丢失,它有可能还需要去刷新缓存,在它重新恢复状态之前会拖慢应用程序。
消费者通过向组织协调者(Kafka Broker)发送心跳来维护自己是消费者组的一员并确认其拥有的分区。对于不同不的消费群体来说,其组织协调者可以是不同的。只要消费者定期发送心跳,就会认为消费者是存活的并处理其分区中的消息。当消费者检索记录或者提交它所消费的记录时就会发送心跳。
如果过了一段时间Kafka停止发送心跳了,会话(Session)就会过期,组织协调者就会认为这个Consumer已经死亡,就会触发一次重平衡。如果消费者宕机并且停止发送消息,组织协调者会等待几秒钟,确认它死亡了才会触发重平衡。在这段时间里,死亡的消费者将不处理任何消息。在清理消费者时,消费者将通知协调者它要离开群组,组织协调者会触发一次重平衡,尽量降低处理停顿。
在重平衡期间,消费者组中的消费者实例都会停止消费,等待重平衡的完成。而且重平衡这个过程很慢。
1.3.2 分区重平衡流程
重平衡过程可以从两个方面去看:消费者端和协调者端。
- 从消费者看重平衡
从消费者看重平衡有两个步骤:分别是 消费者加入组 和 等待领导者分配方案。这两个步骤后分别对应的请求是 JoinGroup 和 SyncGroup。
新的消费者加入群组时,这个消费者会向协调器发送 JoinGroup 请求。在该请求中,每个消费者成员都需要将自己消费的 topic 进行提交。这么做的目的就是为了让协调器收集足够的元数据信息,来选取消费者组的领导者。通常情况下,第一个发送 JoinGroup 请求的消费者会自动称为领导者。领导者的任务是收集所有成员的订阅信息,然后根据这些信息,制定具体的分区消费分配方案。
在所有的消费者都加入进来并把元数据信息提交给领导者后,领导者做出分配方案并发送 SyncGroup请求给协调者,协调者负责下发群组中的消费策略。
当所有成员都成功接收到分配方案后,消费者组进入到 Stable 状态,即开始正常的消费工作。 - 从协调者来看重平衡
从协调者角度来看重平衡主要有下面这几种触发条件:新成员加入组、组成员主动离开、组成员崩溃离开、组成员提交位移。
1)新成员加入组。消费者集群状态处于Stable 等待分配的过程,这时候如果有新的成员加入组的话,重平衡的过程。
2)组成员离开。组成员离开消费者群组指的是消费者实例调用 close() 方法主动通知协调者它要退出,所对应的是LeaveGroup()请求 。
3)组成员崩溃。组成员崩溃是指消费者实例出现严重故障,宕机或者一段时间未响应,协调者接收不到消费者的心跳,就会被认为是组成员崩溃,崩溃离组是被动的,协调者通常需要等待一段时间才能感知到,这段时间一般是由消费者端参数 session.timeout.ms 控制的。
1.4 Kafka的用途
- 1、消息系统
Kafka和传统的消息系统(也称作消息中间件)都具备系统解耦、冗余存储、流量削峰、缓冲、异步通信、扩展性、可恢复性等功能。与此同时,Kafka还提供了大多数消息系统难以实现的消息顺序性保障及回溯消费的功能。 - 2、存储系统
Kafka把消息持久化到磁盘,相比于其他基于内存存储的系统而言,有效地降低了数据丢失的风险。也正是得益于Kafka的消息持久化功能和多副本机制,我们可以把Kafka作为长期的数据存储系统来使用,只需要把对应的数据保留策略设置为"永久"或启用主题的日志压缩功能即可。 - 3、流式处理平台
Kafka不仅为每个流行的流式处理框架提供了可靠的数据来源,还提供了一个完整的流式处理类库,比如窗口、连接、变换和聚合等各类操作。
1.5 消费者是推还是拉
Kafka遵循了一种大部分消息系统共同的传统的设计:Producer将消息推送到Broker,Consumer从Broker拉取消息。
Pull模式的一个好处:Consumer可以自主决定是否批量的从Broker拉取数据。Push模式必须在不知道下游Consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免Consumer崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull模式下,Consumer可以根据自己的消费能力去决定这些策略。
Pull有个缺点:如果Broker没有可供消费的消息,将导致Consumer不断在循环中轮询,直到新消息到达。
1.6 Kafka与传统MQ之间的区别
1、Kafka持久化日志,这些日志可以被重复读取和无限期保留。
2、Kafka是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性。
3、Kafka支持实时的流式处理。
二、Kafka集群
2.1 副本机制
复制功能是 Kafka 架构的核心功能,在 Kafka 文档里面 Kafka 把自己描述为 一个分布式的、可分区的、可复制的提交日志服务。复制之所以这么关键,是因为消息的持久存储非常重要,这能够保证在主节点宕机后依旧能够保证 Kafka 高可用。副本机制也可以称为备份机制(Replication),通常指分布式系统在多台网络交互的机器上保存有相同的数据备份/拷贝。
Kafka 使用Topic来组织数据,每个Topic又被分为若干个分区,分区会部署在一到多个 broker 上,每个分区都会有多个副本,所以副本也会被保存在 broker 上,每个 broker 可能会保存成千上万个副本。
上图所示,有两个 broker ,每个 broker 指保存了一个 Topic 的消息,在 broker1 中分区 0 是 Leader,它负责进行分区的复制工作,把 broker1 中的分区 0 复制一个副本到 broker2 的主题 A 的分区 0。同理,主题 A 的分区 1 也是一样的道理。
副本类型分为两种:一种是 Leader(领导者) 副本,一种是Follower(跟随者)副本。
Leader 副本 :Kafka 在创建分区的时候都要选举一个副本,这个选举出来的副本就是 Leader 领导者副本。
Follower 副本 :除了 Leader 副本以外的副本统称为 Follower 副本,Follower 不对外提供服务。
Kafka 中,Follower 副本也就是追随者副本是不对外提供服务的。这就是说,任何一个追随者副本都不能响应消费者和生产者的请求。所有的请求都是由领导者副本来处理。或者说,所有的请求都必须发送到 Leader 副本所在的 broker 中,Follower 副本只是用做数据拉取,采用异步拉取的方式,并写入到自己的提交日志中,从而实现与 Leader 的同步。
当 Leader 副本所在的 broker 宕机后,Kafka 依托于 ZooKeeper 提供的监控功能能够实时感知到,并开启新一轮的选举,从追随者副本中选一个作为 Leader。如果宕机的 broker 重启完成后,该分区的副本会作为 Follower 重新加入。
首领的另一个任务是搞清楚哪个跟随者的状态与自己是一致的。跟随者为了保证与领导者的状态一致,在有新消息到达之前先尝试从领导者那里复制消息。
跟随者副本在收到响应消息前,是不会继续发送消息,这一点很重要。通过查看每个跟随者请求的最新偏移量,首领就会知道每个跟随者复制的进度。如果跟随者在 10s 内没有请求任何消息,或者虽然跟随者已经发送请求,但是在 10s 内没有收到消息,就会被认为是不同步的。如果一个副本没有与领导者同步,那么在领导者掉线后,这个副本将不会称为领导者,因为这个副本的消息不是全部的。
如果跟随者同步的消息和领导者副本的消息一致,那么这个跟随者副本又被称为同步的副本。也就是说,如果领导者掉线,那么只有同步的副本能够称为领导者。
副本机制的好处:
- 能够立刻看到写入的消息,就是你使用生产者 API 成功向分区写入消息后,马上使用消费者就能读取刚才写入的消息。
- 能够实现消息的幂等性。就是对于生产者产生的消息,在消费者进行消费的时候,它每次都会看到消息存在,并不会存在消息不存在的情况。
- 同步复制和异步复制
跟随者副本在同步领导者副本后会把消息保存在本地 log 中,这个时候跟随者会给领导者副本一个响应消息,告诉领导者自己已经保存成功了,同步复制的领导者会等待所有的跟随者副本都写入成功后,再返回给 producer 写入成功的消息。而异步复制是领导者副本不需要关心跟随者副本是否写入成功,只要领导者副本自己把消息保存到本地 log ,就会返回给 producer 写入成功的消息。
同步复制:
producer 通知 ZooKeeper 识别领导者。
producer 向领导者写入消息。
领导者收到消息后会把消息写入到本地 log。
跟随者会从领导者那里拉取消息。
跟随者向本地写入 log。
跟随者向领导者发送写入成功的消息。
领导者会收到所有的跟随者发送的消息。
领导者向 producer 发送写入成功的消息。
异步复制:和同步复制的区别在于,领导者在写入本地 log 之后,直接向客户端发送写入成功消息,不需要等待所有跟随者复制完成。
- ISR
Kafka 动态维护了一个同步状态的副本的集合(a set of In-Sync Replicas),简称ISR,ISR 也是一个很重要的概念,我们之前说过,追随者副本不提供服务,只是定期的异步拉取领导者副本的数据而已,拉取这个操作就相当于是复制,ctrl-c + ctrl-v大家肯定用的熟。那么是不是说 ISR 集合中的副本消息的数量都会与领导者副本消息数量一样呢?那也不一定,判断的依据是 broker 中参数 replica.lag.time.max.ms 的值,这个参数的含义就是跟随者副本能够落后领导者副本最长的时间间隔。
replica.lag.time.max.ms
参数默认的时间是 10 秒,如果跟随者副本落后领导者副本的时间不超过 10 秒,那么 Kafka 就认为领导者和跟随者是同步的。即使此时跟随者副本中存储的消息要小于领导者副本。如果跟随者副本要落后于领导者副本 10 秒以上的话,跟随者副本就会从 ISR 被剔除。倘若该副本后面慢慢地追上了领导者的进度,那么它是能够重新被加回 ISR 的。这也表明,ISR 是一个动态调整的集合,而非静态不变的。 - Unclean 副本领导者选举
既然 ISR 是可以动态调整的,那么必然会出现 ISR 集合中为空的情况,由于领导者副本是一定出现在 ISR 集合中的,那么 ISR 集合为空必然说明领导者副本也挂了,所以此时 Kafka 需要重新选举一个新的领导者,那么该如何选举呢?现在你需要转变一下思路,我们上面说 ISR 集合中一定是与领导者同步的副本,那么不再 ISR 集合中的副本一定是不与领导者同步的副本了,也就是不再 ISR 列表中的跟随者副本会丢失一些消息。如果你开启 broker 端参数 unclean.leader.election.enable的话,下一个领导者就会在这些非同步的副本中选举。这种选举也叫做Unclean 领导者选举。
这种 Unclean 领导者选举其实是牺牲了数据一致性,保证了 Kafka 的高可用性。你可以根据你的实际业务场景决定是否开启 Unclean 领导者选举,一般不建议开启这个参数,因为数据的一致性要比可用性重要的多。
2.2 控制器机制
broker 之间有一个控制器组件(Controller),它是 Kafka 的核心组件。它的主要作用是在 ZooKeeper 的帮助下管理和协调整个 Kafka 集群,集群中的每个 broker 都可以称为 controller。
- 控制器的选举
Kafka 当前选举控制器的规则是:Kafka 集群中第一个启动的 broker 通过在 ZooKeeper 里创建一个临时节点 /controller 让自己成为 controller 控制器。其他 broker 在启动时也会尝试创建这个节点,但是由于这个节点已存在,所以后面想要创建 /controller 节点时就会收到一个 节点已存在 的异常。然后其他 broker 会在这个控制器上注册一个 ZooKeeper 的 watch 对象,/controller节点发生变化时,其他 broker 就会收到节点变更通知。这种方式可以确保只有一个控制器存在。那么只有单独的节点一定是有个问题的,那就是单点问题。
如果控制器关闭或者与 ZooKeeper 断开链接,ZooKeeper 上的临时节点就会消失。集群中的其他节点收到 watch 对象发送控制器下线的消息后,其他 broker 节点都会尝试让自己去成为新的控制器。其他节点的创建规则和第一个节点的创建原则一致,都是第一个在 ZooKeeper 里成功创建控制器节点的 broker 会成为新的控制器,那么其他节点就会收到节点已存在的异常,然后在新的控制器节点上再次创建 watch 对象进行监听。
- controller 故障转移
broker controller 故障转移主要依赖于zookeeper。一开始,broker1 会抢先注册成功成为 controller,然后由于网络抖动或者其他原因致使 broker1 掉线,ZooKeeper 通过 Watch 机制觉察到 broker1 的掉线,之后所有存活的 brokers 开始竞争成为 controller,这时 broker3 抢先注册成功,此时 ZooKeeper 存储的 controller 信息由 broker1 -> broker3,之后,broker3 会从 ZooKeeper 中读取元数据信息,并初始化到自己的缓存中。
三、Kafka效率高的原因
3.1 利用Partition实现并行处理
Kafka是一个的消息系统,无论是发布还是订阅,都要指定Topic。Topic只是一个逻辑的概念。每个Topic都包含一个或多个Partition,不同Partition可位于不同节点。
一方面,由于不同 Partition 可位于不同机器,因此可以充分利用集群优势,实现机器间的并行处理。另一方面,由于 Partition 在物理上对应一个文件夹,即使多个Partition位于同一个节点,也可通过配置让同一节点上的不同 Partition 置于不同的磁盘上,从而实现磁盘间的并行处理,充分发挥多磁盘的优势。
3.2 顺序写磁盘
Kafka 中每个分区是一个有序的,不可变的消息序列,新的消息不断追加到 partition 的末尾,这个就是顺序写。
由于磁盘有限,不可能保存所有数据,实际上作为消息系统 Kafka 也没必要保存所有数据,需要删除旧的数据。又由于顺序写入的原因,所以 Kafka 采用各种删除策略删除数据的时候,并非通过使用"读 - 写"模式去修改文件,而是将 Partition 分为多个 Segment,每个 Segment 对应一个物理文件,通过删除整个文件的方式去删除 Partition 内的数据。这种方式清除旧数据的方式,也避免了对文件的随机写操作。
3.3 充分利用 Page Cache
引入 Cache 层的目的是为了提高 Linux 操作系统对磁盘访问的性能。Cache 层在内存中缓存了磁盘上的部分数据。当数据的请求到达时,如果在 Cache 中存在该数据且是最新的,则直接将数据传递给用户程序,免除了对底层磁盘的操作,提高了性能。Cache 层也正是磁盘 IOPS 为什么能突破 200 的主要原因之一。
在 Linux 的实现中,文件 Cache 分为两个层面,一是 Page Cache,另一个 Buffer Cache,每一个 Page Cache 包含若干 Buffer Cache。Page Cache 主要用来作为文件系统上的文件数据的缓存来用,尤其是针对当进程对文件有 read/write 操作的时候。Buffer Cache 则主要是设计用来在系统对块设备进行读写的时候,对块进行数据缓存的系统来使用。
使用 Page Cache 的好处:
I/O Scheduler 会将连续的小块写组装成大块的物理写从而提高性能。
I/O Scheduler 会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间。
充分利用所有空闲内存(非 JVM 内存)。如果使用应用层 Cache(即 JVM 堆内存),会增加 GC 负担。
读操作可直接在 Page Cache 内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过 Page Cache)交换数据。
如果进程重启,JVM 内的 Cache 会失效,但 Page Cache 仍然可用。
Broker 收到数据后,写磁盘时只是将数据写入 Page Cache,并不保证数据一定完全写入磁盘。从这一点看,可能会造成机器宕机时,Page Cache 内的数据未写入磁盘从而造成数据丢失。但是这种丢失只发生在机器断电等造成操作系统不工作的场景,而这种场景完全可以由 Kafka 层面的 Replication 机制去解决。如果为了保证这种情况下数据不丢失而强制将 Page Cache 中的数据 Flush 到磁盘,反而会降低性能。也正因如此,Kafka 虽然提供了 flush.messages 和 flush.ms 两个参数将 Page Cache 中的数据强制 Flush 到磁盘,但是 Kafka 并不建议使用。
3.4 零拷贝技术
Kafka 中存在大量的网络数据持久化到磁盘(Producer 到 Broker)和磁盘文件通过网络发送(Broker 到 Consumer)的过程。这一过程的性能直接影响 Kafka 的整体吞吐量。
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。
为了避免用户进程直接操作内核,保证内核安全,操作系统将虚拟内存划分为两部分,一部分是内核空间(Kernel-space),一部分是用户空间(User-space)。
传统的 Linux 系统中,标准的 I/O 接口(例如 read,write)都是基于数据拷贝操作的,即 I/O 操作会导致数据在内核地址空间的缓冲区和用户地址空间的缓冲区之间进行拷贝,所以标准 I/O 也被称作缓存 I/O。这样做的好处是,如果所请求的数据已经存放在内核的高速缓冲存储器中,那么就可以减少实际的 I/O 操作,但坏处就是数据拷贝的过程,会导致 CPU 开销。
3.5 数据压缩
Producer 可将数据压缩后发送给 broker,从而减少网络传输代价,目前支持的压缩算法有:Snappy、Gzip、LZ4。数据压缩一般都是和批处理配套使用来作为优化手段的。
四、相关问题
4.1 Kafka 中的分区器、序列化器、拦截器,它们之间的处理顺序是什么
序列化器
:生产者需要用序列化器(Serializer)把对象转换成字节数组才能通过网络发送给Kafka。而在对侧,消费者需要用反序列化器(Deserializer)把从Kafka中收到的字节数组转换成相应的对象。
分区器
:分区器的作用就是为消息分配分区。如果消息ProducerRecord中没有指定partition字段,那么就需要依赖分区器,根据key这个字段来计算partition的值。
Kafka一共有两种拦截器:生产者拦截器和消费者拦截器。
生产者拦截器
既可以用来在消息发送前做一些准备工作,比如按照某个规则过滤不符合要求的消息、修改消息的内容等,也可以用来在发送回调逻辑前做一些定制化的需求,比如统计类工作。
消费者拦截器
主要在消费到消息或在提交消费位移时进行一些定制化的操作。
消息在通过send方法发往broker的过程中,有可能需要经过拦截器、序列化器和分区器的一系列作用之后才能被真正地发往broker。拦截器一般不是必需的,而序列化器是必需的。消息经过序列化之后就需要确定它发往的分区,如果消息ProducerRecord中指定了partition字段,那么就不需要分区器的作用,因为partition代表的就是所要发往的分区号。
处理顺序 :拦截器->序列化器->分区器。
KafkaProducer在将消息序列化和计算分区之前会调用生产者拦截器的onSend方法来对消息进行相应的定制化操作。然后生产者需要用序列化器(Serializer)把对象转换成字节数组才能通过网络发送给Kafka。最后可能会被发往分区器为消息分配分区。
- Kafka中生产者客户端的整体结构
整个生产者客户端由两个线程协调运行,这两个线程分别为主线程和Sender线程(发送线程)。
在主线程中由KafkaProducer创建消息,然后通过可能的拦截器、序列化器和分区器的作用之后缓存到消息累加器(RecordAccumulator,也称为消息收集器)中。
Sender线程负责从RecordAccumulator中获取消息并将其发送到Kafka中。
RecordAccumulator主要用来缓存消息以便Sender线程可以批量发送,进而减少网络传输的资源消耗以提升性能。
4.2 有哪些情形会造成重复消费
Rebalance
:一个consumer正在消费一个分区的一条消息,还没有消费完,发生了rebalance(加入了一个consumer),从而导致这条消息没有消费成功。rebalance后,另一个consumer又把这条消息消费一遍。
消费者端手动提交
:如果先消费消息,再更新offset位置,导致消息重复消费。
消费者端自动提交
:设置offset为自动提交,关闭kafka时,如果在close之前,调用consumer.unsubscribe(),则有可能部分offset没提交,下次重启会重复消费。
生产者端
:生产者因为业务问题导致的宕机,在重启之后可能数据会重发。
4.3 有哪些情形会造成消息漏消费
自动提交
:设置offset为自动定时提交,当offset被自动定时提交时,数据还在内存中未处理,此时刚好把线程kill掉,那么offset已经提交,但是数据未处理,导致这部分内存中的数据丢失。
生产者发送消息
:发送消息设置的是fire-and-forget(发后即忘),它只管往Kafka中发送消息而并不关心消息是否正确到达。不过在某些时候(比如发生不可重试异常时)会造成消息的丢失。这种发送方式的性能最高,可靠性也最差。
消费者端
:先提交位移,但是消息还没消费完就宕机了,造成了消息没有被消费。
acks没有设置为all
:如果在broker还没把消息同步到其他broker的时候宕机了,那么消息将会丢失。
4.4 Kafka消息是采用Pull模式,还是Push模式
Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息
。
一些消息系统比如Scribe和Apache Flume采用了push模式,将消息推送到下游的consumer。这样做有好处也有坏处:由broker决定消息推送的速率,对于不同消费速率的consumer就不太好处理了。消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是,push模式下,当broker推送的速率远大于 consumer 消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式。
Pull模式的另外一个好处是 consumer 可以自主决定是否批量的从 broker 拉取数据。Push模式必须在不知道下游 consumer 消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免 consumer 崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull 模式下,consumer 就可以根据自己的消费能力去决定这些策略。
4.5 Kafka的那些设计让它有如此高的性能?
- 1、分区
kafka是个分布式集群的系统,整个系统可以包含多个 broker,也就是多个服务器实例。每个主题 topic 会有多个分区,kafka 将分区均匀地分配到整个集群中,当生产者向对应主题传递消息,消息通过负载均衡机制传递到不同的分区以减轻单个服务器实例的压力。
一个 Consumer Group 中可以有多个 consumer,多个 consumer 可以同时消费不同分区的消息,大大的提高了消费者的并行消费能力。但是一个分区中的消息只能被一个 Consumer Group 中的一个 consumer 消费。
同一 Consumer Group 中的多个 Consumer 实例,不同时消费同一个 partition,等效于队列模式。 partition 内消息是有序的, Consumer 通过 pull 方式消费消息。 Kafka 不删除已消费的消息对于 partition,顺序读写磁盘数据,以时间复杂度 O(1)方式提供消息持久化能力。 - 2、网络传输上减少开销
批量发送
:在发送消息的时候,kafka 不会直接将少量数据发送出去,否则每次发送少量的数据会增加网络传输频率,降低网络传输效率。kafka 会先将消息缓存在内存中,当超过一个的大小或者超过一定的时间,那么会将这些消息进行批量发送。
端到端压缩
:当然网络传输时数据量小也可以减小网络负载,kafaka 会将这些批量的数据进行压缩,将一批消息打包后进行压缩,发送 broker 服务器后,最终这些数据还是提供给消费者用,所以数据在服务器上还是保持压缩状态,不会进行解压,而且频繁的压缩和解压也会降低性能,最终还是以压缩的方式传递到消费者的手上。 - 3、顺序读写
kafka 将消息追加到日志文件中,利用了磁盘的顺序读写,来提高读写效率。 - 4、零拷贝技术
零拷贝将文件内容从磁盘通过 DMA 引擎复制到内核缓冲区,而且没有把数据复制到 socket缓冲区,只是将数据位置和长度信息的描述符复制到了 socket 缓存区,然后直接将数据传输到网络接口,最后发送。这样大大减小了拷贝的次数,提高了效率。kafka 正是调用 linux系统给出的 sendfile 系统调用来使用零拷贝。Java 中的系统调用给出的是FileChannel.transferTo 接口。 - 5、优秀的文件存储机制
如果分区规则设置得合理,那么所有的消息可以均匀地分布到不同的分区中,这样就可以实现水平扩展。不考虑多副本的情况,一个分区对应一个日志(Log)。为了防止 Log 过大,Kafka 又引入了日志分段(LogSegment)的概念,将 Log 切分为多个 LogSegment,相当于一个巨型文件被平均分配为多个相对较小的文件,这样也便于消息的维护和清理。
Kafka 中的索引文件以稀疏索引(sparse index)的方式构造消息的索引,它并不保证每个消息在索引文件中都有对应的索引项。每当写入一定量(由 broker 端参数 log.index.interval.bytes 指定,默认值为4096,即 4KB)的消息时,偏移量索引文件和时间戳索引文件分别增加一个偏移量索引项和时间戳索引项,增大或减小 log.index.interval.bytes 的值,对应地可以增加或缩小索引项的密度。
4.6 Kafka为什么要抛弃Zookeeper
- ZooKeeper 的作用
ZooKeeper 是一个开源的分布式协调服务框架,你也可以认为它是一个可以保证一致性的分布式(小量)存储系统。特别适合存储一些公共的配置信息、集群的一些元数据等等。它有持久节点和临时节点,而临时节点这个玩意再配合 Watcher 机制就很有用。当创建临时节点的客户端与 ZooKeeper 断连之后,这个临时节点就会消失,并且订阅了节点状态变更的客户端会收到这个节点状态变更的通知。
集群中某一服务上线或者下线,都可以被检测到。因此可以用来实现服务发现,也可以实现故障转移的监听机制。
Kafka 就是强依赖于 ZooKeeper,没有 ZooKeeper 的话 Kafka 都无法运行。ZooKeeper 为 Kafka 提供了元数据的管理,例如一些 Broker 的信息、主题数据、分区数据等等。在每个 Broker 启动的时候,都会和 ZooKeeper 进行交互,这样 ZooKeeper 就存储了集群中所有的主题、配置、副本等信息。
还有一些选举、扩容等机制也都依赖 ZooKeeper 。例如控制器的选举:每个 Broker 启动都会尝试在 ZooKeeper 注册 /controller 临时节点来竞选控制器,第一个创建 /controller 节点的 Broker 会被指定为控制器。竞争失败的节点也会依赖 watcher 机制,监听这个节点,如果控制器宕机了,那么其它 Broker 会继续来争抢,实现控制器的 failover。 - 为什么要抛弃 ZooKeeper
首先身为一个中间件,需要依赖另一个中间件,这就感觉有点奇怪。你要说依赖 Netty 这种,那肯定是没问题的。但是 Kafka 的运行需要提供 ZooKeeper 集群,这其实有点怪怪的。等于如果你公司要上 Kafka 就得跟着上 ZooKeeper ,被动了增加了运维的复杂度。所以运维人员不仅得照顾 Kafka 集群,还得照顾 ZooKeeper 集群。
ZooKeeper 有个特点,强一致性。如果 ZooKeeper 集群的某个节点的数据发生变更,则会通知其它 ZooKeeper 节点同时执行更新,就得等着大家(超过半数)都写完了才行,这写入的性能就比较差了。
一般而言,ZooKeeper 只适用于存储一些简单的配置或者是集群的元数据,不是真正意义上的存储系统。
如果写入的数据量过大,ZooKeeper 的性能和稳定性就会下降,可能导致 Watch 的延时或丢失。所以在 Kafka 集群比较大,分区数很多的时候,ZooKeeper 存储的元数据就会很多,性能就差了。还有,ZooKeeper 也是分布式的,也需要选举,它的选举也不快,而且发生选举的那段时候是不提供服务的。
以前 Consumer 的位移数据是保存在 ZooKeeper 上的,所以当提交位移或者获取位移的时候都需要访问 ZooKeeper ,这量一大 ZooKeeper 就顶不住。所以后面引入了位移主题(Topic是__consumer_offsets),将位移的提交和获取当做消息一样来处理,存储在日志中,避免了频繁访问 ZooKeeper 性能差的问题。 - 没了 Zookeeper 之后的 Kafka 的怎样的
没了 Zookeeper 的 Kafka 把元数据就存储到自己内部了,利用之前的 Log 存储机制来保存元数据。就和上面说到的位移主题一样,会有一个元数据主题,元数据会像普通消息一样保存在 Log 中。所以元数据和之前的位移一样,利用现有的消息存储机制稍加改造来实现了功能。然后还搞了个 KRaft 来实现 Controller Quorum。
这个协议是基于 Raft 的,就理解为它能解决 Controller Leader 的选举,并且让所有节点达成共识。
在之前基于 Zookeeper 实现的单个 Controller 在分区数太大的时候还有个问题,故障转移太慢了。当 Controller 变更的时候,需要重新加载所有的元数据到新的 Controller 身上,并且需要把这些元数据同步给集群内的所有 Broker。
而 Controller Quorum 中的 Leader 选举切换则很快,因为元数据都已经在 quorum 中同步了,也就是 quorum 的 Broker 都已经有全部了元数据,所以不需要重新加载元数据。并且其它 Broker 已经基于 Log 存储了一些元数据,所以只需要增量更新即可,不需要全量了。这波改造下来就解决了之前元数据过多的问题,可以支持更多的分区。
4.7 Kafka 如何保证可靠性
Kafka 中的可靠性保证有如下四点:
- 对于一个分区来说,它的消息是有序的。如果一个生产者向一个分区先写入消息A,然后写入消息B,那么消费者会先读取消息A再读取消息B。
- 当消息写入所有in-sync状态的副本后,消息才会认为已提交(committed)。这里的写入有可能只是写入到文件系统的缓存,不一定刷新到磁盘。生产者可以等待不同时机的确认,比如等待分区主副本写入即返回,后者等待所有in-sync状态副本写入才返回。
- 一旦消息已提交,那么只要有一个副本存活,数据不会丢失。
- 消费者只能读取到已提交的消息。
4.8 Kafka中的延迟队列怎么实现
在发送延时消息的时候并不是先投递到要发送的真实主题(real_topic)中,而是先投递到一些Kafka内部的主题(delay_topic)中,这些内部主题对用户不可见,然后通过一个自定义的服务拉取这些内部主题中的消息,并将满足条件的消息再投递到要发送的真实的主题中,消费者所订阅的还是真实的主题。
如果采用这种方案,那么一般是按照不同的延时等级来划分的,比如设定 5s、10s、30s、1min、2min、5min、10min、20min、30min、45min、1hour、2hour 这些按延时时间递增的延时等级,延时的消息按照延时时间投递到不同等级的主题中,投递到同一主题中的消息的延时时间会被强转为与此主题延时等级一致的延时时间,这样延时误差控制在两个延时等级的时间差范围之内(比如延时时间为17s的消息投递到30s的延时主题中,之后按照延时时间为30s进行计算,延时误差为13s)。虽然有一定的延时误差,但是误差可控,并且这样只需增加少许的主题就能实现延时队列的功能。
发送到内部主题(delay_topic_*)中的消息会被一个独立的DelayService进程消费,这个DelayService进程和Kafka broker进程以一对一的配比进行同机部署(参考下图),以保证服务的可用性。
针对不同延时级别的主题,在 DelayService 的内部都会有单独的线程来进行消息的拉取,以及单独的 DelayQueue(这里用的是 JUC 中 DelayQueue)进行消息的暂存。与此同时,在DelayService内部还会有专门的消息发送线程来获取 DelayQueue 的消息并转发到真实的主题中。从消费、暂存再到转发,线程之间都是一一对应的关系。如下图所示,DelayService的设计应当尽量保持简单,避免锁机制产生的隐患。
为了保障内部 DelayQueue 不会因为未处理的消息过多而导致内存的占用过大,DelayService 会对主题中的每个分区进行计数,当达到一定的阈值之后,就会暂停拉取该分区中的消息。因为一个主题中一般不止一个分区,分区之间的消息并不会按照投递时间进行排序,DelayQueue的作用是将消息按照再次投递时间进行有序排序,这样下游的消息发送线程就能够按照先后顺序获取最先满足投递条件的消息。
4.9 简述Kafka的日志目录结构
Kafka中的消息是以主题为基本单位进行归类的,各个主题在逻辑上相互独立。每个主题又可以分为一个或多个分区。不考虑多副本的情况,一个分区对应一个日志(Log)。为了防止Log过大,Kafka又引入了日志分段(LogSegment)的概念,将Log切分为多个LogSegment,相当于一个巨型文件被平均分配为多个相对较小的文件。
Log和LogSegment也不是纯粹物理意义上的概念,Log在物理上只以文件夹的形式存储,而每个LogSegment对应于磁盘上的一个日志文件和两个索引文件,以及可能的其他文件(比如以".txnindex"为后缀的事务索引文件)。
4.10 Kafka中有哪些索引文件
每个日志分段文件对应了两个索引文件,主要用来提高查找消息的效率。
偏移量索引文件
:用来建立消息偏移量(offset)到物理地址之间的映射关系,方便快速定位消息所在的物理文件位置。
时间戳索引文件
:则根据指定的时间戳(timestamp)来查找对应的偏移量信息。
- 如果指定了一个offset,Kafka怎么查找到对应的消息
Kafka是通过seek方法来指定消费的,在执行seek方法之前要去执行一次 poll方法,等到分配到分区之后会去对应的分区的指定位置开始消费,如果指定的位置发生了越界,那么会根据auto.offset.reset参数设置的情况进行消费。 - 如果指定了一个timestamp,Kafka怎么查找到对应的消息
Kafka提供了一个offsetsForTimes方法,通过timestamp来查询与此对应的分区位置。offsetsForTimes方法的参数timestampsToSearch是一个Map类型,key为待查询的分区,而value为待查询的时间戳,该方法会返回时间戳大于等于待查询时间的第一条消息对应的位置和时间戳,对应于OffsetAndTimestamp中的offset和timestamp字段。