kafka原理

kafka 的整体结构

在这里插入图片描述
图中展示出了 kafka 的一些重要组件,接下来逐个介绍一下。

(一)Broker

服务代理节点。其实就是一个 kafka 实例或服务节点,多个 broker 构成了 kafka cluster。

(二)Producer

生产者。也就是写入消息的一方,将消息写入 broker 中。

(三)Consumer

消费者。也就是读取消息的一方,从 broker 中读取消息。

(四)Consumer Group

消费组。一个或多个消费者构成一个消费组,不同的消费组可以订阅同一个主题的消息且互不影响。

(五)ZooKeeper kafka 使用 zookeeper 来管理集群的元数据,以及控制器的选举等操作。

(六)Topic

主题。每一个消息都属于某个主题,kafka 通过主题来划分消息,是一个逻辑上的分类。

(七)Partition

分区。同一个主题下的消息还可以继续分成多个分区,一个分区只属于一个主题。

(八)Replica

副本。一个分区可以有多个副本来提高容灾性。

(九)Leader and Follower

分区有了多个副本,那么就需要有同步方式。kafka 使用一主多从进行消息同步,主副本提供读写的能力,而从副本不提供读写,仅仅作为主副本的备份。

(十)Offset

偏移。分区中的每一条消息都有一个所在分区的偏移量,这个偏移量唯一标识了该消息在当前这个分区的位置,并保证了在这个分区的顺序性,不过不保证跨分区的顺序性。

简单来说,作为消息系统的 kafka 本质上还是一个数据系统。既然是一个数据系统,那么就要解决两个根本问题:

当我们把数据交给 kafka 的时候,kafka 怎么存储;
当我们向 kafka 要回数据的时候,kafka 怎么返回。

在这里插入图片描述

消息如何存储 (逻辑层面)

目前大多数数据系统将数据存储在磁盘的格式有追加日志型以及 B + 树型。而 kafka 采用了追加日志的格式将数据存储在磁盘上,整体的结构如下图:
在这里插入图片描述
追加日志的格式可以带来写性能的提升(毕竟只需要往日志文件后面追加就可以了),但是同时对读的支持不是很友好。为了提升读性能,kafka 需要额外的操作。

关于 kafka 的数据是如何存储的是一个比较大的问题,这里先从逻辑层面开始。

(一)Topic+Partition 的两层结构

kafka 对消息进行了两个层级的分类,分别是 topic 主题和 partition 分区。

将一个主题划分成多个分区的好处是显而易见的。多个分区可以为 kafka 提供可伸缩性、水平扩展的能力,同时对分区进行冗余还可以提高数据可靠性。

不同的分区还可以部署在不同的 broker 上,加上冗余副本就提高了可靠性。

(二)Offset

对于追加日志格式,新来的数据只需要往文件末尾追加即可。

对于有多个分区的主题来说,每一个消息都有对应需要追加到的分区(分区器),这个消息在所在的分区中都有一个唯一标识,就是 offset 偏移量:
在这里插入图片描述
这样的结构具有如下的特点:

分区提高了写性能,和数据可靠性;
消息在分区内保证顺序性,但跨分区不保证。
逻辑层面上知道了 kafka 是如何存储消息之后,再来看看作为使用者,如何写入以及读取数据。

如何写入数据

接下来从使用者的角度来看看,如何将数据写入 kafka。

(一)整体流程

生产者将消息写入 kafka 的整体流程如下图:
在这里插入图片描述
在生产端主要有两个线程:main 和 sender,两者通过共享内存 RecordAccumulator 通信。

各步骤如下:

KafkaProducer 创建消息;
1、生产者拦截器在消息发送之前做一些准备工作,比如过滤不符合要求的消息、修改消息的内容等;
2、序列化器将消息转换成字节数组的形式;
3、分区器计算该消息的目标分区,然后数据会存储在 RecordAccumulator 中;
4、发送线程获取数据进行发送;
5、创建具体的请求;
6、如果请求过多,会将部分请求缓存起来;
7、将准备好的请求进行发送;
8、发送到 kafka 集群;
9、接收响应;
10、清理数据。

