【原创】Kakfa log包源代码分析(二)

时间:2023-03-08 22:32:30
【原创】Kakfa log包源代码分析(二)
八、Log.scala
日志类,个人认为是这个包最重要的两个类之一(另一个是LogManager)。以伴生对象的方式提供。先说Log object,既然是object,就定义了一些类级别的变量,比如定义了一个日志文件的后缀名是.log; 索引文件的后缀名是.index; 要被删除的文件的后缀名是.deleted; 要被执行日志清理的临时文件后缀名是.cleaned; 在做swap过程中的临时文件后缀名是.swap。还有一个后缀名.kafka_cleanshutdown,这个在0.8.2版本的Kafka中已经不使用了。除了这些后缀名,这个object还定义了一些常用的方法:
1. filenamePrefixFromOffset:  其实就是为给定的offset前面补至20位生成日志段文件名用于文件名排序(使用ls命令)
2. logFilename:  使用给定的基础offset在给定的路径下生成对应的日志文件,比如位移是1,那么生成的文件名是0...(总共19个)1.log
3. indexFilename:  与logFilename类似,只不过生成的索引文件名是0...(共19个)1.index
4. parseTopicPartitionName:  将日志所在路径的名称解析成topic+分区封装到一个TopicPartition对象返回。比如路径名字是log-topic-0,那么topic名字就是log-topic,分区号就是0
说完了Log object,现在说说Log class了。这里要说的是日志是只能在尾部追加消息的,一个日志对象就是一组日志段(LogSegment)对象,每一个日志段都有一个基础offset标识该日志段中的第一条消息的位置。Kafka支持基于大小和时间的规则创建新的日志段。
Log类有5个构造器参数,分别是:
1. dir:  日志段被创建在哪个目录下
2. config:  日志配置信息
3. recoveryPoint:  恢复的起始offset——即尚未被写入磁盘的第一个offset
4. scheduler:  用于后台操作的一个调度器线程池。主要用于异步地删除日志段和日志段切分时使用
5. time:  提供时间服务的对象实例
该类被标记为是线程安全的,因此一定需要一个锁对象来保护对日志的并发修改。另外还定义了一个字段专门保存该日志上次被写入磁盘的时间:  lastflushedTime。前面说过一个日志对象是由多个日志段组成的,所以该类定义了一个一组日志段Map对象(key就是日志段的基础offset,value就是该日志段)表示该日志包含的所有日志段。值得注意的是,这个map对象使用了java.concurrent包中的ConcurrentNavigableMap——该类提供了很多对于Map的便捷的导航方法。另外,Log类还定义了一个nexxtOffsetMetadata变量用于计算下一条消息的位移,主要计算的方法就是调用LogSegment的nextOffset方法。同时,根据给定的路径名,该类还定义了一个TopicAndPartition对象,把从路径名中提取出来的topic和分区信息保存起来表明这个日志是属于哪个topic,哪个分区的。最后类定义了4个度量元分别统计日志的起始位移、结束位移、日志大小以及日志段的数目。
okay,分析完了所有类成员字段,我们对于类定义的方法进行逐一分析:
1. name:  返回日志路径名称
2. updateLogEndOffset:  使用给定的offset创建一个新的LogOffsetMetadata对象更新到nextOffsetMetadata变量
3. hasCleanShutdownFile:  判断是否存在clean shutown文件——这个功能在Kafka0.8.2以后就不再使用了,主要是为了后向兼容。
4. numberOfSegments:  日志的日志段的数目——主要调用Map.size方法实现,该方法时间复杂度是O(n),所以在日志段数目比较多时要慎用
5. close:  以同步的方式将日志段map中的所有日志段都关闭
6. size:  所有日志段字节数总和
7. logStartOffset:  日志段集合中第一个日志段的基础位移,也就是这个日志对象的基础位移
8. logEndOffsetMetadata:  下一条将要被加入到日志的消息的位移元数据,直接返回nextOffsetMetadata字段
9. logEndOffset:  下一条将要被加入到日志的消息的位移
10. unflushedMessage:  已加入日志但未写入磁盘的消息数
11. flush:  写入所有日志段中尚未持久化的消息
12. delete:  完全删除该日志文件以及目录,并清空日志段map
13. lastFlushTime:  返回最近一次写入磁盘的时间
14. activeSegment:  日志段map中的最后一个日志段表示当前活跃的日志段
15. logSegments无参版:  返回按照offset递增排序的日志段集合
16. addSegment:  将给定的日志段对象加入到现有的日志段map中
17. asyncDeleteSegement:  异步删除给定的日志段对象,通常是请求发起1分钟之后开始执行
18. replaceSegments:   该方法只在启用了日志清理(log cleaning)时候才会被用到。主要逻辑就是将.cleaned文件换成.swap文件,然后再将.swap加入到现有的日志段map中。之后,遍历整个要删除的日志段集合,如果要删除的日志段的基础offset与要新加的日志段的基础位移不一样,说明该日志段的确要被删除,那么调用异步删除日志段的方法将其删除。遍历之后,再将那个新加的日志段的后缀名.swap去掉
19. deleteSegment:  异步删除日志段,在日志段map中将日志段记录去除,并且将对应索引文件改名为*.deleted
20. logSegments带参版:  首先获取不大于from的最大位移,如果不存在这样的offset,那么直接返回小于to的所有位移对应的日志段; 否则返回[floor, to)范围内的日志段
21. truncateFullyAndStartAt:  删除日志所有数据,创建一个空的日志段并重设新的offset(设置起始offset和结束offset为newOffset),最后更新恢复位移点 
22. truncateTo:  将日志截断使之保存的最大offset不会超过给定的targetOffset。当然,如果targetOffset就比现有日志的结束位移还要大自然什么都不做。另外在截断的过程中,还需要判断该log的最小位移(也就是第一个日志段的基础位移)如果比targetOffset大的话,那么直接调用truncateFullyAndStartAt方法删除所有日志数据并设置新的位移点,否则逐一删除那些起始位移比targetOffset大的日志段。此时activeSegment会自动变成当前删除之后最新的那个日志段,所以还要对activeSegment进行截断操作。这些做完之后更新下一条消息offset并重设恢复点位移
23. roll:  日志段的切分。使用当前结束位移作为新日志段的起始位移并把新日志段加入到日志段map中,然后发起一个异步调度任务将旧有日志段的数据同步到磁盘上,最后返回新创建的日志段
24. maybeRoll:  也是做日志段的切分,不过是有条件的: 1. 日志段已经满了; 2. 已过最大时间; 3. 索引文件已经满了。三个条件满足一个就会触发切分操作。
25. deleteOldSegments:  删除掉那些满足条件的日志段——所谓的条件无非就是满足大小或时间方面的要求,而这个函数返回的就是删除日志段的个数。具体做法就是筛选出那些同时满足给定条件并且不能是当前激活日志段或大小不为空的日志段,然后比较一下看看要删除的是否所有日志段,如果是的话直接调用roll方法进行切分,因为Kafka至少要保留一个日志段,如果否的话直接遍历该候选日志段集合,然后删除之。
26. convertToOffsetMetadata:  将给定的位移转化成对应的LogOffsetMetadat对象,这个方法主要用于副本管理使用
27. read:  先说说这个方法的返回对象:  FetchDataInfo——这是一个case类,包含了日志位移元数据信息以及一个消息集合。这个方法也很简单,就是从日志中读取消息,将起始位移和读取到的消息集合封装进一个FetchDataInfo中。此方法接收3个参数:  startOffset表示读取操作执行的开始位移点; maxLength表示最多读取的字节数; maxOffset表示读取操作不能超过的位移点,即返回的消息集合中不能包含该位移。具体逻辑如下: 首先检查下一条消息的位移与给定的起始位移,如果两者相等直接返回,只不过是空的消息集合。否则的话,找到小于等于startOffset的最大位移所在的日志段。如果startOffset比当前最大位移还大或者压根就没有找到刚才的日志段,那么说明要读取的内容已经超出了日至当前的结束offset,直接报错退出。okay,如果到这里很运行正常的话,那么下面就开始循环读取消息: 如果读取的消息集合不为空直接返回,否则跳到下一个日志段继续读取直到下一个日志段为空退出循环。此时的情况是我们已经跨过了最后一个日志段但给定的startOffset确实合法的值——这是有可能的,比如所有比startOffset大的消息都已经被删除了。如果是这样的话,程序简单地返回一个包含空消息集合的FetchDataInfo对象。
26. trimInvalidBytes:  消除消息集合尾部的无效字节。在学习这个方法之前,我们要了解Kafka在这个scala文件中定义一个case class类: LogAppendInfo类——这个类保存了每个消息集合的各种信息: 包括这个集合的起始位移、结束位移、未压缩消息(shallow message)数,合法字节数、用到的压缩算法以及表明该消息集合位移是否是单调增加的布尔值。现在再来看trimInvalidBytes方法,这个方法接收2个参数: 一个要是做trim的消息集合,另一个是用LogAppendInfo对象标识的消息集合的通用信息,结果自然是被trim过的消息集合——可能与原消息集合相同。具体做法是: 首先计算出这个消息集合的合法字节数(这要通过analyzeAndValidateMessageSet方法给出,后面会说这个方法)——如果合法字节数小于0,直接报退出。如果该字节数就是消息集合的字节数,那么说明不用做trim直接返回传入的messages即可。否则即说明有非法的字节,那么就新建一个ByteBufferMessageSet消息集合,将limit设置为刚才计算的合法字节数,然后返回。
27. analyzeAndvalidateMessageSet:  上面的方法提到了一个消息集合的合法字节与非法字节,那么如何定义合法性呢?答案就有这个方法给出。从字面上来说,这个方法做的工作就是分析验证消息集合,主要的工作有: a. 验证消息CRC码; b. 验证消息长度合法性; c. 计算消息集合的起始位移; d. 计算结束位移; e. 消息集合中的消息数;  f. 计算合法字节数(就是合法消息字节数的累加和);  g. 验证位移是否单调增加;  h. 验证是否使用了压缩,如果指定了多个,只以最后一条消息的压缩算法为准
28. loadSegments:  该方法就是加载磁盘上的日志文件。具体逻辑如下: a. 如果给定的路径不存在则创建出来;  b. 遍历该目录路径下的所有文件删除掉那些临时文件(包括后缀名是.deleted和​.cleaned); c. 如果发现是以.swap结尾的文件,说明在上一次的swap过程中Kafka失败了,需要执行恢复操作。针对上面的情况,先去掉结尾的.swap然后判断是.log还是.index结尾。如果是索引文件(.index结尾)则直接删除,反正后面可以重建; 如果是日志数据文件(.log结尾),那么先删除对应的索引文件,然后将.swap去掉表示修复成功; d. 第一遍遍历之后再次进行第二遍遍历。对目录下的每个文件,如果它是索引文件,则寻找对应的.log文件,如果不存在抛出告警信息并直接该索引文件; 如果存在的话不做任何处理; 但如果该文件本身就是日志数据文件,则必然是000000...0000【offset】.log这样的形式;  e. 提取基础offset,并判断是否存在对应的索引文件,然后就创建新的日志段对象。f. 创建日志段之后判断是否存在索引文件,如果没有的话重建索引;  g. 最后将新创建的日志段加入到日志段map中,至此第二遍遍历完成;  h. 此时判断日志段map中是否存在任何日志段,如果没有的话则创建一个offset为0的空日志段——因为每个日志都至少要有一个日志段。如果map中的确有日志段,先调用recoverLog方法(稍后会说)恢复日志段然后重设activetSegment的索引长度(否则容易引发日志段切分);j. 最后为每个日志段检查对应的索引文件(确保索引文件为空以及索引长度一定要是8的倍数,因为索引项长度总是位移的整数倍)
29. recoverLog:  主要为日志段map中自恢复点起的每个日志段重建索引文件并且砍掉那些位于日志和索引尾部的无效字节。如果发现确实存在无效字节,那么就把那些日志段全部删除掉
30. append:  添加给定的消息集合到当前激活的日志段中,如果满足条件的话做切分。
九、LogCleanerManager.scala
首先要说的是,这是一个对log包私有的类,只在LogCleaner内使用用于管理每个要被清理的分区状态。日志清理分别有三个状态:  正在清理(LogCleaningInProgress)、清理结束(LogCleaningAborted)和清理暂停(LogCleaningPaused)。每一个要被清理的分区都要首先被设置为“正在清理”状态。当分区在被清理的时候,可以申请终止或暂停该清理过程。而如果分区处于暂停状态,只有清理线程重新恢复时才会重新清理该分区。
这个类的构造函数接收两个参数:  
1. logDirs:  一个文件对象数组,每个文件代表要执行清理操作的日志文件所在的目录
2. logs:  一个池对象,底层用ConcurrentHashMap实现,将topic与分区的消息和对应的日志对象关联起来。
这个类还定义以下的成员变量:
1. loggerName:  用于写日志的logger对象
2. offsetCheckpointFile:  包私有的字符串变量,只做测试用的文件名使用
3. checkpoints:  创建一个路径=>OffsetCheckpoint对象的映射,为每一个日志都保存一个offset的检查点信息
4. inProgress:  当前正在被清理的日志集合,key是TopicAndPartition,value是LogCleaningState
5. lock:  使用ReentrantLock实现的全局锁,用于控制对清理进行中的集合以及检查点的所有访问
6. pausedCleaningCond:  配合lock使用,使用Java concurrent.locks包的Condition类实现,提供类似于Object.wait, Object.notify的方法。主要用于等待分区状态切换到暂停状态
7. dirtiesLogCleanableRatio:  创建了一个叫“max-dirty-percent”的度量元用于追踪可清理日志所占总的“脏”日志的百分比。
该类定义的方法如下:
1. allCleanerCheckpoints:  读取检查点文件内容,该文件内容格式固定为: 第一行是版本号0,第二行是数字N,表示一共有N项offset检查点记录,后面有N行,每行都是检查点记录,格式为topic名 分区号 位移
2. grabFilthiestLog:  顾名思义,就是要挑出所有日志中"最脏最需要清理"的那个来并把它动态地添加到正在清理的集合中。清理线程每次运行时都调用这个方法重新计算要最新要清理的日志。具体做法就是: 首先选取那些启用了compact(即日志清理)且本身不在in-progress集合中的日志对象,然后为它们每一个创建一个新的LogToClean对象——这个class后面会说到,大概就是要保存topic、分区以及最近的清理位移点。从这些LogToClean对象中选取可清理百分比最高的那一个,将它视为“最脏”也是最需要马上清理的日志并更新dirtiestLogCleanableRatio变量保存起来。之后我们在刚才计算的LogToClean集合中挑出那些可清理比率大于50%(因为Kafka默认的min.cleanable.dirty.clean比率是0.5)的来——如果没有,直接返回None,说明没有需要清理的;如果集合有数据,那么在从中选出可清理比率最高的那个加入到inProgress集合后返回。
3. abortAndPauseCleaning:  终止一个特定分区当前正在进行的清理工作并且暂停该分区后续所有的清理工作。该方法会一直处于阻塞状态直到分区清理工作被终止以及后续操作被暂停。具体流程如下: a.如果分区本身并不在"正在清理"集合中,那么只是将其标记为暂停;b. 如果分区本身就在inProgress集合中,那么首先标记为已终止(aborted)状态;c. 清理者线程定期地轮询状态,一旦发现状态变为已终止,那么抛出LogCleaningAbortedException停止清理任务;d. 清理任务结束后调用doneCleaning方法来设置分区状态为已暂停(paused);e. 最后abortAndPauseCleaning方法一直等待直到分区状态变为paused后退出。
4. resumeCleaning:  恢复一个处于paused状态的分区的清理状态,该方法会一直处于阻塞状态直到恢复操作执行结束。具体流程为首先判断该分区是否在inProgress集合中,如果不在抛出异常表明该分区本身不需要恢复因为它原本也不是处于暂停状态;如果该分区本身处于paused状态,那么将其从inProgress集合中移除
5. abortCleaning:  终止一个特定分区当前正在进行的清理工作。该方法会一直处于阻塞状态直到终止操作执行结束。具体流程就是先调用abortAndPausedCleaning方法将给定分区置于paused状态,然后再次调用resumeCleaning方法将该分区恢复——其实就是只做终止清理这一项任务,不要设置状态
6. isCleaningInState:  判断一个分区的清理状态。调用该方法的时候最好加锁保证结果的一致性。具体流程也很简单,如果该分区不在inProgress集合中,那么返回false,因为该分区肯定不处于任何状态;相反地,如果在inProgress集合中,那么判断与给定状态是否一致
7. checkCleaningAborted:  检查一个分区的清理状态是否是已终止,如果是的话抛出异常LogCleaningAbortedException
8. updateCheckpoints:  更新日志的检查点文件。具体流程是返回给定日志文件对应的OffsetCheckpoint对象加上给定的要更新的一起写回到检查点文件中。
9. doneCleaning:  保存结束位移,并将inProgress集合中所有不是"已终止"状态的日志都移除
十、LogCleaner.scala
    这个源文件主要用于做日志清理(log cleaning),也就是使用一组清理者线程为启用了日志压缩(log compact)的topic删除过期日志记录。那么很自然的一个问题就是,什么是topic的过期日志?Kafka规定,如果消息M1和M2的键都是K1,但位移数分别是O1和O2且O1<O2, 那么就认为M1是过期记录,可以删除。
    一旦启用了日志压缩,每个日志都可以认为是由两组日志段构成的: "干净"的日志段组——以前被清理过和需要被清理的日志段组。值得注意的是,当前正在使用的日志段是不能被清理的。
    Kafka使用一个线程池来执行日志清理。每个线程计算出"最需要清理"的日志并对其进行清理。一个日志的"需要清理"的程度大致由下面的百分比公式表征:  需要清理的字节数/日志总的字节数。
    日志清理的流程大致如下: 1. 首先为需要清理的日志创建一个mapping保存key到last_offset。这里的key被封装进一个ByteBuffer对象,其实就是消息的key;2.一旦创建了这样的映射关系,Kafka会重拷贝每个满足条件的日志段从而实现日志压缩的目的,不满足的条件就是key对应的位移比日志段的位移都要大。
    为了避免多次压缩之后将日志文件和索引文件都弄的非常小,Kafka支持可以合并连续的小日志段。另外还需要处理日志截断的情况。如果在日志清理过程中发生日志截断,则直接中断该日志清理工作。
    值得一提的是,payload是null的消息被看做是一个删除标志位(delete marker或delete tombstone)——日志清理者对于这类消息的处理是不同的,并且它只会保存这类消息一段时间(默认是1天)。
    这个scala文件定义了4个类,分别是LogCleaner、Cleaner、CleanerStats和LogToClean。我们先从简单地开始说:
