进入Kafka的数据,都被作为日志存储到磁盘上了,并能够在磁盘上保留一段时间,最后被清理。其中,每一个分区的日志,还具备有日志轮转功能,这个设计类似于Log4j等日志系统。此外,kafka号称性能极好,它有disk io操作,查询性能居然也很好,它是怎么做到的呢?
想了解大数据的学习路线,想学习大数据知识以及需要免费的学习资料可以加群:784789432.欢迎你的加入。每天下午三点开直播分享基础知识,晚上20:00都会开直播给大家分享大数据项目实战。
- 1、LogManager 设计之类图
- 2、Partition Log的结构
- 3、Partition Log操作
- 4、LogManager
- 5、Log Compaction
- 6、Configuration
1、LogManager设计之类图
在Kafka的代码里,进行日志管理的类是LogManager,它提供了对Log管理方面的操作,其中对Log的基本操作有:
createLog(TopicAndPartition partition) // 为分区创建Log deleteLog(TopicAndPartition partition) // 删除某个分区
从这两个方法可以看出是Kafka对于Log的管理是基于Partition的。
也就是说,在一个Kafka broker内,任何一个partition都会有一个逻辑的Log与之对应。
所以呢,上面类图中的Log类,其实就是Partition Log,在后续内容中,就将其称为Partition Log了。
2、Partition Log的结构
2.1 逻辑结构
Partition Log的逻辑结构其实从上面的类图就可以看的一清二楚了:
1) 一个Partition Log由多个LogSegment组成
2) 一个LogSegment代表了整个Partition Log中的某一段,它是由一个FileMessageSet和一个OffsetIndex组成
3) FileMessageSet:顾名思义,它代表了一个基于文件的消息集合。内部采用channel方式访问物理文件。
4) OffsetIndex:可以将其看做一个index table, 每一项代表了一个offset 与 物理文件(.log 文件)position的映射。此外它会与实际的物理文件(.index) 之间是建立的 mmap。
5) 每一个Partition Log有一个nextOffsetMetadata 记录,用于记录下一个要添加的messageset的offset
6)每一个Log都有一个recoverypoint,用于记录真正flush到磁盘的消息的最后一个offset
2.2 物理结构
下面是一个kafka log dir下的文件分布:
其中__consumer_offsets是一个topic,每一个以__consumer_offset-* 的目录就代表一个Partition Log的目录:
1)一个Partition Log 对应于一个kafka broker 的log dir下面的一个分区目录。
2)一个LogSegment对应了partition log directoy下的两个文件:一个.log文件,一个.index文件。
在Partition Log目录下,.log文件与.index文件是成对出现的,组成一个log segment。其中文件名是一个Long类型的数字,代表了该log segment的base offset (也就是该segment内第一条消息的offset)。
3、Partition Log操作
3.1 append()添加日志
为什么采用追加模式(append)?
因为日志在物理上是以File方式存储,所以设计者日志文件设计为append模式,这也是为了性能的考虑。如果目前的设计方案上,改为可修改模式,估计整个系统估计就乱套了。
此外在修改模式下操作文件,会导致磁盘磁头频繁的变位,去找到修改点,效率可想而知了。
而append模式,其实就是顺序写操作。
同一时间,只有一个segment是active的
Log中有多个segment,由于需要在整个Partition Log范围内采用append模式,所以就要求同一时间,只能有一个segment是活动的,也就是说只能往一个segment里添加消息,一旦active segment达到配置上限,就需要进行roll操作,产生一个新的segment 作为active。
append(messages: ByteBufferMessageSet),把一个基于ByteBuffer 的message set追加到Partition Log的处理流程:
1) 基于nextOffsetMetadata为 message set 分配一个offset,作为message set中第一条message的offset
2) 基于active segment 的当前size或者create time来判断是否需要进行日志文件轮转,如需轮转,则发起一个异步操作进行 segment flush操作。并创建一个新的segment作为active segment。
3)调用activeSegment.append(firstOffset, messages)来追加日志。这个过程实际上就是把messages 的offset 与 相对position写到index文件,将messagset内容写到了.log文件中。
4)基于activeSegment的baseOffset, size 分配nextOffsetMetadata
5)如果未flush的messages数量大于配置的flush间隔(flush.messages)的话,就调用相关segment的flush()操作,将确保指定范围的数据完全真正的写到文件。这个动作是同步的。
此外:
上面的第2)步中,提到会基于size 或者time的方式进行日志轮转功能,具体的情况是:
- 根据size判断:segment.size > config.segmentSize – messageSetSize 时,也就是说如果添加指定的messageset后,会导致 segment的size超出config.segmentSize是会进行轮转。
- 根据time判断:currentTime – segment.createTime > config.segmentMs – segment.rollJitterMs 时,也就是segment file从创建到现在,在磁盘上存在的时间超过config.segmentMs存活时间。
3.2 read()读取数据
经过append的messageset都被写到了磁盘里了。写的数据我们读出来才有意义。Partition Log中定义的read提供了从某个offset开始读取一定量的数据的功能。
read(startOffset: Long, maxLength: Int, maxOffset: Option[Long] = None) 读取数据的执行步骤:
1)验证读取数据操作的有效性:根据startOffset 和 nextOffsetMetadata 来判断要读取的数据是否存在,若存在则继续。
2)基于segments.floorEntry(startOffset) 采用二分法快速定位从哪个segment读取数据。
根据方法的其他参数 maxLength, maxOffset来决定要读取多少个segment。
3)从挑选出的这些segments中读取数据。具体来说调用的是segment.read(startOffset, maxOffset, maxSize, maxPosition) 。但此时并没有真正的读取数据,只是生成了一个基于FileChannel读取指定范围的数据的iterator而已。当真正需要的时候,才会调用FileMessageSet#toMessageFormat() 来引发该iterator的调用,从而真正的从磁盘读取数据。
此外,segment.read(startOffset) 又是基于 index快速定位出要读取的message的startOffset对应在.log文件的position, 这样就知道从.log中哪个position开始读取数据了,然后结合其他参数生成一个消息窗口,最终以一个Iteartor<MessageAndOffset>的方式返回。
3.3 deleteSegment(segment)删除日志段
日志不能无限期的增长呀,那样磁盘肯定是不够用的,所以基于某些策略来对无效、过期日志清理。因为append数据时,会在响应的.index文件中记录某个offset相应的在.log文件中的position,那么如果在删除日志时,采取删除文件内容的方式,就会引发文件的修改,从而导致.index文件中的记录失效。所以Kafka设计者在设计文件清理时,采取了整个segment删除的策略。
deleteSegment(segment: LogSegment) 删除的处理步骤:
1)将LogSegment对应的 .index, .log文件进行重命名,其实就是在文件后面加上.deleted后缀(.index.deleted, .log.deleted)
2)使用scheduler异步调度segment.delete()
其实就是删除文件了。
3.4 truncatTo(targetOffset) 删除offset大于 targetOffset的消息
在满足了上面deleteSegment的情况下,要做到从前往后删除就相对容易了。
truncateTo(targetOffset: Long) 的步骤:
1) 从前往后计算好要删除哪些完整的segment,对于这些直接调用deleteSegment(segment)来删除
2) 对于部分删除的那个segment,仍然采取截断式删除。Index文件中的记录会进行重新计算。
3.5 recoverLog() 恢复日志
recoverLog() kafka重启时进行日志恢复
它的实质就是认为真正写到磁盘的数据才是有效的,未写到磁盘的就是无效的数据,所以要将Log截断到log的recoverypoint,也就是调用 log.truncatTo(log.recoverypoint)
4、LogManager
4.1 logManager提供的基本管理功能
LogManager就是在Partition Log的基础上,提供了管理功能,包括:
·loadLogs(): 启动Kafka Broker时会从配置的log dirs下加载log segments
·getLog(partition): 用于在运行时获取某个分区的Log对象
·createLog(partition):为某个Partition创建replica时,会用到。(同步操作)
·deleteLog(partition): 删除指定分区(同步操作)
·cheanupExpiredSegments(log):清理过期的segments, 判断过期与否的标准是:
currentTime- segment.lastModified > log.config.retentionMs 。也就是说以segment的.log文件的最后修改时间为基准,之后在磁盘上保留时间超过 retentionMs就认为是过期的。
该动作会定期执行。
·cleanupSegmentsToMaintainSize(log):如果Partition Log的数据文件(.log)的size超过了log.config.retentionSize就会开始对segments从前往后进行遍历执行deleteSegment(),直到log.size < log.config.retentionSize为止。
·cleanupLogs() // 本质是调用cleanupExpiredSegments(), cleanupSegmentsToMaintainSize()
- truncateTo(Map<TopicAndPartition,Long>) 对log进行截断操作。当某个replica超出时才会执行。
·flushDirtyLogs() 定期flush Log。一个Log是否要flush 该log的最后一次flush时间,具体来说就是 currentTime – log.lastFlushTime > log.config.flushMs 时,就会强制flush。
- ·checkpointRecoveryPointOffsets (): 将所有的log dir下所有的partition log的 recovery point 写到log dir下的recovery-point-offset-checkpoint 文件中。以便于重启时进行log recover操作。
4.2 startup()
def startup() { /* Schedule the cleanup task to delete old logs */ if(scheduler != null) { info("Starting log cleanup with a period of %d ms.".format(retentionCheckMs)) scheduler.schedule("kafka-log-retention", cleanupLogs, delay = InitialTaskDelayMs, period = retentionCheckMs, TimeUnit.MILLISECONDS) info("Starting log flusher with a default period of %d ms.".format(flushCheckMs)) scheduler.schedule("kafka-log-flusher", flushDirtyLogs, delay = InitialTaskDelayMs, period = flushCheckMs, TimeUnit.MILLISECONDS) scheduler.schedule("kafka-recovery-point-checkpoint", checkpointRecoveryPointOffsets, delay = InitialTaskDelayMs, period = flushCheckpointMs, TimeUnit.MILLISECONDS) } if(cleanerConfig.enableCleaner) cleaner.startup() }
启动时,启动了三个定时任务:cleanupLogs(), flushDirtyLogs(), checkpoinRecoveryPointOffset()
4.3 shutdown()
这个没有什么好说的,主要就是将所有partition log中未完成的内容flush到磁盘。
5、Log Compaction
Kafka 除了可以作为一个MQ外,因为它会把所有的数据存储到磁盘,所以它还可以作为一个数据存储系统。一旦要作为一个数据存储系统,数据可能是无限,但磁盘毕竟是有限的,
如此存储了大量的无效数据、重复数据等,磁盘很快就没有了。
所以呢,如果作为一个存储系统来使用Kafka时,对数据进行整理是很有必要的了。
日志整理最终要达到的目的是:
只保留每一个key 的最后一条数据。而在Kafka Broker中 日志整理的工作是由 LogCleaner来完成的,就是LogManager中的那个cleaner。
默认情况下,是不会开启的。此外,一旦开启了,在启动kafka时,就不会进行 log recove的操作了。
6、Configuration
6.1 Partition Log Configuration
1) log.dir, log.dirs
配置日志目录,如果有多块磁盘的话,最好利用上多磁盘来提升读写性能。
一旦使用了log.dirs,一个topic分配给某个Broker的那些partation,会相对均匀的分布到各个磁盘里。
2) 定时flush盘配置:log.flush.interval.ms,log.flush.scheduler.interval.ms
log.flush.scheduler.interval.ms是LogManager指定定时任务flushDirtyLogs的周期
log.flush.interval.ms是LogManager中的定时任务执行flushDirtyLogs内部,会首先基于该配置判定是否到了再次flush盘时间。
3) 定时写recoverypoint的配置:log.flush.offset.checkpoint.interval.ms
它是LogManager调度checkpointRecoveryPointOffsets的周期。默认值 60000,即 1分钟
4) 分区日志保留大小:log.retention.bytes
每个partition log在磁盘里最多保留大小。
LogManager中的定时任务cleanupLogs()在按照size方式来清理Segments时(调用的是cleanupSegmentToMaintainSize)时,会根据这个值进行计算需要删除多少segment。
5) 分区日志保留时间:log.retention.hours, log.retention.minutes, log.retention.ms
每个Partition Log Segment在磁盘上保留最长时间。
LogManager中的定时任务cleanupLogs在按照time方式来清理Segments时(调用的是 cleanupExpiredSegments)时,会根据这个值来进行判断哪些segment要被删除。
这三个配置项都可以,优先级是:log.retention.hours > log.retention.ms > log.retention.minutes
Log.retention.hours 的默认值是 168,即7d。
6) 保留日志检查间隔:log.retention.check.interval.ms
其实就是LogManager执行cleanupLogs()任务的周期
7) 异步删除Log segment时的延迟配置:log.segment.delete.delay.ms
其实就是LogManager.cleanupLogs()中两种清理策略下的,在删除的log segment 时,会采用异步删除方式,具体来说就是会调用scheduler.schedule(deleteSegment, delay)
这里配置的 就是这个delay.
8) Log 基于time的roll配置:log.roll.hours, log.roll.ms, log.roll.jitter.hours, log.roll.jitter.ms
log.roll.hours, log.roll.ms 用于在Log.append(messages)前,判断是否需要roll时的基于time的判断方案。
log.roll.jitter.ms, log.roll.jitter.hours 是判断时额外考虑的抖动时间。
具体的判断算法是:
segment.size > 0 && time.milliseconds - segment.created > config.segmentMs - segment.rollJitterMs
配置的优先级:
log.roll.ms > log.roll.hours
log.roll.jitter.ms > log.roll.jitter.hours
9) Log基于size的roll配置:log.segment.bytes
在Log.append(messages)前,判断是否需要roll时的基于size的判断方案。
判断算法:
segment.size > config.segmentSize - messagesSize
segment.size 是 segment当前的size,messagesSize是要append的messages的size
10) log.flush.interval.messages
在Log.append()的最后阶段,会根据这个判定是否要执行一次同步flush盘操作。具体判定标准就是: log.endoffset – log.recoverypoint > log.flush.interval.messages
默认值是Long.MAX ,其实就是关闭了这个flush盘的动作,原因是它是个同步操作。
11) 其他的次要的配置:
log.preallocate :在创建一个新的segment时,要不要给.log文件预设size,默认是false。在windows上运行broker时,需要配置为true。
log.index.size.max.bytes: .index文件的大小限制,至少为8。默认值是 10 * 1024 * 1024 ,也即是 10M 。其实在Log.append()中判断是否需要roll时,也会基于它来判断是否需要roll。
Index文件的中的一条记录只有 8byte,所以默认的10M就代表了 一个segment中默认是可以存下10* 1024* 128 个messageSet的。
6.2 Log Compaction Configuration
对应日志整理,目前并未深入了解,有关配置仅在此处列出:
log.cleaner.backoff.ms |
The amount of time to sleep when there are no logs to clean |
long |
15000 |
[0,...] |
medium |
log.cleaner.dedupe.buffer.size |
The total memory used for log deduplication across all cleaner threads |
long |
134217728 |
medium |
|
log.cleaner.delete.retention.ms |
How long are delete records retained? |
long |
86400000 |
medium |
|
log.cleaner.enable |
Enable the log cleaner process to run on the server? Should be enabled if using any topics with a cleanup.policy=compact including the internal offsets topic. If disabled those topics will not be compacted and continually grow in size. |
boolean |
true |
medium |
|
log.cleaner.io.buffer.load.factor |
Log cleaner dedupe buffer load factor. The percentage full the dedupe buffer can become. A higher value will allow more log to be cleaned at once but will lead to more hash collisions |
double |
0.9 |
medium |
|
log.cleaner.io.buffer.size |
The total memory used for log cleaner I/O buffers across all cleaner threads |
int |
524288 |
[0,...] |
medium |
log.cleaner.io.max.bytes.per.second |
The log cleaner will be throttled so that the sum of its read and write i/o will be less than this value on average |
double |
1.7976931348623157E308 |
medium |
|
log.cleaner.min.cleanable.ratio |
The minimum ratio of dirty log to total log for a log to eligible for cleaning |
double |
0.5 |
medium |
|
log.cleaner.threads |
The number of background threads to use for log cleaning |
int |
1 |
[0,...] |
medium |