在消息累加器 RecordAccumulator 中来进行缓存,缓存大小通过参数 buffer.memory 配置,默认 32MB。累加器根据分区来管理每一个消息,其中消息又被组织成 ProducerBatch 的形式(通过 batch.size 控制大小,默认 1MB),为了提高吞吐量降低网络请求次数,ProducerBatch 中可能包含一个或多个消息。

当消息不多时一个 Batch 可能没有填满,但不会等待太长时间,可以通过 linger.ms 控制等待时间,默认 0。增大这个值可以提高吞吐量,但是会增加延迟。

当生产消息的速度过快导致缓存满了的时候,继续发送消息可能会有阻塞或异常,通过参数 max.block.ms 控制,默认 60 秒。

数据到达发送线程创建好请求之后,需要对其进行重新组合,根据需要发送到的 broker 节点分组,每个节点就是一个连接,每个连接可以缓存的请求数通过 max.in.flight.requests.per.connection 控制,默认 5。每个请求的大小通过 max.reqeust.size 控制,默认 1MB。

(二)发送方式

消息的发送有三种方式:

发后即忘(fire and forget):只管发送不管结果,性能最高,可靠性也最差;
同步(sync):等集群确认消息写入成功再返回,可靠性最高,性能差很多;
异步(async):指定一个 callback,kafka 返回响应后调用来实现异步发送的确认。
其中前两个是同步发送,后一个是异步发送。不过这里的异步发送没有提供 callback 的能力。

那么生产者发送消息之后 kafka 怎么才算确认呢?这涉及到 acks 参数:

acks = 1, 默认值 1,表示只要分区的 leader 副本成功写入就算成功;
acks=0,生产者不需要等待任何服务端的响应,可能会丢失数据;
acks=-1 或 acks=all,需要全部处于同步状态的副本确认写入成功,可靠性最强,性能也差。

如何读取消息

(一)消费消息

消费模式

消息的消费一般来说有两种模式:推模式和拉模式,而 kafka 中的消费是基于拉模式的。消费者通过不断地调用 poll 来获取消息进行消费,基本模式如下(伪代码):

while(true) {

    records := consumer.Pull()

     for record := range records {

          // do something with record

     }

}
位移提交

kafka 中的消息都有一个 offset 唯一标识,对于消费者来说,每消费完一个消息需要通知 kafka,这样下次拉取消息的时候才不会拉到已消费的数据(不考虑重复消费的情况)。这个消费者已消费的消息位置就是消费位移,比如:
在这里插入图片描述

假设 9527 当前拉取到消息的最大偏移量且已经消费完,那么这个消费者的消费位移就是 9527,而要提交的消费位移是 9528,表示下一条需要拉取的消息的位置。

消费者一次可能拉取到多条消息,那么就会有一个提交的方式问题。kafka 默认使用的是自动提交,即五秒自动将拉到的每个分区中最大的消息位移(相关参数是 enable.auto.commit 和 auto.commit.interval.ms)。不过这可能导致重复消费以及数据丢失的问题

重复消费

在这里插入图片描述
上一次提交的消费位移是 9527,说明 9526 及之前的消息都已经被消费了;当前这次 pull 拉取到的消息是 9527、0528 和 9529,因此,这次消费成功后要提交的唯一就是 9530;消费者当前正在处理消息 9528,如果此时消费者挂掉,如果此时还没有提交 9530,那么 9527 到 9529 之间的消息都会被分配到下一个消费者,导致消息 9527 重复处理。

下面看一下消息丢失。还是上面的图,如果消费者刚拉取到 9527 到 9529 这三个消息,刚好自动提交了 9530,而此时消费者挂了,那么还没有处理就提交了,导致这三条消息丢失。

(二)分区分配策略

消息在 kafka 的存储是分多个分区的,那么消费者消息分区的消息也就有一个分区分配策略。拿最开始的图来说就是下面 consumer group 这部分:
在这里插入图片描述

