阅读《深入理解Kafka核心设计与实践原理》第五章 日志存储
日志存储
1. 文件目录布局
Kafka中的消息是以主题为基本单位,各个主题在逻辑上相互独立。
一个分区对应一个日志(Log),为了防止Log过大,Kafka有引入日志分段(LogSegment),将Log切分成多个LogSegment,有利于消息的维护和清理。
向Log追加消息是顺序写入,只有最后一个LogSegment才能执行写入操作,在此之前的所有LogSegment都不能写入数据。
为了便于消息的检索,每个LogSegment的日志文件都有对应的两个索引文件,偏移量索引文件和时间戳索引文件
.index
.log
.timeindex
消费者提交的消费位移是保存在kafka的内部主题__consumer_offsets 中的,初始这个主题并不存在,当第一次有消费者消费消息会自动创建这个主题。
2. 日志索引
每个日志分段文件对应了两个索引文件,用来提高查找消息的效率。
偏移量索引文件用来简历消息偏移量offset到物理地址之间的映射关系,快速定位消息所在的物理文件位置。
时间戳索引文件则根据指定的时间戳来查找对应的偏移量信息。
索引是个稀疏索引,会将这个稀疏索引文件映射到内存中,可以使用二分查找发来快速定位偏移量的位置,稀疏索引是时间和空间等多方面之间的一个折中。
日志分段文件到**达一定条件也需要进行切分**,那么对应的索引文件也需要切分,切分条件:
- 以文件大小为维度 log.segment.bytes 默认1G
- 以时间大小为维度 log.roll.ms
- 索引文件超过一定的大小
那配置了这个参数之后如果有很多很多分区,然后因为这个参数是全局的,因此同一时刻需要做很多文件的切分,这磁盘IO就顶不住了啊,因此需要设置个rollJitterMs,来岔开它们。rollJitterMs,这其实是个扰动值,应的参数是log.roll.jitter.ms,避免日志切分时造成惊群
3. 日志清理
kafka提供了两种日志清理策略:
- 日志删除 (Log Retention)按照一定的保留策略直接删除不符合条件的日志分段。
- 日志压缩(Log Compaction)针对每个消息的key进行整合,相同的key不同value,只保留最后一个版本。
日志删除:
- 基于时间
- 基于大小
- 基于日志起始偏移量
日志压缩Compact
对于相同的key,不同的value值,只保留最后一个版本。
Log Compaction会保留key相应的最新value值,那么当需要删除一个key时怎么办?
kafka提供了一个墓碑消息的概念,如果一条消息的key不为null,但是其value为null,那么此消息就是墓碑消息。
4. 磁盘存储
kafka使用磁盘来存储消息,事实上磁盘可以比我们预想的快,也可能比预想的慢,取决于如何使用它,操作系统对于线性读写做了深层次优化,预读(大文件)/后写(小文件)
kafka采用文件追加的方式来写入消息,在日志文件的尾部追加新的消息,并且不允许修改已写入的消息,这种方式属于顺序写入。
4.1 页缓存
kafka大量使用了页缓存,高吞吐量的原因之一。
页缓存是操作系统实现的一种主要的磁盘缓存,以此来减少对磁盘I/O的操作,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问。
java的对象内存开销很大,通常是真实数据大小的几倍或更多,空间使用率低下,java垃圾回收会随着堆内数据增多而变慢。
kafka使用文件系统并依赖于页缓存的做法要优于维护一个进程内缓存和其他结构。
并且kafka服务重启,页缓存还是会保持有效,而进程则需要重建。
尽量避免swap分区内存的交换。
4.2 零拷贝
数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手。减少了内核态和用户态之间的上下文切换。对于linux系统而言,零拷贝依赖底层的sendfile()方法实现。
常见的文件发送:
使用了零拷贝技术:
零拷贝技术通过DMA(Direct Memory Access)技术将文件内容复制到内核模式下的Read Buffer 中。不过没有数据被复制到 Socket Buffer,相反只有包含数据的位置和长度的信息的文件描述符被加到Socket Buffer中。DMA引擎直接将数据从内核模式中传递到网卡设备(协议引擎)。这里数据只经历了2次复制就从磁盘中传送出去了,**并且上下文切换也变成了2次。零拷贝是针对内核模式而言的,数据在内核模式下实现了零拷贝。**