LogToClean类
这是个日志的helper的case类,保存了topic、分区和第一个需要清理的字节位移点(firstDirtyOffset)信息。另外还定义了一些常用的方法和变量:
1. cleanBytes:  计算从位移[0, firstDirtyOffset)内的所有日志段的总字节数——这个值就是总的"干净"的字节数
2. dirtyBytes:  计算从位移[firstDirtyOffset, 当前正在使用的日志段基础位移)的所有日志段的总字节数——这个值是总的"脏"的字节数,也就是需要清理的部分
3. totalBytes:  总的字节数,即需要清理的部分+不需要清理的部分,所以是cleanBytes + dirtyBytes
4. cleanableRatio:  可清理程度百分比,用dirtyBytes/totalBytes
5. compare:  因为这个类继承了Ordered方法,所以需要复写compare方法以实现排序。具体方法是调用java.lang.Math的signum方法比较两个LogToClean实例的cleanableRatio以计算出谁更需要马上进行日志清理。
CleanStats类
顾名思义,就是日志清理的统计信息类,包括清理开始时间(startTime)、映射创建完成时间(mapCompleteTime)、清理结束时间(endTime)、已读字节数(bytesRead)、已写入字节数(bytesWritten)、已读映射索引字节数(mapByteRead)、已写入映射索引字节数(mapMessagesRead)、已读消息数(messagesRead)、已写入消息数(messagesWritten),以及OffsetMap底层缓冲区的使用率(bufferUtilization)等。该类定义的方法还有:
1. clear:  清除所有统计信息的值,重置为初始值
2. readMessage:  读取1条消息将messagesRead加一,同时将给定的字节数size更新到bytesRead
3. recopyMessage:  与readMessage效果相同
4. indexMessage:  从mapping索引中读取一条消息时将mapMessagesRead数+1,同时将给定的size数更新到mapBytesRead
5. indexDone:  表示作为索引的mapping构建完成,因此需要更新mapCompleteTime
6. allDone:  标识日志清理结束,更新endTime
7. elapsedSecs:  计算日志清理花费的时间,单位是秒
8. elapsedIndexSec:  计算构建索引mapping的时间,单位是秒
Cleaner类
这个类是真正执行日志清理逻辑的,一共有8个构造函数参数:
1. id:  清理者线程id
2. offsetMap:  保存key=>offset映射的mapping对象
3. ioBufferSize:  读缓冲区/写缓冲区大小,默认是属性log.cleaner.io.buffer.size/log.cleaner.threads/2——为什么除以2?因为Cleaner会创建2个这样大小的缓冲区分别给读、写使用
4. maxIoBufferSize: 最大的io缓冲器大小,由属性max.message.bytes决定,默认是1000000字节——这是Kafka能允许的最大消息长度
4. dupBufferLoadFactor:  日志清理所用哈希表的负载因子,由属性log.cleaner.io.buffer.load.factor指定,默认是0.9
5. throttle:  控制清理IO速度用的
6. time: 提供时间服务
7. checkDone:  一个函数,接收TopicAndPartition类型的参数去检查,通常都是检查清理线程运行状态以及这个partition清理状态
该类会维护一个元组用来保存当前清理过程以及上一次清理完成的过程,另外还创建了2个读写IO buffer。下面说说它提供的方法:
1. restoreBuffers:  恢复IO缓冲器大小为初始值
2. growBuffers:  扩容IO缓冲器大小至2倍,但不能超过maxIoBufferSize的限制
3. buildOffsetMapForSegment:  将日志段中给定消息添加到位移mapping中。这里要注意的是这个日志段中的消息必须是有key的,因为日志压缩就是根据key来实现的。另外还有一个问题就是如果一条消息的大小比读缓冲区大小要大的话那么需要调用growBuffers来扩容读缓冲区以容纳大消息,当然了,在构建完mapping之后要恢复读缓冲区的大小长度
4. buildOffsetMap:  就是用于构建key => offset的mapping,key相同时会更新offset,即只保存最后的offset。当然这里的key要属于日志中需要清理的部分。具体做法就是首先清除所有统计信息,然后根据给定的start,end位移计算出需要清理的日志段集合。之后为该集合中的每一个"脏"的日志段构建mapping(调用buildOffsetMapForSegment方法)。当然调用buildOffsetMapForSegment的前提是需要判断每个日志段的起始位移是否在预估的结束位移之后(预估的结束位移就是哈希表槽位数乘以负载因子再加上dirty段起始位移),如果没有超出自然可以为该段创建映射,但如果超出了也不一定就不能,因为可能存在同一个key的多条消息,这样在哈希表中只是一条记录而已。
5. groupSegmentsSize:  将一组日志段分为不同的组中,每个组的数据和索引大小都不超过给定的上限,并返回一个日志段分组的列表。Kafka收集这样的一组日志段把它们合并成一个日志段以防止日志清理操作后有些日志段的大小会缩减地太大。举个例子来说,如果有一组日志段(s1, s2, s3),日志数据长度分别是100字节,200字节,300字节,现在我们设定每个组的最大长度是400字节,那么最后的结果就是一个列表,里面有两个group:  (s1, s2)和(s3)
6. cleanInto:  使用给定的key=>位移映射关系将源日志段清理到一个目标日志段中去。其中对于源日志段中的消息,有两种是我们可以忽略不计的: 一种是位移比mapping中的小;另一种是空消息体的消息(本身就是一个删除标识符)且已经过了log.cleaner.delete.retention.ms规定的时间——这两种消息我们不用存入目标日志段
7. cleanSegments:  清理一组日志段到一个单个的日志段。具体做法就是创建一个后缀是.cleaned的日志文件和索引文件,然后使用它们创建一个新的日志段,然后不断地调用cleanInto方法将每个日志段清理并加到这个新创建的日志段中,最后调用replaceSegments方法使新日志段生效
8. clean: 其实是Cleaner类的主方法,就是用于清理日志,并返回清理后的第一个位移。具体流程就是调用之前定义的所有方法组合成一段逻辑: 先构建位移mappig,然后将所有日志段分组并根据mapping逐一做清理
LogCleaner类
这个类很类似于一个日志清理的管理类。构造函数接收4个参数:
1. config:  清理者配置类
2. logDirs:  日志文件目录
3. logs:  一个日志的哈希表
4. time:  提供时间服务
定义的变量和方法有:
1. cleanManager: 分区清理工作的管理者,主要用于终止、恢复清理工作等
2. newGauge:  一堆度量元用于追踪清理工作的各种指标
3. throttler:  限制清理者线程IO速率用的
4. cleaners:  一组CleanerThread线程类,执行真正的日志清理工作。每个线程都会不断地计算最需要清理的日志并执行清理
5. startup:  启动所有清理者线程
6. shutdown: 关闭所有清理者线程
7. abortCleaning:  中断某个分区当前的清理工作。
8. updateCheckpoints:  更新检查点文件并且删除掉那些已不存在的topic和分区。检查点文件名字叫cleaner-offset-checkpoint
9. abortAndPauseCleaning:  中断某个分区当前的清理工作并暂停后续的清理
10. resumeCleaning:  恢复已暂停的分区清理状态
11. awaitCleaned:  只用于测试。不多说了~~
CleanerThread类
这个类是CleanManager类的私有内部类,表示一个清理者线程。在这个线程中会创建一个Cleaner实例并且使用它在方法cleanOrSleep中执行真正的清理工作。
十一、LogManager类
这个类应该说是log包中最重要的类了,也是kafka日志管理子系统的入口。日志管理器(log manager)负责创建日志、获取日志、清理日志。所有的日志读写操作都交给具体的日志实例来完成。日志管理器维护多个路径下的日志文件,并且它会自动地比较不同路径下的文件数目,选择在最少的日志路径下创建新的日志。Log manager不会尝试去移动分区,另外专门有一个后台线程定期地裁剪过量的日志段。下面我们先看看这个类的构造函数参数
1. logDirs:  log manager管理的多组日志目录
2. topicConfigs:  topic=>topic的LogConfig的映射
3. defaultConfig:  一些全局性的默认日志配置
4. cleanerConfig:  日志压缩清理的配置
5. ioThreads:  每个数据目录都可以创建一组线程执行日志恢复和写入磁盘,这个参数就是这组线程的数目,由num.recovery.threads.per.data.dir属性指定
6. flushCheckMs: 日志磁盘写入线程检查日志是否可以写入磁盘的间隔,默认是毫秒,由属性log.flush.scheduler.interval.ms指定。
7. flushCheckpointMs: Kafka标记上一次写入磁盘结束点为一个检查点用于日志恢复的间隔,由属性log.flush.offset.checkpoint.interval.ms指定,默认是1分钟,Kafka强烈建议不要修改此值。
8. retentionCheckMs: 检查日志段是否可以被删除的时间间隔,由属性log.retention.check.interval.ms指定,默认是5分钟
9. scheduler: 任务调度器,用于指定日志删除、写入、恢复等任务
10. brokerState: Kafka broker的状态类(kafka.server包中)。Broker的状态默认有未运行(Not Running),启动中(Starting),从上次未正常关闭恢复中(Recovering from unclean shutdown),作为Broker运行中(running as broker),作为Controller运行中(running as controller),挂起中(pending)以及关闭中(shutting down)。当然Kafka允许用于自定制状态。
11. time: 和很多类的构造函数参数一样,提供时间服务的变量
Kafka在恢复日志的时候是借助检查点文件来做的,因此每一个需要做日志恢复的路径下都需要有这么一个检查点文件,名字也固定叫"recovery-point-offset-checkpoint"。另外由于在做一些操作时需要将目录下的文件锁住,因此Kafka还创建了一个后缀是.lock的文件标识这个目录当前是被锁住的。
下面我们针对具体的方法一一分析:
1. createAndValidateLogDirs: 创建并验证给定日志路径的合法性,特别要保证不能出现重复路径,并且要创建那些不存在的路径,而且还要检查每个目录都是可读的
2. lockLogDirs: 在给定的所有路径下创建一个.lock文件,如果某个路径下已经有.lock文件说明Kafka的另一个进程或线程正在使用这个路径
3. loadLogs: 恢复并加载给定路径下的所有日志。具体做法是为每个路径创建一个线程池。为了向后兼容,该方法要在路径下寻找是否存在一个.kafka_cleanshutdown的文件,如果存在的话就跳过这个恢复阶段,否则的话就将broker的状态设置为恢复中状态,真正的恢复工作是由Log实例来完成的。然后读取对应路径下的recovery-point-offset-checkpoint文件读出要恢复检查点。前面说到了检查点文件的格式大概类似于以下的内容:
第一行必须是版本0,第二行数是topic/分区数,以下每行都是三个字段: topic partition offset,读完这个文件之后会创建一个TopicAndPartition => offset的map。
========================
9
log-topic 0 48
kafkatopic 1 0
abcd 1 0
abcd 0 0
autocreated 0 2
abc 0 0
accesslog_topic 0 0
kafkatopic 0 0
autocreated 1 1
========================
之后为每个目录下的子目录都构建一个Log实例然后使用线程池调度执行清理任务。之后删除这些任务对应的cleanShutdown文件。至此日志加载过程结束
4. startup: 开启后台线程做日志冲刷(flush)和日志清理。主要是使用调度器安排3个调度任务: cleanupLogs、flushDirtyLogs和checkpointRecoveryPointOffsets——自然地有3个对应的方法来实现对应的方法。同时判断一下是否启用了日志压缩,如果启用了调用cleaner的startup方法开启日志清理。
5. shutdown: 关闭所有日志。首先关闭所有清理者线程,然后为每个日志目录创建一个线程池执行目录下日志文件的写入磁盘与关闭操作同时更新外层文件中检查点文件的对应记录
6. logsByTopicPartition: 返回一个map保存TopicAndPartition => log的映射
7. allLogs: 返回所有topic所有分区的日志
8. logsByDir: 日志路径 => 路径下所有日志的映射
9. flushDirtyLogs: 将任何超过了写入间隔且有未写入消息的日志全部冲刷到磁盘上
10. checkpointLogsInDir: 在给定的路径中标记一个检查点
11. checkpointRecoveryPointOffsets: 将日志路径下所有日志的检查点写入到一个文本文件中
12. truncateTo: 截断分区日志到指定的offset并使用这个offset作为新的检查点(恢复点)。具体做法就是遍历给定的map集合,获取对应的分区的日志,如果要截断的offset比该日志当前正在使用的日志段的基础位移还小的话(也就是说要截断一部分当前日志段),就需要暂停清理者线程了。之后开始执行阶段操作,最后再恢复清理者线程。
13. tuncateFullyAndStartAt: 删除一个分区所有的数据并在新offset处开启日志。操作前后需分别需要暂停和恢复清理者线程
14. getLog: 返回某个分区的日志
15. createLog: 为给定分区创建一个新的日志。如果日志已经存在则返回
16. deleteLog: 删除一个日志
17. nextLogDir: 创建日志时选择下一个路径,目前的实现是计算每个路径下的分区数然后选择最少的那个。
18. cleanupExpiredSegments: 删除那些过期的日志段,就是当前时间减去最近修改时间超出了规定的那些日志段,并且返回被删除日志段的个数
19. cleanupSegmentsToMaintainSize: 如果没有设定log.retention.bytes,那么直接返回0,表示不需要清理任何日志段(这也是默认情况,因为log.retention.bytes默认是-1);否则计算出该属性值与日志大小的差值。如果这个差值弄够容纳某个日志段的大小,那么这个日志段就需要被删除。
20. cleanupLogs: 删除所有满足条件的日志,返回被删除的日志数