Kafka的数据结构与算法

目录

前言

kafka的分治思想

01

第1次分治

02

第2次分治

03

第3次分治

04

第4次分治

05

kafka的查询逻辑

结语


前言

    作为大数据时代背景下,流数据处理的明星组件,kafka的重要性不言而喻,网上解读kafka的文章可以说不计其数,有解释各种概念的,也有各种案例说明的。但是能讲到kafka核心干货的文章,却并不多见。

大家都知道它快,那它为什么快呢?快的原因是什么呢?

对于kafka的一些基本概念和基本知识点,这里不再赘述,本文将从kafka的数据结构和查找算法这个视角,来聊一聊kafka如何做到快的。

kafka的分治思想

任何存储组件,其数据最终都会落到磁盘上,比如我们最熟悉的MySQL,其在磁盘上的存储的文件大致分为数据文件和索引文件两大类。

kafka存储数据(消息本身)的文件的数据结构是数组,数组的特点就是:数据间位置连续,如果按照顺序读取,或者追加写入的话,其时间复杂度为O(1),效率最高。

那按照这个思路来想的话,是不是kafka的数据存储用一个数组的数据结构就能满足要求了呢?

试想,如果kafka设计只用一个数组来存储数据,一来如果数据量特别大时,对这个单一数组的无论是写入压力还是读取压力是不是都特别大;二来如果只用一个数组,那么它后台存储的数据只能用到一块磁盘,对这块磁盘的IO来说将是非常大的一个考验。

那么怎么办?

我们知道,针对大数据量的场景,要想快,有一个核心指导思想就是:分治

kafka正是采用了这种思想,并将该思想发挥到了极致的典型案例。

01

第1次分治

    kafka通过topic给用户提供数据的读写,对于不同的业务来说,可以定义不同的topic来达到数据分治的目的,不同的业务写入或者读取不同的topic,且不同的topic会尽可能分散在不同的broker中,提高数据的IO效率。

一个kafka实例可以创建多个topic

虽然kafka没有限制topic的个数,但是也不要盲目多建,因为越多的topic,代表着越多的数据存储单元,容易导致同一个topic的数据在磁盘存储位置的不连续,从而降低数据读写的IO。

02

第2次分治

对于kafka的topic,我们在创建之初可以设置多个partition来存放数据,对于同一个topic的数据,每条数据的key通过哈希取模被路由到不同的partition中(如果没有设置key,则根据消息本身取模),以此达到分治的目的。

同样,对于每个topic的partition数量来说,也不宜过多,因为partition是kafka管理数据的基本逻辑组织单元,越多的partition意味着越多的数据存储文件(一个partition对应至少3个数据文件),同样容易隔断磁盘数据的连续性,影响数据读写的IO性能。
另外,过多的partition还会导致broker的操作系统内存OOM,即每一个partition文件至少对应2个索引文件(至少1个.index文件和1个.indextime文件),而索引文件是需要常驻内存的,因此partition数量不宜过多。

官网给出的partition不宜过多的理由之一。

03

第3次分治

你以为将topic中的数据均匀分布到各个partition中就完了吗?

当然不是。

试想,如果你作为一个consumer,想从一个特定的位置进行数据消费,比如特定的时间点或者特定的数据偏移量(offset),该怎么办呢?

如果按照一般的思维,就得从存储数据的这个文件从头遍历,每个partition的数据都要遍历一次,直到找到目标时间的数据或者目标偏移量的数据为止,很明显,这样的时间复杂度为O(n),如果数据量很大,这个寻址的时间消耗将是巨大的,且会带来非常高的磁盘IO,效率低下。

那为了快速找到目标数据的位置,可以有哪些办法呢?

索引+分治

即原本一个partition对应一个文件的情况,变成了一个partition对应多个不同类型的文件,kafka将文件根据不同的功能分成了3大类:

kafka的数据文件类型

  • .index文件:offset索引文件,用来记录log文件中真实消息的相对偏移量和物理位置,为了减少索引文件的大小,这里用了一种比较聪明的做法,叫稀疏索引,即只记录相对offset的范围段(后文详细说明),可用于快速定位目标offset的消息;

其中参数:segment.index.bytes用来控制.index文件的大小,默认最大10MB;
参数:index.interval.bytes用来设置索引文件记录真实消息的消息间隔大小,默认为4KB,即上一条索引记录和下一条索引记录中间会间隔4KB的消息。
  • .timeindex文件:时间索引文件,类比.index文件,用来记录log文件中真实消息写入的时间情况,跟offset索引文件功能一样,只不过这个以时间作为索引,用来快速定位目标时间点的数据;

  • .log文件:用来记录producer写入的真实消息,即消息体本身;

这样一来,consumer如果想要去找到特定偏移量或者时间点的数据,不需要直接去消息文件中找了,而是去.index文件或者.timeidex文件中查找,而这两个作为索引文件,是需要加载到内核态内存中的,这样一来,通过这种索引+分治的手段,就可以快速找到目标数据。

但是,如果partition的数据都写到这3个文件中,随着数据的不断写入,这3个文件就会越来越大,会导致它们的读写IO也会越来越高,负荷越来越重。

那么怎么办?

继续分治...

04

第4次分治

单个文件过大,怎么办?

