1、文件目录布局
不考虑多副本的情况,一个分区对应一个日志(log)。为了防止log过大,Kafka还引入了日志分段(LogSegment)的概念,将log切分为多个LogSegment。
Log对应了一个命名形式为<topic>-<partition>的文件夹。
向Log中追加消息时时顺序写入的,只有最后一个LogSegment才能执行写入操作。
为了方便消息的检索,每个LogSegment中的日志文件,以“.log”为文件后缀的都有两个索引文件:偏移量索引文件(“.index”)和时间戳索引文件(".timeindex")。每个LogSegment都有一个基准偏移量baseOffset,用来表示当前LogSegment中第一条消息的offset。
2、日志索引
偏移量索引文件用来建立消息索引量(offset)和物理地址之间的映射关系,方便快速定位消息所在的物理文件位置。时间戳索引文件根据指定的时间戳来查找对应的偏移量信息。
Kafka的索引文件以稀疏索引的方式构建消息的索引,它并不保证每个消息在索引文件中都有对应的索引项。每当写入一定量的消息时,偏移量索引文件和时间戳索引文件分别增加一个偏移量索引项和时间戳索引项,增大或减少log.index.interval.bytes的值,对应地可以缩小或增加索引项的密度。
稀疏索引的方式是在磁盘空间、内存空间、查找时间等多方面之间的一个折中,使用二分法来快速定位偏移量的位置。
2.1、偏移量索引
每个索引量占据8个字节。relativeOffset是相对偏移量,表示消息相对于baseOffset的偏移量,占据4个字节。position是物理地址,也就是消息在日志分段文件中对应的物理位置。
没有使用绝对偏移量而使用相对偏移量,是为了节省索引文件所占的空间。绝对偏移量占8和字节,相对偏移量占用4个字节。
例如:如何查找偏移量是268的消息。首先是定位到baseOffset为251的日志分段,然后计算出relativeOffset=268-251=17,之后再在对应的索引文件中找到不大于17的索引项,最后找到根据索引项中的position定位到具体的日志分段文件位置开始查找目标消息。那么如何查找baseOffset为251的日志分段呢?这里不是顺序查找,而是使用了跳跃表的结构。
2.2、时间戳索引
timestamp:时间戳索引,当前日志分段最大的时间戳。
relativeOffset:时间戳所对应的消息的相对偏移量。
每当消息写入一定量的时候,就会在偏移量索引和时间戳索引文件中分别增加一个偏移量索引项和时间戳索引。两个文件增加的索引项的操作是同时进行的。
如果要查找timeStamp=15682185885开始的消息,首先是找到不小于指定时间戳的日志分段。这里无法再使用跳跃表定位到对应的日志分段了。
步骤1:将target和每个日志分段中的最大时间戳largestTimeStamp逐一对比,直到找到不小于target的多对应的日志分段。
步骤2:找到对应的日志分段后,在时间戳索引文件中使用二分查找算法找到对应的偏移量。
步骤3:在偏移量索引文件中使用二分查找到不大于此的索引量。
步骤4:从步骤1中找到日志分段文件中的838的物理位置开始查找不小于target的消息。
3、日志清理
Kafka将消息存储在磁盘中,为了控制磁盘所占空间的不断增加就需要对消息做一定的清理。两种策略:
日志删除:按照一定的保留策略直接删除不符合条件的日志分段。三种,基于时间、基于日志大小、基于日志起始偏移量。
日志压缩:针对每个消息的key进行整合,对于有相同key的不同value值,只保留最后一个版本。
通过设置broker端的参数log.cleanup.policy来设置日志清理策略,默认值delete,日志压缩是compact。可以同时启用两种方式,“delete,compact”。
4、磁盘存储
Kafka依赖磁盘来做存储和缓存消息。在我们的印象中,磁盘处于一个尴尬的位置,不是很快。在传统的消息中间件RabbitMQ中,就使用内存作为默认的存储介质。然而,事实上磁盘可以比我们预想的要快,也可能比我们预想的要慢,这取决于我们怎么使用它。
一项研究表明,磁盘的顺序写入速度和随机写入速度相差6000倍。操作系统可以针对线性读写做深层次的优化,比如预读和后写。顺序写盘的速度不仅比随机写盘的速度快,而且比随机写内存的速度也快。
Kafka在设计时采用了文件追加的方式来写入消息,即只能在日志文件的尾部追加新的消息,并且也不允许修改以写入的消息,这是属于典型的顺序写盘的操作。
4.1、页缓存
页缓存是操作系统实现的一种主要的磁盘缓存,以此来减少磁盘I/O的操作。具体来说,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问。
当一个进程准备读写磁盘上的文件内容时,操作系统会先查看读取的数据所在的页(page)是否在页缓存(pagecache)中,如果命中则直接返回数据,从而避免了对物理磁盘的I/O操作。如果没有命中,则操作系统会向磁盘发起读取请求并将读取的数据页存入页缓存中,之后再将数据返回给进程。同样,如果一个进程需要将数据写入磁盘,那么操作系统也会将检测数据对应的页是否在页缓存中,如果不存在,则会先在页缓存中添加相应的页,最后将数据写入页对应的页。被修改后的页也就变成了脏页,操作系统会在合适的时间将脏页中的数据写入磁盘,以保持数据的一致性。
Kafka中大量使用了页缓存,这是Kafka实现高吞吐的重要因素之一。
4.2、零拷贝
除了消息顺序追加、页缓存等技术,Kafka还使用零拷贝(Zero-Copy)技术来进一步提升性能。
所谓的零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手。零拷贝大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换。