一共有三个分区,消费组 1 有四个消费组,所以有一个处于空闲状态;消费组 2 有两个消费组,所以有一个消费组需要处理两个分区。

kafka 消费者的分区分配策略通过参数 partition.assigment.strategy 来配置,有如下几种:

1、Range:按照消费者的总数和分区总数进行整除运算来分配,不过是按照主题粒度的,所以可能会不均匀。比如:
在这里插入图片描述
2、RoundRobin:将消费组内所有消费者及消费者订阅的所有主题的分区按照字典序排序,然后通过轮询方式这个将分区一次分配给每个消费者。比如:
在这里插入图片描述
3、Sticky:这个策略比较复杂,目的是分区的分配尽可能均匀,以及分配要尽可能和上次保持一致。

(三)再均衡

消费者之间的协调是通过消费者协调器(ConsumerCoordinator)和组协调器(GroupCoordinator)来完成的。其中一项就是消费者的再均衡。

下面几种情况会导致消费者再均衡的发生:

1、有新的消费者加入;
2、有消费者下线;
3、有消费者主动退出;
4、消费组对应的组协调器节点发生变化;
5、订阅的主题或分区发生数量变化。

再均衡会经过下面几个步骤:

1、FindCoordinator:消费者查找组协调器所在的机器,然后建立连接;
2、JoinGroup:消费者向组协调器发起加入组的请求;
3、SyncGroup:组协调器将分区分配方案同步给所有的消费者;
4、Heartbeat:消费者进入正常状态,开始心跳。

如何存储消息 (物理层面)

在前面介绍了逻辑层面 kafka 是如何存储数据的,接下来在物理层面继续。还是这张图:
在这里插入图片描述

(一)日志文件

kafka 使用日志追加的方式来存储数据,新来的数据只要往日志文件的末尾追加即可,这样的方式提高了写的性能。

但是文件也不能一直追加吧,因此,kafka 中的 log 文件对应着多个日志分段 LogSegment。

采用分段的方式方便对其进行清理。而 kafka 有两种日志清理策略:

日志删除(Log Retention):按照一定策略直接删除日志分段;
日志压缩(Log Compaction):对每个消息的 key 进行整合,只保留同一个 key 下最新的 value。
日志删除
日志删除策略有过期时间和日志大小。默认保留时间是 7 天,默认大小是 1GB。

虽然默认保留时间是 7 天,但是也有可能保留时间更长。因为当前活跃的日志分段是不会删除的,如果数据量很少,当前活跃日志分段一直没能继续拆分,那么就不会删除。

kafka 会有一个任务周期性地执行,对满足删除条件的日志进行删除。

日志压缩
日志压缩针对的是 key,具有相同 key 的多个 value 值只保留最近的一个。

同时,日志压缩会产生小文件,为了避免小文件过多,kafka 在清理的时候还会对其进行合并:

(二)日志索引

日志追加提高了写的性能,但是对于读就不是很友好了。为了提高读的性能,就需要降低一点写的性能,在读写之间做一点平衡。也就是在写的时候维护一个索引。

kafka 维护了两种索引:偏移量索引和时间戳索引。

偏移量索引

为了能够快速定位给定消息在日志文件中的位置,一个简单的办法就是维护一个映射,key 就是消息的偏移量,value 就是在日志文件中的偏移量,这样只需要一次文件读取就可以找到对应的消息了。

不过当消息量巨大的时候这个映射也会变很大,kafka 维护的是一个稀疏索引(sparse index),即不是所有的消息都有一个对应的位置,对于没有位置映射的消息来说,一个二分查找就可以解决了。

下图就是偏移量索引的原理:
在这里插入图片描述
比如要找 offset 是 37 的消息所在的位置,先看索引中没有对应的记录,就找不大于 37 的最大 offset 是 31,然后在日志中从 1050 开始按序查找 37 的消息。

时间戳索引

时间戳索引就是可以根据时间戳找到对应的偏移量。时间戳索引是一个二级索引,现根据时间戳找到偏移量,然后就可以使用偏移量索引找到对应的消息位置了。原理如下图:
在这里插入图片描述

