05-Kafka日志存储

Kafka日志存储

Kafka系列文章是基于:深入理解Kafka:核心设计与实践原理一书,结合自己的部分实践和总结。

一、文件目录

1.1 文件目录布局

  • 一个Topic可以分为一个或者若干分区,一个分区又有若干副本,这些副本最终对应的都是日志文件。但是一个分区副本可能会有非常多的消息,因此它对应的日志文件会被分段存储,与此同时为了保证高效索引,保存相关时间戳等还会有一些附加的文件信息。总的来说,一个副本对应一个日志文件夹,文件夹内按照分段会有若干子文件夹,每个子文件夹内部包括分段后的日志文件和2个索引文件,另外还有可能的事物索引文件,示图如下:

在这里插入图片描述

1.2 文件名规范

  • 如果一个topic有若干分区,那么每一个分区会对应一个文件夹存储,文件夹命名规范为:topicName-partionNo,比如topic名称为testTopic,有3个分区,那么三个分区对应的三个存储文件夹为:testTopic-0、testTopic-1和testTopic-2

  • 分区并不是底层日志文件存储的最小单位,一个分区有可能有非常多的消息,因此每个分区对应的是若干分段的日志文件,当第一个日志文件到达指定条件就会触发生成一个新的日志文件,所有的写入都是在最新的日志文件中进行,因此最新的日志文件可以称为活跃日志文件,并且日志文件是有命名规范的,其名称是00xxx.log,其中xxx是日志文件中第一条消息在分区中的offset,前面的0会补全包装文件名长度是20位,比如00000000000000001000.log说明该文件内的第一条消息的offset是1000。

  • 每个日志文件会对应2个索引文件(可能还有其他的文件),2个索引文件帮助快素根据偏移量或者时间戳查找消息,比如00000000000000001000.log会对应00000000000000001000.index和00000000000000001000.timeindex这2个日志文件,索引检索可以参考3.3。

PS:从相邻的2个日志文件名称我们可以推断出前一个日志文件中包含的消息数量,比如2个日志文件分别为00000000000000001000.log和
00000000000000001500.log,那么说明前一个日志文件消息的offset是1000到1499共500条消息,最新的日志文件则不能确定,因为有可能还在继续写入
  • 除了前面的日志文件和索引文件之外,还可能会有其他文件,比如事物文件或者Kafka内部的一些检查点文件等。

二、日志格式

  • 日志文件的格式和存储效率、扩展以及优化都有一定的关系,如果格式不够精炼那么性能和效率都会大打折扣(存储、IO以及字段对功能的支持等)。Kafka从0.8.x到2.0.0,消息格式经历了3个版本,分别是:v0、v1和v2。

2.1 v0版本

  • 在Kafka 0.10.0版本之前采用v0存储格式。

在这里插入图片描述

  • 相关字段
crc32: 校验值,校验magic-value之间的值
magic:消息格式版本号,v0版本该值为0
attributes:消息属性,1字节,包含消息压缩类型和部分保留的bit位
后面4位是key和value及其长度。

2.2 v1版本

  • 从Kafka的0.10.0版本到0.11.0版本,使用的是v1格式,相比v0多了一个时间戳字段timestamp

在这里插入图片描述

  • v1版本和v0版本字段含义几乎一致,只有attributes和timestamp字段的区别。另外v1版本消息长度比v0增加了8字节。
PS: 在v0版本,attributes字段中前3bit表示压缩类型,在v1版本中将第4个bit表示timestamp字段的时间戳类型,0代表CreateTime,1代表LogAppendTime。
    若类型是CreateTime,时间戳保存的是生产者创建消息时的时间戳,
    若类型是LogAppendTime,时间戳表示写入日志文件的时间。
    默认是CreateTime,由broker配置项log.message.timestamp.type决定

