Kafka: LogManager


 

        进入Kafka的数据,都被作为日志存储到磁盘上了,并能够在磁盘上保留一段时间,最后被清理。其中,每一个分区的日志,还具备有日志轮转功能,这个设计类似于Log4j等日志系统。此外,kafka号称性能极好,它有disk io操作,查询性能居然也很好,它是怎么做到的呢?

 想了解大数据的学习路线,想学习大数据知识以及需要免费的学习资料可以加群:784789432.欢迎你的加入。每天下午三点开直播分享基础知识,晚上20:00都会开直播给大家分享大数据项目实战。

 

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

猜你喜欢

转载自blog.csdn.net/jingluodashen/article/details/80709523