Kafka学习笔记二(架构与数据存储)
观前提醒:本文使用的Kafka架构为0.11版本,存在大量与新版本不同的内容
Kafka的架构从大到小来讲的话,首先还得是从集群开始
Kafka的Cluster由N个Broker组成,这些Broker就是一个个的Kafka Server。
而作为一个集群,往往是存在主从主备之类的结构的,在Kafka中,就是借助了Zookeeper来协助选主,每个Broker在启动时都会尝试在Zookeeper上注册/controller
临时节点来竞选。第一个创建/controller
节点就被指定为Kafka集群的主,也就是"控制器"。竞争失败的其他节点就会依赖Watcher机制对这个节点进行监听,如果控制器宕机了,其他Broker就会开始争抢。
Controller作为Kafka集群的主,相较起普通的Broker只是多了一些额外的工作:比如说对Broker的管理、对Topic的管理、对Partition的管理。
从Broker往内就是Topic主题,每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic,类似于数据库的table或者ES的Index。
逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可(实际上生产或消费数据不必关心数据存于何处);Producer将消息推送到topic,由订阅该topic的Consumer从topic中拉取消息。
Topic主题内会被分成多个Partition分区,每个topic至少有一个partition。正常情况下多分区是可以提高topic的效率。
消息以追加的形式写入分区,每条消息都会有一个自增的编号为Offset偏移量,偏移量是唯一的标识一条消息,且可以决定读取数据的位置,消费者通过偏移量来决定下次读取的消息。
我们某一个业务也可以通过修改偏移量达到重新读取消息的目的,偏移量由用户控制。这其中不会有线程安全的问题,因为消息被消费之后,并不会被马上删除。这样还可以让多个业务重复使用Kafka 的消息,但是消息最终还是会被删除,默认生命周期为 1 周(7 * 24小时)。
数据会存放到topic的partation中,partation会存放到Broker上,但是有可能会因为Broker损坏导致数据的丢失。
因此Kafka为一个Partition生成多个副本,并且分散在不同的Broker上,如果一个Broker故障了,Consumer可以在其他Broker上找到Partition的副本,继续获取消息。
这些副本被分为了一个Leader和N个Follower,N+1为设置的备份数;其中Leader负责读写数据,Follower只负责备份数据。
文件存储
文件存储结构
Topic在物理层面以partition为分组,一个topic可以分成若干个partition,每个partition对应于一个文件夹。
为防止log文件过大导致数据检索效率低下,将每个partition分为多个Segment
Segment 文件由三部分组成,分别为“.index”文件、“.log”文件、“.timeindex”文件
partition全局的第一个segment从0开始,后续每个segment文件名为当前segment文件第一条消息的offset值
数值大小为64位,20位数字字符长度,没有数字用0填充
Segment 文件的生成规则
-
按照时间周期生成
-
#默认7天。如果达到7天,重新生成一个新的Segment log.roll.hours = 168
-
-
按照文件大小生成
-
#默认大小是1个G。如果一个Segment存储达到1G,就构建一个新的Segment log.segment.bytes = 1073741824
-
Segment 文件的存储优点
-
加快查询效率
-
通过将分区的数据根据 offset 划分到多个比较小的Segment文件,在检索数据时,可以根据offset 快速定位数据所在的Segment
-
加载单个Segment文件查询数据,可以提高查询效率
-
-
删除数据时减少IO
- 删除数据时,Kafka 以 Segment 为单位删除某个Segment的数据,避免一条一条删除,增加 IO 负载,性能较差
问题:为什么不直接将offset作为索引的第一列,而用一个相对偏移量作为第一列?
-
直接offset 作为索引的第一列,随着offset越来越大,索引变得非常大,查询性能会降低;
-
通过计算相对偏移量,可以在数据量大的情况下,节省索引的空间,提高检索的效率。
索引与数据
-
索引分为两类:一种是稠密索引,一种是稀疏索引。
-
稠密索引
- 即每一条记录,对应一个索引字段。稠密索引,访问速度非常块,但是维护成本大。
-
稀疏索引
-
并没有为每条记录建立了索引字段,而是把记录分为若干个块,为每个块建立一条索引字段。
-
稀疏索引字段,要求索引字段是按顺序排序的,否则无法有效索引。
-
-
-
kafka选择的就是稀疏索引
- 默认情况下,.log文件每增加4096字节,在.index 中增加一条索引。
-
#.log文件每增加4096字节,在.index中增加一条索引 log.index.interval.bytes=4096
-
数据查看方式
-
//查看.index文件的内容
kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.index --print-
data-log
-
//查看.timeindex文件内容
kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.timeindex –
print-data-log
-
//查看.log文件的内容
kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.log --print-data-
log
-
-
.index 位移索引
- 索引中的数据包含两列
- 第一列 offset :表示这条数据在这个 Segment 文件中的位置,是这个文件的第几条;
- 第二列 Position : 表示这条数据在 Segment 文件中的物理偏移量
- 索引中的数据包含两列
-
.timeindex 时间戳索引
- 和index偏移量索引文件一样,timeindex时间戳索引文件也是采用稀疏索引,默认每写入4k数据量的间隔记录一次时间索引项。
- 每个时间戳索引分为2部分,共占12个字节:
- timestamp :当前日志分段文件中建立索引的消息的时间戳;
- relativeoffset :时间戳对应消息的相对偏移量;
- 时间戳查找的时候首先拿要查找的时间戳和每个时间戳索引文件的最后一条记录进行比较,如果最后一条记录的时间戳小于等于0,就和文件修改时间比较,找到不小于查找时间戳的时间索引文件。找到对应的日志段时间戳索引文件以后,二分法查找不大于查找时间戳的offset,再根据此offset进行偏移量文件查找。
数据检索流程
- step1
- 先根据offset计算这条offset是在哪一份segment中
- step2
- 在index索引文件中,根据二分检索,得到索引中离这条数据最近的位置
- step3
- 根据得到的索引位置在log文件中读取,这样可以从要查找的数据范围从1G缩小到4K
文件清楚策略
-
Setp1:设置过期时间
-
Kafka 中 默认的日志保存时间为 7 天 ,定时任务会查看每个分段的最大时间戳(计算逻辑同上),若最大时间戳距当前时间超过7天,则需要删除。
-
删除日志分段时, 首先会先从跳跃表中移除待删除的日志分段,保证没有线程对这些日志分段进行读取操作。然后将日志分段所对应的所有文件添加上".delete"的后缀。最后由专门的定时任务来删除以".delete"为后缀的文件。
-
可以通过调整如下参数修改保存时间。配置文件在kafka的config/server.properties文件中:
-
log.retention.hours,最低优先级小时,默认 7 天。 log.retention.minutes ,分钟。 log.retention.ms ,最高优先级毫秒。 log.retention.check.interval.ms,负责设置检查周期,默认 5 分钟。
-
-
Setp2:选择清除策略
-
kafka有两种数据清除策略,delete删除和compact压缩,默认是删除。
-
delete删除:
-
log.cleanup.policy = delete
-
当不活跃的segment的时间戳是大于设置的时间的时候,当前segment就会被删除
-
-
compact压缩:
-
log.cleanup.policy = compact
-
对于相同key的不同value值,只保留最后一个版本。
-
应用场景:统计商城当前的销售总额
-
-
-
Step3:确认清除时机
-
Kafka的segment数据段清除不是及时的,他更像JVM垃圾回收那样,先打上deleted清除标记,在下一次清除的时候一起回收。
-
如果历史数据被删除,这时如果有消费者的偏移量就是从0开始的,确实是会造成偏移量丢失,并且默认强制把偏移量移动到最近的一个起始位置开始消费。
-
文件存储问题
-
如果index和timeindex触发时机都是4K,那么timeindex和index的偏移量是否会完美契合?
-
理论上两者应该是完美契合的
-
如果有差别应该在segment正常roll或者清除的时候添加一条timeindex
-