2.3 消息压缩

  • Kafka为了让压缩后能够取得比较好的效果,不是将消息单条压缩,而是将消息批量压缩。通常情况,生产者压缩消息后发生给broker,broker也是压缩存储,消费者获取到消息后处理时才会解压,保持了端到端的压缩。
2.3.1 压缩类型
  • 压缩类型由broker端配置项compression.type决定,默认值是producer表示保留生产者使用的压缩方式。
PS:配置uncompressed表示不压缩,另外合法配置项有:gzip、snappy和lz4。
2.3.2 压缩原理
  • 消息的压缩是批量的。Kafka将一批消息集(比如20条)压缩之后保存为一个内部消息的格式(inner message),然后再定义一个外层消息(wrapper message),外层消息的key是null,value就是内层消息。
内层消息的格式和前面描述的v0或者v1没有什么差异,就是[offset/messagesize/Record][offset/messagesize/Record][offset/messagesize/Record]这样的格式。
  • 外层消息会维护内层消息的一些信息便于解压后恢复内层消息的offset。每个内层消息的offset都是从0开始,但是外层消息会保存内存消息中最后一个消息的分区offset,由此就可以计算出前面的所有消息的offset。
  • 在v1版本中,外层维护的时间戳会比offset复杂一些,会考虑到内存的时间戳类型,细节就不展开,有兴趣可以阅读参考文章[1]的5.2.3小节。

2.4 变长字段

  • 从Kafka的0.11.0版本开始使用v2的日志格式,该版本相比v0和v1有了很大的差异,参考Protocol Buffer引入了变长整型(Varints)和ZigZag编码。
Varints是一种使用一个或者多个字节来序列化整数的方法,数字越小占用存储空间越小。Varints编码0-63之间占1个字节,64-8191占2字节,
8192-1048575占3字节;
ZigZag编码以锯齿形的方式将正数和负数都映射为正数,这样有些负数也能使用Varints的方式编码。比
如0 -> 0,-1 -> 1,1 -> 2,2 -> 3,2147483647 -> 4294967294,-2147483648 -> 4294967295 

PS: Varints和ZigZag这部分有兴趣可阅读参考文章[1]的5.2.4小节
  • 利用Varints和ZigZag可以节约一些空间,比如消息的key为null时,keylength是用-1表示的,存储-1需要4字节,但利用ZigZag编码-1等价于1只需要1字节空间,另外Kafka默认消息大小是1000012(1M左右),采用Varints编码方式大部分的情况是可以节约空间的。v2版本的日志格式就是这样做的,不过需要注意Varints编码并不是一直可以节约空间int32最长的时候会占据5字节(浪费1个字节),但是整体来说会节约存储。

2.5 v2版本

  • v2版本日志格式基于Varints和ZigZag做了不少修改,日志格式中的字段和存储形式和前面的v0和v1有比较大的区别,有兴趣可以阅读参考文章[1]的5.2.5小节,参考文章中有测试740B的消息在v0中存储占用空间320B,v1中占据400B,v2占据191B,节约了不少空间。
  • v2版本的日志格式出了节约空间之外,还提供了新的功能,比如:事物,幂等性等。
  • 查看日志方法
./kafka-run-class.sh  kafka.tools.DumpLogSegments --files  /kafka-logs/topiv-1/00000000000000256715.log
等价于下面的命令,不过2.0.0版本才有kafka-dump-log.sh,其内部也是调用kafka-run-class.sh
./kafka-dump-log.sh --files /kafka-logs/topiv-1/00000000000000256715.log

三、日志索引

3.1 索引方式

  • 每个日志分段文件对应有2个索引文件,一个offset索引文件维护了offset和物理地址间的映射关系,一个时间戳索引文件根据指定的时间戳查找消息。不过Kafka采用的是稀疏索引的方式,并不是每一哥offset或者时间戳都有一个索引项,而是写入一定数据量时才创建一个索引项,这样可以在空间和效率取得一定的平衡,索引密度由配置项log.index.interval.bytes值决定,默认是4096(4KB)。
