为了规避随机读写带来的时间消耗,kafka采用顺序写的方式存储数据。即使是这样,但是I/O操作仍然会造成磁盘的性能瓶颈,所以kafka还有一个性能策略。
零拷贝
一般应用程序有一个buffer空间在用户空间中,来自于网络或者磁盘,无论来自网络或者磁盘,都需要通过内核,也就是说内核中也要有buffer。
1)磁盘到内核 --> 2)内核到应用程序buffer 写数据时 --> 3)应用程序buffer写到内核buffer --> 4)内核buffer写到磁盘
这个过程多了两次拷贝,kafka本身因为不处理数据,所以没有必要把数据放入应用程序的buffer中。所以搞了个基于内核的数据存储和传输,使用sendfile机制,直接基于内核kernel处理。
- push和pull的模式
无论有多少producer,都往kafka进行push数据,kafka可以不关心producer的具体位置。consumer是从kafka pull数据,无论有多少消费数据,对kafka基本没有压力。 - 采用zookeeper来管理brokers和consumers
zookeeper主要存放元数据信息,这是一种积木式创新的体现。 - 在consumer端实现消息的一致性
kafka本身可以保存consumer已经消费过数据的offset,所以如果consumer出错的话,重新启动consumer,就可以从最近的数据开始。
保存消费端的消费位置 Offset
offset 即 每个消息针对每个consumer group 的偏移量,记录该consumer group 消费到了具体的位置。
在kafka 中,体用了一个__consumer_offsets-*
的一个topic ,把offset 信息写入到这个topic中。默认有50个分区。
查看groupid的offset存储在哪个分区中,计算公式为
- (“分组id”.hashCode())%__consumer_offsets的分区总数)
System.out.println(Math.abs(("KafkaConsumerDemo1".hashCode())%50));
- 查看当前consumer group 的offset 信息
bin/kafka-simple-consumer-shell.sh --topic __consumer_offsets --partition 4 --broker-list 192.168.45.135:9092,192.168.45.131:9092,192.168.45.134:9092 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter"
[groupid,topic,partition]::[OffsetMetadata[offset,..]....]
[KafkaConsumerDemo1,demo,0]::[OffsetMetadata[165,NO_METADATA],CommitTime 1547543212536,ExpirationTime 1547629612536]
消息的文件存储机制
一个topic 可以有多个partition 在物理磁盘上进行保存,进入到logs目录中,可以找到对应partition下的日志内容
cd /guaoran/kafka/logs/guaoran-0/
ls
00000000000000000000.index 00000000000000000000.log 00000000000000000000.timeindex leader-epoch-checkpoint
kafka 是通过分段的方式将log分为多个LogSegment,LogSegment是一个逻辑上的概念,一个LogSegment对应磁盘上的一个日志文件和一个索引文件,其中(.log)日志文件是用来记录消息的,(.index)索引文件时用来保存消息的索引。
LogSegment
当kafka producer 不断发送消息,必然会引起partition文件的五险扩张,这样对于消息文件的维护以及被消费的消息的清理都会带来非常大的挑战,所以kafka 以segment 为单位又把partition进行细分。每个partition相当于一个巨型文件被平均分配到多个大小相等的segment数据文件中(每个segment文件中的消息不一定相等),这种特性方便已经被消费的消息的清理,提高磁盘的利用率。
server.properties 中有以下几个配置
# 分段文件的大小
log.segment.bytes=107370
## 消息清理
# 日志消息默认存储7天
log.retention.hours=168
# 消息的大小,超过这个大小,会清理
log.retention.bytes=1073741824
为了看到明显的效果,将分段文件大小改小了,并进行发送多个消息到 guaoran 的topic中,再次查看
segment 文件由三部分组成,分别是.index , .log , .timeindex 后缀,
segment 文件命令规则:partition全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一天消息的offset值进行递增。
采用以下命令对 .index 文件进行查看
/guaoran/kafka/kafka_2.11-1.1.0/bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.index --print-data-log
结果如下:
offset: 53 position: 4124
offset: 106 position: 8264
...
offset: 1302 position: 103050
offset: 1354 position: 107210
采用以下命令对 .log文件进行查看
/guaoran/kafka/kafka_2.11-1.1.0/bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.log --print-data-log
结果如下:
offset: 1301 position: 102970 CreateTime: 1547557716588 payload: message_1301
offset: 1302 position: 103050 CreateTime: 1547557716601 payload: message_1302
offset: 1303 position: 103130 CreateTime: 1547557716612 payload: message_1303
offset: 1304 position: 103210 CreateTime: 1547557716624 payload: message_1304
...
offset: 1353 position: 107130 CreateTime: 1547557717167 payload: message_1353
offset: 1354 position: 107210 CreateTime: 1547557717179 payload: message_1354
offset: 1355 position: 107290 CreateTime: 1547557717183 payload: message_1355
第一个log文件的最后一个offset为1355,所以下一个segment的文件命名为00000000000000001356.log
segment 中 index 和 log 的对应关系
如上面所看查看的index 和log 的文件内容,进行分析
为了提高查找消息的性能,为每一个日志文件添加2个索引,索引文件:offsetIndex 和 TimeIndex ,分别对应 .index 和 .timeindex
.index 文件中存储了索引 以及物理偏移量。.log 文件中存储了消息的内容。索引文件的元数据执行对应数据文件中message 的物理偏移地址。以【1302,103050】为例, log文件中,对应的是滴1302条记录,物理偏移量(position)为103050,position 是ByteBuffer 的指针位置。
在 partition 中如何通过 offset 查找 message
- 根据offset 的值,查找 segment 段中的 index 索引文件。由于索引文件命名是以上一个文件的最后一个offset进行命令的,所以,使用二分查找算法能够根据offset快速定位到指定的索引文件。
- 找到索引文件后,根据offset进行定位,找到索引文件中的复合范围的索引。(kafka 采用稀疏索引的方式来提高查找性能)
- 得到position以后,在到对应的log文件中,从position处开始查找offset对应的消息,将每条消息的offset与目标offset进行比较,知道找到消息
比如找 offset=1303的消息,那么会先找到000.index 文件,找到【1302,103050】 这个索引,在到log文件中,根据 103050 这个position 开始查找offset = 1303的消息,当确定对应的消息后进行返回。
日志清除策略以及压缩策略
日志清除策略
日志是分段存储的,一方面能够减少单个文件内容的大小,另一方面,方便kafka 进行日志清理。日志的清理策略有两个:
- 根据消息的保留时间,当消息在kafka中保存的时间超过了指定的时间,就会触发清理过程
log.retention.hours=168
默认7天 - 根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阈值,则开始删除最久的消息。kafka会启动一个后台线程,定期检查是否存在可以删除的消息。
log.retention.bytes=1073741824
默认1G
通过上面这两个参数来设置,当其中任意一个达到要求,都会执行删除。
日志压缩策略
kafka 还提供了日志压缩功能,通过这个功能可以有效的减少日志文件的大小,缓解磁盘紧张的情况,在很多实际场景中,消息的key和value的值之间的对应关系是不断变化的,就像数据库中的数据会不断被修改一样消费者只关心key对应的最新value值。因此,我们可以开启kafka的日志压缩功能,服务端会在后台启动Cleaner 线程池,定期将相同的key进行合并,只保留最新的value值。
默认情况下启动日志清理程序,要在特定主题上启用日志清理,您可以添加特定于日志的属性 log.cleanup.policy=compact
,日志清理程序可以配置为保留最小量的日志的未压缩“头”。通过设置压缩时间延迟来启用此功能。 log.cleaner.min.compaction.lag.ms
.
日志压缩的原理