1. 日志存储:
1.1 消息压缩:
常见的压缩算法是数据量越大压缩效果越好,一条消息通常不会太大,这就导致压缩效果并不是太好。
而kafka实现的压缩方式是将多条消息一起压缩,这样可以保证较好的压缩效果。
在一般情况下,生产者发送的压缩数据在Broker中也是保持压缩状态进行存储的,消费者从服务端获取的也是压缩的消息,消费者在处理消息之前才会解压消息,这样保持了端到端的压缩。
Kafka日志中使用哪种压缩方式是通过参数 compression.type
来配置的,默认值为 producer
,表示保留生产者使用的压缩方式。这个参数还可以配置为 gzip
, snappy
, lz4
,分别对应 GZIP、SNAPPY、LZ4 这3种压缩算法。如果配置为 uncompressed
,则表示不压缩。
2. 日志清理:
Kafka将消息存储在磁盘中,消息不可能无限制存储,为此Kafka制定了如下的清理规则:
Kafka中每一个分区副本都对应一个log,而log又可以分为多个日志分段(LogSegment),这样也便于日志的清理工作。Kafka提供了两种日志清理策略:
- 日志删除:
按照一定的保留策略直接删除不符合条件的日志分段; - 日志压缩:
针对每个消息的key进行整合,对于有相同key的不同value值,只保留最后一个版本。
可以通过 broker端的参数 log.cleanup.policy
来设置日志清理策略:
- 此参数默认为
delete
,即采用日志删除策略; - 如果需要采用日志压缩策略,则修改参数为
compact
,并置log.cleaner.enable
为true(默认为true); - 如果置为
delete, compact
,则 可以同时支持日志删除和日志压缩两种策略。
2.1 日志删除:
broker端参数: log.retention.check.interval.ms
用来配置检测日志删除操作的周期,默认值为300000(5分钟)。
2.1.1 日志分段的三种保留策略:
- 基于时间的保留策略:
日志删除任务 会检查当前日志文件中是否有保留时间超过设定的阈值(retenionMs) 来寻找可删除的日志分段文件集合。
阈值 retenionMs 可以通过 broker端参数设置:
log.retention.hours
log.retention.minutes
log.retention.ms
/*
其中,log.retention.ms 的优先级最高,log.retention.minutes 的优先级次之,log.retention.hours的优先级最低。
默认情况下,只配置了 `log.retention.hours` 参数,其值为 168, 即默认情况下 日志分段文件的保留时间为 7天。
*/
- 基于日志大小的保留策略:
日志删除任务 会检查当前日志的大小是否超过设定的阈值(retentionSize)来寻找可删除的日志分段文件集合。
阈值 retentionSize 可以通过broker端参数设置:
log.retention.bytes
log.segment.bytes
/*
`log.retention.bytes` 默认值为-1,表示无穷大(表示的所有日志的总大小);
`log.segment.bytes` 的默认值为 1073741824,即 1G。
*/
- 基于日志起始偏移量的保留策略:
基于日志其实偏移量的保留策略的判断是某日志分段的下一个日志分段的起始偏移量 baseOffset 是否小于等于 logStartOffset,若是,则可以删除此日志分段。
2.2 日志压缩:
Kafka中的 日志压缩(Log Compaction)是指在默认的日志删除(Log Retention)规则之外提供的一种清理过时数据的方式。
如图所示,Log Compaction对于有相同key的不同value值,只保留最后一个版本。 如果应用程序值关心key对应的最新value值,则可以开启Kafka的日志清理的功能, Kafka会定期将相同key的消息进行合并,只保留最新的value值。
3. 磁盘存储:
3.1 追加写:
Kafka依赖于文件系统(磁盘)来存储和缓存消息。
计算机系统中各层存储介质的存取速度如图所示,磁盘是一个存储速度比较低的介质,例如RabbitMQ中,就使用内存作为默认的存储介质而磁盘作为备选介质,以此实现高吞吐和低延迟的特性。
然而,事实上如果“合理的”使用磁盘,其存取速度要比我们预想的快的多:
有关测试结果表明,一个由6块 7200r/min 的RAID-5阵列组成的磁盘簇的线性(顺序)写入速度可以达到 600MB/s,而随机写入速度只有 100KB/s,磁盘的顺序写入速度可以达到随机写入速度的 6000倍。
这是因为,在顺序读写的情况下,操作系统可以针对线性读写做深层次的优化,比如预读(read-ahead,提前将一个比较大的磁盘块读入内存) 和 后写(write-behind,将很多小的逻辑写操作合并起来组成一个大的物理写操作)技术。
顺序写盘的速度不仅比随机写盘的速度快,而且也比随机写内存的速度快。
Kafka在设计时,采用了“文件追加”的方式来写入消息,即: ① 只能在日志文件的尾部追加新的消息,② 并且也不允许修改已写入的消息。
这种方式属于典型的顺序写磁盘的操作,所以就算Kafka使用磁盘作为存储介质,它所能承载的吞吐量也不容小觑。
“追加写” 是Kafka在性能上具备足够竞争力的第一点原因。
3.2 页缓存:
页缓存是Kafka提升读写性能的第二点方法。
页缓存是操作系统实现的一种主要的磁盘缓存,以此用来减少对磁盘I/O的操作。
具体来说,就是把磁盘中的数据缓存在内存中,把对磁盘的访问变成对内存的访问,当一个进程准备读取磁盘上的文件内容时,操作系统会先查看待读取的数据所在的页(page)是否在页缓存(pagecache)中,如果存在(命中)则直接返回数据,从而避免了对物理磁盘的I/O操作。
同样,如果一个进程需要将数据写入磁盘,那么操作系统会先检查数据对应的页是否在页缓存中,如果不存在则会先在页缓存中添加相应的页,最后将数据写入对应的页。被修改过后的页也就变成了 “脏页”,操作系统会在合适的时间把脏页中的数据写入磁盘,以保持数据的一致性。
Kafka中大量使用了页缓存,这是Kafka实现高吞吐的重要因素之一。
默认情况下是由操作系统来负责具体的刷盘任务,但在Kafka中同样提供了同步刷盘即间接性强制刷盘(fsync)的功能,可以通过参数进行配置:
log.flush.interval.messages
log.flush.interval.ms
强制同步刷盘的好处是提高消息的可靠性,防止由于机器掉电等异常造成处于页缓存而没有及时写入磁盘的消息丢失;缺点是损耗性能。
建议使用默认的操作系统的默认刷盘机制,不要手动修改。
3.3 零拷贝:
除了消息顺序追加、页缓存等技术,Kafka使用的第三个优化磁盘读写性能的技术是:零拷贝(Zero-Copy)。
所谓的“零拷贝”就是将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序的方法。
零拷贝大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换。
举例来说:假如需要将本机磁盘中的静态资源(如图片、文件)等发送给用户,在不使用零拷贝的默认处理方式下,需要先调用 read() 系统调用从内核空间将文件拷贝到应用进程空间,在由应用进程调用 socket() 函数将文件发送到网卡,从用户空间拷贝到内核空间:
磁盘 ->(内核空间 -> 应用进程空间 -> 内核空间)-> 网卡
从上面的图中可以看到,数据平白无故的从内核模式到用户模式“走了一圈”,浪费了两次复制过程。
如果采用零拷贝技术,那么应用程序可以直接请求内核把磁盘中的数据传输给Socket。
零拷贝技术通过 “DMA”(Direct Memory Access) 技术将文件内容复制到内核模式下的Read Buffer中,不过没有数据被复制到Socket Buffer,相反只有包含数据的位置和长度的信息的文件描述符被加到Socket Buffer中。DMA引擎直接将数据从内核模式中传递给网卡设备。
3.4 总结:
追加写、页缓存、零拷贝,是保证Kafka作为消息中间件时具备高效的读写性能的关键原因。