Kafka通过二分在索引文件中找到最接近目标的索引,然后再去物理文件中进行定位。(因为offset是单调递增因此可以使用二分法,因为不是每一个offset都保存了
索引,因此不能保证可以获取精确索引,因此有时会返回最接近的,时间戳索引也是类似)

3.2 文件切分

  • 数据日志文件分段存储,到达一定的条件就会生成新的日志文件,日志的追加永远只在最新的文件,触发生成新的日志文件的条件有多种,达到其一即生成新的日志文件。
1.日志文件大小达到:log.segment.bytes, 默认1GB
2.当前日志文件中消息的最大时间戳和当前系统时间的差值大于:log.roll.ms或者log.roll.hours,如果配置了前者以前者为准,默认情况只配置了后者且默认值是168(即7天)
3.偏移量索引文件和时间戳索引文件中任意一个达到:log.index.size.max.bytes, 默认10485760,即10MB
4.追加的消息的对应偏移量和分段日志的偏移量差值大于:Integer.MAX_VALUE, 分段日志的偏移量是分段日志中第一条消息的offset,这个条件一般不会达到

PS:当前写入的日志文件成为活跃日志分段,因此只会有一个活跃的分段日志文件。注意日志文件的大小是log.index.size.max.bytes配置的,活跃日志分段对应的索引文件采用的是预分配的方式,在写入阶段它占用的空间是固定
为log.index.size.max.bytes大小,而那些非活跃的日志分段对应的索引文件大小则是其实际大小,活跃的分段对应的索引文件会在索引文件进行切分的时候才会切分为实际大小,写入的过程大小是不变的。这里注意每一个索引文件
的大小很可能不一样,因为有可能索引文件还没到达阈值就已经切分了,有可能其他的条件被触发了。

3.3 索引分类

3.3.1 偏移量索引
  • 偏移量索引中,每个索引项占8字节,包括relativeOffset和position两部分,分表代表消息偏移量和消息在日志分段文件中的物理位置。
  • 命名规则
relativeOffset占4字节,它表示消息相对baseOffset的偏移量,baseOffset是日志分段文件的第一条消息的offset,也是分段日志文件和索引文件的文件名称。(名称中的非零部分)
这里没有使用消息的绝对偏移量(offset)而是保持相对offset,可以节约空间,绝对偏移量大小是8字节。
  • 查找过程
假设现在有3个日志文件,文件1是offset 0-800,文件2是801-1600,文件3是1600-2400。现在需要查找offset为1000的消息(注意这是消息在分区中的偏移量),
那么查找过程如下: 
第一步:根据offset 1000在跳表中查找对应的日志文件,找到不大于1000的最后一个baseOffset是800,然后即定位到偏移
量为1000的消息在00000000000000000800.log,对应的索引在00000000000000000800.index。
第二步:找到00000000000000000800.index之后,计算相对偏移量是1000-800=200,由此找到不大于200的最大的相对偏移量
索引,也就是195对应的索引处。(因为索引文件是稀疏索引,不能保证对应的相对偏移量一定有索引项)
第三步:根据相对偏移量195的索引项知道它在日志文件中的物理位置为0x01(绝对偏移量是995),然后在日志文件中的0x01
开始顺序向后遍历,找到offset为1000的消息(相对偏移量是200))。

在这里插入图片描述

PS: Kafka的索引文件中每个索引项大小是8B,因此整个文件大小是8B的整数倍是最节约空间,当log.index.size.max.bytes设置不是8整数倍时,内部会向下转换为8的整数倍。
  • 查看索引文件
./kafka-run-class.sh kafka.tools.DumpLogSegments --files  /kafka-logs/topic-1/00000000000000256715.index

