Kafka日志
概述
Kafka中的消息是存储在磁盘上的,主要了解下面几点:
- 为什么要使用磁盘作为存储介质
- 消息的存储格式
- 快速检索到指定的消息
- 消息不可能无限制存储,消息的清理规则
- kafka高吞吐量原因
文件目录布局
Kafka 中的消息是以主题为基本单位进行归类的,各个主题在逻辑上相互独立。每个主题又可以分为一个或多个分区,分区的数量可以在主题创建的时候指定,也可以在之后修改。每条消息在发送的时候会根据分区规则被追加到指定的分区中,分区中的每条消息都会被分配一个唯一的序列号,也就是通常所说的偏移量。从存储结构来看,一个分区对应一个日志(Log)。为了防止 Log 过大,Kafka又引入了日志分段(LogSegment)的概念,将Log切分为多个LogSegment,相当于一个巨型文件被平均分配为多个相对较小的文件,这样也便于消息的维护和清理。日志以文件夹形式进行存储,日志分段对应了一个日志文件和两个索引文件及其他文件。关系图如下所示:
某一个分区目录下文件内容如下图所示:
日志文件格式演变
Kafka的消息格式也经历了3个版本:v0版本、v1版本和v2版本。
v0
v0格式如下图所示:
左边的RECORD部分就是v0版本的消息格式,offset是逻辑值,而非实际物理偏移值,message size表示消息的大小,这两者在一起被称为日志头部。日志头部和RECORD一起用来描述一条消息。相关字段解释如下:
- crc32(4B):crc32校验值。校验范围为magic至value之间
- magic(1B):消息格式版本号,此版本的magic值为0
- attributes(1B):消息的属性。总共占1个字节,低3位表示压缩类型:0表示NONE、1表示GZIP、2表示SNAPPY、3表示LZ4(LZ4自Kafka 0.9.x引入),其余位保留。
- key length(4B):表示消息的key的长度。如果为-1,则表示没有设置key,即key=null。
- key:可选,如果没有key则无此字段。
- value length(4B):实际消息体的长度。如果为-1,则表示消息为空。
- value:消息体。可以为空,比如墓碑(tombstone)消息。
v0版本一个消息最小大小为14B。
v1
v1比v0版本就多了一个timestamp字段,表示消息的时间戳。v1版本的消息结构如下图所示。
v1版本的消息要比v0版本的大8个字节,即22B。
v2
前置知识
Varints(变长整形)是使用一个或多个字节来序列化整数的一种方法。数值越小,其占用的字节数就越少。Varints中的每个字节都有一个位于最高位的msb位(most significant bit),除最后一个字节外,其余msb位都设置为1,最后一个字节的msb位为0。这个msb位表示其后的字节是否和当前字节一起来表示同一个整数。除msb位外,剩余的7位用于存储数据本身,这种表示类型又称为Base 128。Varints中采用的是小端字节序,即最小的字节放在最前面。
以296=256+32+8为例,对应的字节为 1010 1000 0000 0010,去掉msb位后结果为000 1000 000 0010,小端字节序转换后为 000 0010 000 1000 -> 0000 0010 0000 1000 = 56+32+8。
ZigZag编码以一种锯齿形(zig-zags)的方式来回穿梭正负整数,将带符号整数映射为无符号整数,这样可以使绝对值较小的负数仍然享有较小的Varints编码值。
根据Varints的规则可以推导出0~63之间的数字占1个字节,64~8191之间的数字占2个字节,8192~1048575之间的数字占3个字节。
v2版本中消息集称为Record Batch,而不是先前的Message Set,其内部也包含了一条或多条消息,消息的格式如下图所示。
消息格式Record的关键字段,可以看到内部字段大量采用了Varints,这样Kafka可以根据具体的值来确定需要几个字节来保存。大大节省了空间。
日志索引
日志索引文件有偏移量索引文件和时间戳索引文件,Kafka 中的索引文件以稀疏索引(sparse index)的方式构造消息的索引,它并不保证每个消息在索引文件中都有对应的索引项。每当写入一定量(由 broker 端参数 log.index.interval.bytes
指定,默认值为4096,即4KB)的消息时,偏移量索引文件和时间戳索引文件分别增加一个偏移量索引项和时间戳索引项,增大或减小log.index.interval.bytes
的值,对应地可以增加或缩小索引项的密度。类似于跳表数据结构。
日志清理
策略:
- 日志删除:删除不符合条件的日志分段
- 日志压缩:针对每个消息的key进行整合,对于有相同key的不同value值,只保留最后一个版本。
使用:
broker端设置参数log.cleanup.policy
,默认为delete
,即删除策略,要采用日志压缩,设置为compact
。
日志删除
在Kafka的日志管理器中会有一个专门的日志删除任务来周期性地检测和删除不符合保留条件的日志分段文件,这个周期可以通过broker端参数log.retention.check.interval.ms
来配置,默认值为300000,即5分钟。当前日志分段的保留策略有3种:基于时间的保留策略、基于日志大小的保留策略和基于日志起始偏移量的保留策略。
基于时间
通过broker端参数log.retention.hours
、log.retention.minutes
和log.retention.ms
来配置,其中 log.retention.ms
的优先级最高,log.retention.minutes
次之,log.retention.hours
最低。默认情况下只配置了log.retention.hours
参数,其值为168,故默认情况下日志分段文件的保留时间为7天。
基于日志大小
broker端参数log.retention.bytes
来配置,默认值为-1,表示无穷大。注意log.retention.bytes
配置的是Log中所有日志文件的总大小,而不是单个日志分段(确切地说应该为.log日志文件)的大小。单个日志分段的大小由 broker 端参数 log.segment.bytes
来限制,默认值为1073741824,即1GB。
基于日志起始偏移量
如下图所示,日志分段1和日志分段2的起始偏移量小于logStartOffset,日志分段3的下一个日志偏移量比25大,所以日志分段3不会被删除,日志分段1和日志分段2将被删除。
日志压缩
对于有相同key的不同value值,只保留最后一个版本。如果应用只关心key对应的最新value值,则可以开启Kafka的日志清理功能,Kafka会定期将相同key的消息进行合并,只保留最新的value值。
日志存储
Kafka使用磁盘存储消息。Kafka 在设计时采用了文件追加的方式来写入消息,即只能在日志文件的尾部追加新的消息,并且也不允许修改已写入的消息,这种方式属于顺序读写。顺序读写于随机读写相比随机读写来说速度快的多,保证Kafka的高吞吐量,除此以外,还有一些其他的方式保证它的高性能。
页缓存
页缓存是操作系统实现的一种主要的磁盘缓存,以此用来减少对磁盘 I/O 的操作。具体来说,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问。
零拷贝
除了消息顺序追加、页缓存等技术,Kafka还使用零拷贝技术来进一步提升性能。零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手。零拷贝大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换。