拆!

kafka对单个.index文件、.timeindex文件、.log文件的大小都有限定(通过不同参数配置),且这3个文件互为一组,当.log文件的大小达到阈值则会自动拆分形成一组新的文件。如图所示:

这样一来,将原本过大的文件进行了分治拆解,减小了单个文件的读写压力。

将数据文件拆分为多个,还会带来其他的一些好处。比如对需要的索引文件的加载,由于索引文件是需要预先加载到内存的,如果单个索引文件过大,会导致对内存的消耗过大;
此外,对过期数据的删除也会有帮助,可以以文件为单位进行数据删除,而不是删除文件中的某一个部分数据,效率更高。

这种将数据拆分成多个的小文件叫做segment,一个log文件代表一个segment。

segment的大小由参数 log.segment.bytes来控制,默认为1073741824 byte(1GB)

以上,就描述完了kafka在数据存储结构上采用的分治思想。

但是,如果想快速定位到某条特定的消息(根据offset或者消息时间),光有分治还不行啊,必须得有路由和索引才可以。

分治的作用只是将大份数据进行打散成多份小数据,而如果想要快速定位到其中的某条数据,还必须要有索引或者路由策略才可以。否则,查找效率一样不佳。

接下来,再看看kafka的查询逻辑...

05

kafka的查询逻辑

分治思想解决了数据集中的问题,将原本一份很大的数据进行了多个层次的拆分,让数据分散到多台服务器(多个broker)的多个不同目录,以及同个目录下的多个文件中。

那么如何解决快速定位到想要开始消费的数据呢?以offset定位为例。

对于消费者来说,想要消费哪个topic的数据,kafka先通过topic名字找到其topic,其时间复杂度为O(1),然后到每个partition中定位具体的offset,如何定位呢?

按顺序查找吗?

当然不是。

前面不是说有索引文件吗?每个消息的segment对应2个索引文件,一个.index文件和一个.timeindex文件,目的就是用来快速找到目标offset的消息。

来看一眼单个partition的文件结构:

单个partition的文件布局

看到的这个文件结构是按照时间进行倒叙排列的,每3个为一组,每一组的文件名都一样,命名方式为当前segment中最小的offset,其中保存最小offset的segment的log文件为:00000000000000000000.log,即offset从0开始的segment。

于是,可以看到图中所有segment的起始offset依次为(其中起始offset为0的的segment因为删除策略被删除了):

单个partition中每个segment的起始offset

那么如果想查找到某一个特定的offset属于哪个segment,用什么办法呢?

很明显:二分查找。

比如我要查找offset为2999999的消息,那么通过二分查找即定位到图中的2890012和3791564之间,而因为2999999<3791564,因此该offset的消息就定位到在2890012这个segment中,即在日志文件00000000000002890012.log中。

但是只定位到00000000000002890012.log这个文件还不够啊,因为这个日志文件本身还是很大的(默认1GB),要想精确定位到想要的消息位置,怎么办呢?

总不能按顺序遍历吧?

这个时候.index文件就派上用场了,上文提到.index文件是用来记录.log文件中消息的相对offset的,那如何进一步精确定位到想要消息的具体位置呢?

先来看看.index文件跟.log文件的映射关系(因为这个两个文件是通过二进制存储的,不能直接查看,下面以示意图表示):

通过.index文件查找.log文件中的消息

可以看到,.index文件中记录的是offset在对应.log文件中实际数据的位置,但是考虑到.index文件的大小,方便读到内存中,所以它不可能记录每一个offset的数据位置,怎么办呢?

于是就用稀疏的记录方式,即有一些记录,有一些不记录,而且是大部分不记录,只记录小部分。

具体的记录方式为:先记录一条offset的位置,然后跨过特定数据量的间隔(默认4KB),再记录下一条,且整个记录过程是顺序的,而这个跨过特定的数据量由参数log.index.interval.bytes来控制,这样一来.index文件就会只占用很少的空间,方便直接读取到内存中,进行offset与实际数据物理位置的映射,方便高效查找。

弄清楚了.index文件与.log文件的对应关系,再回到刚才那个疑问,如何快速查找到offset为2999999的消息呢?

很明显,还是二分查找,通过O(logN)的查找复杂度,在.index文件中定位到其物理位置在相对偏移量为109970(109970+2890012=2999982为小于2999999的最大偏移量)和110028(110028+2890012=3000040 为大于2999999最小的偏移量)这两个索引中间。

即定位到offset为2999999这条消息的具体物理位置在位置244421和位置344498之间。

可还是还没有完全找到啊?

接下来怎么找?

因为kafka中存储的消息是连续的,即按顺序存储的。

所以既然知道了目标消息在位置244421和位置344498之间,那么最后一步的查找,很明显还是还是二分找找,最终定位到位置为344486的消息即为目标消息。

如果想基于时间这个方式对消息进行查找,其查找的方式跟offset查找的思路是一样的,这里不再赘述。

结语

以上,可以看出kafka通过4次分治的理念对数据进行了拆分,然后又通过3次二分查找最终定位到目标消息。

即4次分治,3次二分,就是kakka对应的数据存储结构和查找算法。

(文章转自我的公众号[Anryg是码农],欢迎关注获取更多优质内容)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值