//部分输出如下(将日志导出到一个文件,发现起始的消息offset正是文件名中包含的数字,符合前面的命名规则):
Dumping /kafka/kafka-logs-intellif/ifaas-target-0/00000000000000256715.index
offset: 256716 position: 7882
offset: 256717 position: 15777
offset: 256718 position: 23767
offset: 256719 position: 31790
offset: 256720 position: 39779
offset: 256721 position: 47854
offset: 256722 position: 55895
offset: 256723 position: 63875
3.3.2 时间戳索引
  • 时间戳索引的索引项包含2部分,分别是时间戳和相对偏移量,占据字节8+4为12字节,和偏移量索引一样,会满足12的整数倍大小,如果设置不满足会向下转换。

  • 查找过程

假设现在有3个日志文件,文件1是时间戳 1-100,文件2时间戳101-200,文件3时间戳201-300。现在需要查找时间戳为150的消息,那么查找过程如下: 
步骤如下:
第一步:根据时间戳150,和每个文件中的最大时间戳largestTimeStamp比较,找到不小于150的largestTimeStamp对应
的日志分段。(这里无法使用跳表,但是为了获取每个分段日志文件中对大的时间戳不是扫描日志文件,而是扫描对应
的索引,直接找到索引的最后一个索引项)
第二步:找到满足条件的分段日志之后,在该日志文件对应的索引文件中使用二分法查找不大于150的最大索引项,找到
索引项之后就知道了相对偏移量X。   
第三步:根据偏移量,再到偏移量索引文件中二分查找不大于X的的最大索引项,找到之后就知道该消息在日志文件中的
位置Y。
第四步:从分段日志文件中的Y位置开始,扫描时间戳不小于150的第一条消息。

PS:时间戳索引的方法最后还是要转到偏移量的方法,多转了一步,流程是:
时间戳 -> 根据时间戳最大值找到目标日志分段 -> 分段对应索引中找到相对偏移量 -> 日志文件中找到消息
  • 查看索引文件
./kafka-run-class.sh kafka.tools.DumpLogSegments --files  /kafka-logs/topic-1/00000000000000256715.timeindex

//部分输出如下:
Dumping /kafka/kafka-logs-intellif/ifaas-target-0/00000000000000256715.timeindex
timestamp: 1563248553235 offset: 256716
timestamp: 1563248558397 offset: 256717
timestamp: 1563248558404 offset: 256718
timestamp: 1563248558412 offset: 256719
timestamp: 1563248558419 offset: 256720
timestamp: 1563248563532 offset: 256721
timestamp: 1563248563540 offset: 256722
timestamp: 1563248563548 offset: 256723

四、日志清理

  • Kafka消息会持久化到磁盘,但是对于部分消息日志有删除的需要,比如很久之前的消息。Kafka的每一个分区副本都对应一个Log,该Log包含若干分段的日志文件
Topic -> 若干分区,每一个分区对应一个路径 -> 每个分区有若干副本,但是这些副本会分布在不同的broker,因此一个broker只有该Topic下的某分区的
一个副本 -> 进入分区的路径内会有多个日志分段文件
  • 分类
Kafka的日志清理策略包括日志删除和日志压缩2种。
  • 配置
1.broker配置log.cleanup.policy这是清理策略,默认delete;
2.如果要采用日志压缩的方式,将log.cleanup.policy设置为compact,同时将log.cleaner.enable设置为true
3.log.cleanup.policy可以配置为"delete,compact"的形式支持2种清理策略
4.日志清理可以控制到主题级别,对应配置为cleanup.policy

4.1 日志删除

4.1.1 机制
  • Kafka的日志管理器会周期性检查日志文件,并删除不符合条件的日志分段文件,该周期通过配置log.retention.check.interval.ms决定,默认300000ms(5min)。
4.1.2 保留策略
4.1.2.1 基于时间
  • 时间阈值通过配置决定。
配置项作用默认值
log.retention.hours以小时为阈值单位默认168即7天。优先级最低
log.retention.minutes以分钟为阈值单位无默认值,优先级居中
log.retention.ms以分钟为阈值单位无默认值,优先级最高
  • 阈值比较原则。