(三)零拷贝 (重点)

kafka 将数据存储在磁盘上,同时使用日志追加的方式来提升性能。为了进一步提升性能,kafka 使用了零拷贝的技术。

零拷贝简单来说就是在内核态直接将文件内容复制到网卡设备上,减少了内核态与用户态之间的切换。

非零拷贝:

在这里插入图片描述

零拷贝:

在这里插入图片描述

kafka 的可靠性

kafka 通过多副本的方式实现水平扩展,提高容灾性以及可靠性等。这里看看 kafka 的多副本机制。
在这里插入图片描述

AR: Assigned Replicas

所有的副本统称为 AR。

ISR: In-Sync Replicas

ISR 是 AR 的一个子集,即所有和主副本保持同步的副本集合

OSR: Out-of-Sync Replicas

OSR 也是 AR 的一个子集,所有和主副本未保持一致的副本集合。所以 AR=ISR+OSR。

kafka 通过一些算法来判定从副本是否保持同步,处于失效的副本也可以通过追上主副本来重新进入 ISR。

LEO: Log End Offset

LEO 是下一个消息将要写入的 offset 偏移,在 LEO 之前的消息都已经写入日志了,每一个副本都有一个自己的 LEO。

HW: High Watermark

所有和主副本保持同步的副本中,最小的那个 LEO 就是 HW,这个 offset 意味着在这之前的消息都已经被所有的 ISR 写入日志了,消费者可以拉取了,这时即使主副本失效其中一个 ISR 副本成为主副本消息也不会丢失。

( 二)主副本 HW 与 LEO 的更新

LEO 和 HW 都是消息的偏移量,其中 HW 是所有 ISR 中最小的那个 LEO。下图展示了消息从生产者到主副本再同步到从副本的过程:
1、生产者将消息发送给 leader;
2、leader 追加消息到日志中,并更新自己的偏移量信息,同时 leader 也维护着 follower 的信息(比如 LEO 等);
3、follower 向 leader 请求同步,同时携带自己的 LEO 等信息;
4、leader 读取日志,拉取保存的每个 follower 的信息(LEO);
5、leader 将数据返回给 follower,同时还有自己的 HW;
6、follower 拿到数据之后追加到自己的日志中,同时根据返回的 HW 更新自己的 HW,方法就是取自己的 LEO 和 HW 的最小值。

从上面这个过程可以看出,一次同步过程之后 leader 的 HW 并没有增长,只有再经历一次同步,follower 携带上一次更新的 LEO 给 leader 之后,leader 才能更新 HW,这个时候才能能确认消息确实是被所有的 ISR 副本写入成功了。

leader 的 HW 很重要,因为这个值直接决定了消费者可以消费的数据。

(三)Leader Epoch

考虑下面的场景,初始时 leader 以保存了两条消息,此时 LEO=2,HW=1:

正在上传图片…

在 sync 1 中 follower 拉取数据,追加之后还需要再请求 leader 一次(sync 2)才能更新 leader 和 follower 的 HW。

这样在更新 HW 中就会有一个间隙,当 sync 1 成功之后 sync 2 之前 follower 挂掉了,那么重启之后的 HW 还是 1,follower 就会截断日志导致 m2 丢失,而此时 leader 也挂掉的话这个 follower 就会成为 leader,m2 就彻底丢失了(即使原来的 leader 重启之后也改变不了)。

为了解决这个问题,kafka 引入了 leader epoch 的概念,其实这就是一个版本号,在 follower 同步请求中不仅仅传递自己的 LEO,还会带上当前的 LE,当 leader 变更一次,这个值就会增 1。

由于有了 LE 的信息,follower 在崩溃重启之后就不会轻易截断日志,而是会请求最新的信息,避免了上述情况下数据丢失的问题。

Kafka为什么这么快

	1、kafka写入的时候是写入到page cache的不是直接写入到磁盘,操作系统自己决定什么时候把 os cache 里的数据真的刷入磁盘文件中。
	2、kafka采用的是顺序IO
	3、零拷贝技术
	4、分区分段+索引
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值