PS: 决定某个日志分段是否删除并不是比较该文件最后的修改时间,因为该时间很容易被改变,而是通过该分段
日志文件对应的时间戳索引文件找到最后一条时间戳,如果这个最大的时间戳距离当前时间大于阈值,则需要删除
该日志分段文件。
  • 删除方式
PS:找到需要删除的日志文件并不会立刻删除该文件,首先会给对应的文件名添加.delete后缀,然后交给一个延迟
任务来删除对应的文件,延迟任务的执行延迟时间通过file.delete.delay.ms配置,默认60000(1min)
  • 其他
PS: 删除日志文件需要考虑其他的因素,不能简单删除,包括:
1.如果需要删除的分段数等于该分区副本的全部日志分段,那么需要先创建一个活跃分段日志文件保证能够接受消息,然
后执行删除操作。
2.需要维护Log对象中的跳表,保证不能再访问过期的日志文件。
4.1.2.2 基于大小
  • 大小阈值通过配置决定。
log.retenton.bytes(默认-1表示无穷大),但是注意该配置代表的是所有日志文件的总大小而不是单个分段的大小,单
个分段大小由log.segment.bytes指定,默认1073741824(1GB)
  • 删除过程
基于大小删除和基于时间删除这两种方案的删除过程是类似的,区别在于查找需要删除的分段过程有所不同。基于大小
的方案会根据日志文件的大小和阈值的大小计算出差值diff,这个差值就是需要删除的日志大小,
然后日志文件的第一个分段文件开始查找可删除的分段日志文件,之后再删除(删除过程和基于时间的策略一致)
4.1.2.3 基于offset
  • 分区副本的日志文件虽然分段存储,但是正常情况下,保存下来的若干个日志文件的offset是连续的,也就是后一个文件的第一条消息的offset和前一个文件的最后一条消息的offset是相邻的。即日志文件的起始偏移量(logStartOffset)等于第一个分段日志文件的baseOffset。 但是这并不是绝对的,logStartOffset可以通过命令修改。
  • 基于offset的策略删除日志文件的依据是:如果某个日志分段文件的下一个日志分段文件的起始偏移量(baseOffset)小于logStartOffset,那么就会删除这个日志分段文件。
假设3个日志分段文件的offset分别是0-10,11-20,21-30,logStartOffset是25,那么对于分段1而言他的下一个分段2的
baseOffset是11,小于25,因此分段1需要本删除,同理分段2也需要被删除,分段3则会保留。

4.2 日志压缩(Compaction)

  • Kafka的日志压缩是在日志删除的基础上提供的一种清理过时数据的方法,他会将key相同的详细进行处理,只保留最新的value。
  • 日志压缩和消息压缩一定要区分开,日志压缩是对key相同的消息进行去重保留最新的,消息将消息使用指定的压缩方式进行压缩后传输和保存。

五、磁盘存储

5.1 顺序/随机写

  • 速度
顺序写内存 > 顺序写磁盘 > 随机写内存(1) > 随机写磁盘

PS:操作系统对于顺序写磁盘做了深度优化,包括预读和后写,前者是将大块磁盘一次读入内存,后者是将很多小的写逻辑合并为一个大的写操作。
Kafka的日志文件不允许修改已经写入的操作,只能追加写入,是一种很典型的顺序写操作。这是Kafka速度快的一个原因。

5.2 页缓存

  • 页缓存是操作系统实现的一种磁盘缓存用于减少不必要的磁盘IO,简而言是将对磁盘的读写转换为对内存的读写,即使在需要回收内存时也不会造成很大的开销但是却可能大大提高写入的性能。
  • 读取和写入
1.在页缓存的机制下,读取磁盘的操作一旦在内存命中,那么就不需要进行IO,如果没有命中则进行IO加载磁盘数据
2.写入时,操作系统也会检查对应的页是否在内存,如果在则写入页缓存,操作系统会适时得将数据写入到磁盘,如果不在那么就添加页缓存,然后写数据到页缓存。

PS:操作系统的参数vm.dirty_background_ratio指定了当脏页数量达到系统内存的指定百分比之后会触发后台线程处理脏页,一般小于10,不建议0。
  • 进程VS操作系统
对于Kafka进程而言,可以在内部缓存需要处理的数据,与此同时这一份数据还可能在OS的页缓存被缓存了,因此缓存了2份,而且OS的页缓存一般难以禁止。另外:
1.进程内部缓存对象的方式通常效率很低,对象的大小往往是真实数据的数倍大小,
2.进程维护也会增加进程的复杂度,
3.进程重启进程内部的数据丢失但是OS缓存不会丢失,
4.进程内部缓存数据势必加大GC的负担。
但是将数据缓存的工作交给OS的页缓存来做前面的4个问题都不存在了,页缓存直接缓存的原始二进制数据,操作系统有一套完善的机制来维护页缓存,应用进程不
需要关注,因为这些好处,Kafka大量使用了页缓存,这是Kafka实现高吞吐的一大原因,数据被写入页缓存由OS负责刷盘。
  • Kafka大量使用了页缓存,这是Kafka实现高吞吐的一大原因,数据被写入页缓存由OS负责刷盘。不过Kafka也提供了配置来强制刷盘:log.flush.interval和message.log.flush.interval.ms。不过不建议同步刷盘,这样对吞吐和性能有较大影响,消息可靠性应该听过副本来保证

  • Swap:Linux的Swap机制会将部分磁盘当做内存使用,将非活跃进程数据调入swap区,显然这样会影响被调度的进程的性能,Linux提供参数vm.swappiness表示系统使用swap的"积极程度",值从0-100,0表示不会发生交换,100表示会积极的交换,这个参数对Kafka的调优也有一定的参考。

5.3 IO流程

  • 阅读参考文章[1]的5.5.2小节,该部分介绍了关于Linux的IO方面的流程。

5.4 零拷贝

  • 顺序、页缓存和零拷贝是Kafka性能优异的三个关键因素。零拷贝能够让数据直接从磁盘传输到网卡而不需要进过应用进程,由此大大提高了数据传输的性能。
  • 该部分可以阅读参考文章的[1]的5.5.3小结和参考文章[2]

六、总结

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
rsyslog-kafka是一种将rsyslog日志服务器与Apache Kafka消息队列集成的工具。rsyslog是一个功能强大的开源日志收集器,可用于在Linux系统上收集、处理和转发日志数据。而Kafka是一个高度可扩展的分布式消息系统,用于实时处理和存储大量数据。 通过rsyslog-kafka的集成,我们可以将rsyslog收集到的日志数据发送到Kafka消息队列中,从而实现日志的实时处理和存储。这种集成的好处是可以应对流量大、实时性要求高的日志场景,提高日志的传输速度和处理能力。 使用rsyslog-kafka的过程大致分为以下几步:首先,需要配置rsyslog服务器以收集特定文件或设备的日志数据;然后,配置rsyslog-kafka模块,指定Kafka的主题(topic)和其他相关参数;接下来,rsyslog-kafka将会将收集到的日志数据传输到Kafka消息队列中;最后,消费者可以从Kafka消息队列中实时接收、处理和存储这些日志数据。 rsyslog-kafka具有一些优点。首先,通过使用Kafka作为消息队列,可以轻松地扩展和处理大量的日志数据。其次,rsyslog-kafka提供了高度可配置性,可以根据需求选择日志的格式、过滤器和其他参数。此外,rsyslog-kafka还支持故障转移和高可用性机制,确保日志数据的可靠传输和存储。 总之,rsyslog-kafka是一种强大的工具,可以将rsyslog日志服务器与Kafka消息队列集成,实现高效的日志收集、传输、处理和存储。它为大规模的日志数据场景提供了解决方案,并提供了灵活的配置选项和高可靠性的机制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值