1.数据目录
通过 LogDirsCommand ,也就是 kafka-log-dire.sh 脚本可以查看当前数据目录:
数据目录下面的索引目录下面就是当前副本的数据信息,其中每个索引由多个分区 <topic>-<partition>,也就是 topic-n 的目录:
下面是名称为 flinkin-10 这个主题的序号为0 的分区的数据目录,这里设置了两个副本,那么主从副本都有这个目录:
随着数据的写入,index、log 会按照序号生成多个 LogSegment,文件以第一条消息的 offset 命名如下,会按照配置(log.segment.bytes)以切割范围进行生成:
可以使用 kafka-dump-log.sh 读取当前 LogSegment 的log、index文件,调用的 kafka.tools.DumpLogSegments 类:
解析log:--files C:\workspace\kafkaData\data1\flinkin-10-0\00000000000000000000.log --print-data-log
解析index:--files C:\workspace\kafkaData\data1\flinkin-10-0\00000000000000000000.index
Dumping C:\workspace\kafkaData\data1\flinkin-10-0\00000000000000000000.index
offset: 54 position: 4158
offset: 108 position: 8316
offset: 162 position: 12474
offset: 216 position: 16632
offset: 270 position: 20790
offset: 324 position: 24948
offset: 378 position: 29106
offset: 432 position: 33264
offset: 486 position: 37422
Dumping C:\workspace\kafkaData\data1\flinkin-10-0\00000000000000000000.log
Starting offset: 0
baseOffset: 0 lastOffset: 0 count: 1 position: 0 CreateTime: 1677472156090 size: 77 crc: 731763849
baseOffset: 54 lastOffset: 54 count: 1 position: 4158 CreateTime: 1677472696515 size: 77 crc: 3602645097
...
baseOffset: 108 lastOffset: 108 count: 1 position: 8316 CreateTime: 1677473236908 size: 77 crc: 874173656
baseOffset: 162 lastOffset: 162 count: 1 position: 12474 CreateTime: 1677473777392 size: 77 crc: 971161618
baseOffset: 216 lastOffset: 216 count: 1 position: 16632 CreateTime: 1677474317808 size: 77 crc: 1917564448
baseOffset: 270 lastOffset: 270 count: 1 position: 20790 CreateTime: 1677474858266 size: 77 crc: 4149271504
baseOffset: 324 lastOffset: 324 count: 1 position: 24948 CreateTime: 1677475398670 size: 77 crc: 2050957251
baseOffset: 378 lastOffset: 378 count: 1 position: 29106 CreateTime: 1677475939070 size: 77 crc: 1989913168
baseOffset: 432 lastOffset: 432 count: 1 position: 33264 CreateTime: 1677476479473 size: 77 crc: 1879865070
baseOffset: 486 lastOffset: 486 count: 1 position: 37422 CreateTime: 1677477019975 size: 77 crc: 2931625151
...
log文件和index文件的对应关系:
主要是为了快速定位到指定 offset 的消息,index文件中两个字段的含义(offset,position)指的是指定 offset 的消息在 log 文件中的物理 position,这里大概每54条消息建立一个索引。先根据 offset 所在区间从index找到起始查找位置 position 比如要查找 offset=280 的消息,先找到 offset=270 对应的 position=20790,直接定位到 log 文件 position=20790 处,从这里作为起始查找点往后查找,匹配目标 offset=280 找到目标记录。
index是稀疏索引,index是全量索引,二次定位加快查找速度。
在 log 文件中每行记录的字段如下:
offset:491 消息偏移量
baseOffset: 491
lastOffset: 491
count: 1
baseSequence: -1
lastSequence: -1
producerId: -1
producerEpoch: -1
partitionLeaderEpoch: 0
isTransactional: false
isControl: false
position: 37807 当前记录在当前文件系统中的物理位置
CreateTime: 1677477070026
size: 77
magic: 2
compresscodec: NONE
crc: 2105625321
isvalid: true
payload: 内容消息
而每个主题的消费的 offset 并不在以上的文件中,而是在内部主题 __consumer_offsets 中。__consumer_offsets 是 kafka 自行创建的,和普通的 topic 相同。它存在的目的之一就是保存 consumer 提交的位移,同时有默认50个分区。
这个主题提交的消息结构是 groupId+topic+partition+offset,会根据 groupId 做哈希求模将详细写入
2.分区机制
根据分区机制,实际上在创建主题时就会确定分区、副本等信息如何分配并分配到哪些 broker 上:
ZkAdminManager.createTopics() 待创建主题集合:HashMap(flinkin-30 -> CreatableTopic(name='flinkin-30', numPartitions=2, replicationFactor=1, assignments=[], configs=[]))
ZkAdminManager.createTopics() 当前活跃broker:ArrayBuffer(BrokerMetadata(1,None))
AdminUtils.assignReplicasToBrokers() 分区副本到broker的分配计算入口
AdminUtils.assignReplicasToBrokersRackUnaware() 分区副本到broker分配计算 currentPartitionId=0,replicaBuffer=ArrayBuffer(1)
AdminUtils.assignReplicasToBrokersRackUnaware() 分区副本到broker分配计算 currentPartitionId=1,replicaBuffer=ArrayBuffer(1)
然后不同的 broker 在处理ISR变更请求后在本地建立 partition、replica 等 log 目录。因为有多分区机制,那么kafka不能保证跨分区的消息顺序性。
解决的办法就是需要保证顺序处理的消息都写入同一个主题的同一个分区,这样保证了这些消息在同一个 partition 中有序。但这样即使这样也无法保证顺序消费。
因为多个消费者顺序从partition 中取消息来消费根据任务处理速度无法保证顺序执行完毕。当然如果只有一个消费者的话就更能保证消费的顺序性了。
生产中通常是根据实体编号一致性hash的向同一个分区提交消息,而负责这些实体消息的处理由同一个消费者完成,同时在消息中定义消息id、
根据poll模式,消费者每拿到一批次消息,现在应用内按照消息id.sort()、实体id.sort()后再进行处理。
在创建主题时,不同 broker 节点的现有负载一样,所以多个分区到broker的分配,以及不同分区的多个副本到 broker 的分配都需要一些分配计算以均衡负载。创建主题时是否指定replica-assignment、broker.rack、disable-rack-aware等参数
使用replica-assignment参数:按照指定的方案来进行分区副本的创建。不使用replica-assignment参数:按照内部的逻辑来计算分配方案。如果指定机架信息的分配策略 则 broker.rack:当前broker在哪个机房,disable-rack-aware:是否指定机架信息。
各自计算代码如下:
AdminUtils.assignReplicasToBrokersRackUnaware()
AdminUtils.assignReplicasToBrokersRackAware()
默认分配代码如下:
private def assignReplicasToBrokersRackUnaware(nPartitions: Int,
replicationFactor: Int,
brokerList: Seq[Int],
fixedStartIndex: Int,
startPartitionId: Int): Map[Int, Seq[Int]] = {
val ret = mutable.Map[Int, Seq[Int]]()
val brokerArray = brokerList.toArray
// 根据brokers长度随机产生一个数 作为开始下标
val startIndex = if (fixedStartIndex >= 0) fixedStartIndex else rand.nextInt(brokerArray.length)
// 代表副本之间的broker间隔数,为了将副本分片更均匀的分配到brokers中
var currentPartitionId = math.max(0, startPartitionId)
var nextReplicaShift = if (fixedStartIndex >= 0) fixedStartIndex else rand.nextInt(brokerArray.length)
// 轮询所有分区,将每个分区的副本分配到broker中
for (_ <- 0 until nPartitions) {
// 分区编号大于0 且 分区编号能被brokers的长度整除时 副本间隔数+1
if (currentPartitionId > 0 && (currentPartitionId % brokerArray.length == 0))
nextReplicaShift += 1
// 第一个副本的下标是 当前分区编号+startIndex 后 与broker的个数取余数
val firstReplicaIndex = (currentPartitionId + startIndex) % brokerArray.length
val replicaBuffer = mutable.ArrayBuffer(brokerArray(firstReplicaIndex))
// 确定了分区的第一个副本的broker之后 通过 replicaIndex获取其余副本的broker
for (j <- 0 until replicationFactor - 1)
// 将此分区的副本信息保存到ret中 key为分区编号。
replicaBuffer += brokerArray(replicaIndex(firstReplicaIndex, nextReplicaShift, j, brokerArray.length))
ret.put(currentPartitionId, replicaBuffer)
// 为下一个分区分配副本
currentPartitionId += 1
}
ret
}
另外对于写入的消息如何确定写入到哪一个分区呢?这部分代码在 KafkaProducer 代码中实现调用路径如下,使用 kafka-console-producer.sh,调用 ConsoleProducer 调用消息发送可以进行测试:
KafkaProducer.doSend() => 调用生产者消息发送
KafkaProducer.partition() => 确定分区,partitioner=DefaultPartitioner
DefaultPartitioner.partition() => 确定分区
一共三中分区路由实现类:
(1)DefaultPartitioner:
如果消息中指定了分区(业务代码send消息时),则使用该分区
如果未指定分区但存在key,则根据序列化key使用murmur2哈希算法对分区数取模。
如果不存在分区也不存在key,则会使用粘性分区策略.Sticky Partitioner
(2)RoundRobinPartitioner:
将消息平均分配到每个分区中,不受性能影响。与key无关。
(3)UniformStickyPartitioner:
他跟DefaultPartitioner 分区策略的唯一区别就是。
DefaultPartitionerd 如果有key的话,那么它是按照key来决定分区的,这个时候并不会使用粘性分区 UniformStickyPartitioner 是不管你有没有key, 统一都用粘性分区来分配。
其优缺点等同于无key条件下的DefaultPartitioner.
3.日志分段 LogSegment
也就是定义的一个日志分段(log、index等文件的集合):
class LogSegment private[log] (val log: FileRecords, // log 文件
val lazyOffsetIndex: LazyIndex[OffsetIndex],// index 文件
val lazyTimeIndex: LazyIndex[TimeIndex],// timeindex 文件
val txnIndex: TransactionIndex,
val baseOffset: Long,// 当前分段文件中第一条消息的 offset 值
val indexIntervalBytes: Int,// 索引项之间间隔的最小字节数,对应 index.interval.bytes 配置
val rollJitterMs: Long,
val time: Time // log.segment.ms,以时间为维度切分segment
) extends Logging {
// 判断是否需要roll一个新的segments段
def shouldRoll(rollParams: RollParams): Boolean = {}
}
写入日志:
不管是主副本处理 Produce 请求还是从副本处理 Fetch 请求,消息记录最终都会调用 Log.append() 将消息记录写到本地的日志系统中。重点关注主从副本 HW、LEO 等字段的各自的写入,以及整个日志文件的格式。
会根据index.interval.bytes配置记录索引。
ReplicaManager.appendToLocalLog() => 追加消息到本地log
Partition.appendRecordsToLeader() =>
Log.append() => 消息写入本地log
LogSegment.append() => 写入分段日志
FileRecords.append() => 文件写入
def append(largestOffset: Long, //待写入消息的最大位移值
largestTimestamp: Long, //待写入消息的最大时间戳
shallowOffsetOfMaxTimestamp: Long, //最大时间戳对应消息的位移
records: MemoryRecords): Unit = {
if (records.sizeInBytes > 0) {
trace(s"Inserting ${records.sizeInBytes} bytes at end offset $largestOffset at position ${log.sizeInBytes} " +
s"with largest timestamp $largestTimestamp at shallow offset $shallowOffsetOfMaxTimestamp")
val physicalPosition = log.sizeInBytes()
if (physicalPosition == 0)
rollingBasedTimestamp = Some(largestTimestamp)
ensureOffsetInRange(largestOffset)
// 调用 FileRecords.append() 执行写入,写入os的页缓存
val appendedBytes = log.append(records)
trace(s"Appended $appendedBytes to ${log.file} at end offset $largestOffset")
// 更新最大时间戳和所属消息的位移,消息老化机制使用
if (largestTimestamp > maxTimestampSoFar) {
maxTimestampSoFar = largestTimestamp
offsetOfMaxTimestampSoFar = shallowOffsetOfMaxTimestamp
}
// 更新index等文件,根据index.interval.bytes配置的稀疏索引之间的字节数,默认每4k写入就创建一个索引
if (bytesSinceLastIndexEntry > indexIntervalBytes) {
offsetIndex.append(largestOffset, physicalPosition)
timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestampSoFar)
bytesSinceLastIndexEntry = 0
}
bytesSinceLastIndexEntry += records.sizeInBytes
}
}
读取日志:
读取日志需要先从index文件获取 小于offset 所在最大的一个索引 offset作为查询起始点(使用二分查找)再去 log文件上查找。最终是调用 FileRecords.slice(startPosition, fetchSize) 读取文件。
另外缓存文件的读写是使用MappedByteBuffer,底层是使用了页缓存PageCache,而操作系统使用LRU机制管理页缓存,同时由于Kafka写入索引文件是文件末尾追加写入,因此几乎所有索引查询都集中在尾部,如果在文件全文进行二分查找可能会碰到不在页缓存中的索引,导致缺页中断,阻塞等待从磁盘加载没有被缓存到page cache的数据。Kafka通过对索引文件进行冷热分区,在AbstractIndex类中定义热区的分界线warmEntries值是8192,标识索引文件末尾的8KB为热区,保证查询最热那部分数据所遍历的Page永远是固定的。
ReplicaManager.readFromLog() => 从log读取
ReplicaManager.readFromLocalLog() => 从本地log读取
Partition.readRecords() => 读取当前主副本中未同步,根据 fetchOffset
Log.read() => 读取log
LogSegment.read() => 读取log
LogSegment.translateOffset() => 根据 offset 确定起始查询位置
FileRecords.slice() => 读取日志
def read(startOffset: Long, //要读取的第一条消息的位移
maxSize: Int, //能读取的最大字节数
maxPosition: Long = size, //能读到的最大文件位置
minOneMessage: Boolean = false //是否允许在消息体过大时至少返回第一条消息
): FetchDataInfo = {
if (maxSize < 0)
throw new IllegalArgumentException(s"Invalid max size $maxSize for log read from segment $log")
// 先更新起始消息的offset,从index中获取要开始查找的起始索引
// 二分查找,查找小于等于 offset 的最大索引,将这个位置作为查询起始位置
// 最终调用的 indexSlotRangeFor() 定义的 binarySearch() 二分查找
val startOffsetAndSize = translateOffset(startOffset)
// if the start position is already off the end of the log, return null
if (startOffsetAndSize == null)
return null
val startPosition = startOffsetAndSize.position
val offsetMetadata = LogOffsetMetadata(startOffset, this.baseOffset, startPosition)
val adjustedMaxSize =
if (minOneMessage) math.max(maxSize, startOffsetAndSize.size)
else maxSize
if (adjustedMaxSize == 0)
return FetchDataInfo(offsetMetadata, MemoryRecords.EMPTY)
// 计算fetch的最大size
val fetchSize: Int = min((maxPosition - startPosition).toInt, adjustedMaxSize)
// 从指定位置读取指定大小的消息集合 log.slice(startPosition, fetchSize)
FetchDataInfo(offsetMetadata, log.slice(startPosition, fetchSize), firstEntryIncomplete = adjustedMaxSize < startOffsetAndSize.size)
}
删除日志:
需要定时删除过期、过大的的日志,主要依靠kafka-delete-logs定时任务来驱动,根据条件判断是否需要删除并进行标记,最后将标记了删除的日志进行删除。
def deleteIfExists(): Unit = {}
判断roll分段:
在消息写入前会判断是否需要另起一个 LogSegmet。两个维度判断一是判断当前 activeSegment 在追加本次消息之后,长度是否超过 LogSegment 允许的最大值,二是判断当前 activeSegment 的存活时间超过了允许的最大时间。
Log.append() => 消息写入本地log
Log.maybeRoll() => 判断是否需要新建 LogSegment。
LogSegment.shouldRoll() => 计算消息追加后是否超过尺寸、计算是否还应该存活,综合是否pan'duan应该新建 LogSegmet。
LogSegment.timeWaitedForRoll() => 计算当前 activeSegment 是否超过 segments.ms 设置的存活时间,从第一条消息获取时间戳。
AbstractIndex.canAppendOffset() => 判断还能否在 inedx 中创建索引,也就是除了 log 能写入之外,还应该判断 index 文件能否写入。
def shouldRoll(rollParams: RollParams): Boolean = {
val reachedRollMs = timeWaitedForRoll(rollParams.now, rollParams.maxTimestampInMessages) > rollParams.maxSegmentMs - rollJitterMs
size > rollParams.maxSegmentBytes - rollParams.messagesSize ||
(size > 0 && reachedRollMs) ||
offsetIndex.isFull || timeIndex.isFull || !canConvertToRelativeOffset(rollParams.maxOffsetInMessages)
}
def timeWaitedForRoll(now: Long, messageTimestamp: Long) : Long = {
// 加载第一条消息的时间戳
loadFirstBatchTimestamp()
rollingBasedTimestamp match {
case Some(t) if t >= 0 => messageTimestamp - t
case _ => now - created
}
}
def canConvertToRelativeOffset(offset: Long): Boolean = {
offsetIndex.canAppendOffset(offset)
}
恢复日志:
在 broker 启动后会加载 LogSegment 信息到内存。从磁盘上加载所有日志段信息到内存中,并创建相应的 LogSegment 对象实例。会遍历每个Logsegment中的所有消息,缓存最大消息offset和position、重建index索引文件、缓存总字节数等信息。
最后会根据 epoch 执行日志截断,将当前节点所在的副本包目前集群的主副本保持一致。
Log.loadSegments() => 节点启动 locally 执行
Log.recoverLog() => 日志恢复入口,遍历每个日志段
Log.loadSegmentFiles() => 恢复段
Log.recoverSegment() => 恢复段
LogSegment.recover() => 日志段恢复
def recover(producerStateManager: ProducerStateManager, leaderEpochCache: Option[LeaderEpochFileCache] = None): Int = {
offsetIndex.reset()
timeIndex.reset()
txnIndex.reset()
var validBytes = 0
var lastIndexEntry = 0
maxTimestampSoFar = RecordBatch.NO_TIMESTAMP
try {
// 遍历每个log中的每个batches
for (batch <- log.batches.asScala) {
// 该集合中的消息必须要符合 Kafka 定义的二进制格式
// 消息头字段大小必须保证 >= 14
batch.ensureValid()
ensureOffsetInRange(batch.lastOffset)
// The max timestamp is exposed at the batch level, so no need to iterate the records
// 更新当前的最大时间戳以及所属消息的位移值
if (batch.maxTimestamp > maxTimestampSoFar) {
maxTimestampSoFar = batch.maxTimestamp
offsetOfMaxTimestampSoFar = batch.lastOffset
}
// Build offset index
// 建立索引项,如果当前的所有读取的byte 减去 上次建索引文件的bytes 大于 indexIntervalBytes(默认配置),建立新索引
if (validBytes - lastIndexEntry > indexIntervalBytes) {
offsetIndex.append(batch.lastOffset, validBytes)
timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestampSoFar)
lastIndexEntry = validBytes
}
validBytes += batch.sizeInBytes()
// 1. magic是消息格式版本,总有3个版本。V2是最新版本
// 2. leader epoch是controller分配给分区leader副本的版本号。
// 每个消息批次都要有对应的leader epoch。Kafka会记录每个分区leader不同epoch对应的首条消息的位移。
// 比如leader epoch=0时producer写入了100条消息,那么cache会记录<0, 0>,之后leader变更,epoch增加到1,之后producer又写入了200条消息,那么cache会记录<1, 100>。
// 而从副本恢复过程就根据epoch来做日志截断,单纯参考HW值可能因为HW的延迟更新造成截断后丢失数据
if (batch.magic >= RecordBatch.MAGIC_VALUE_V2) {
leaderEpochCache.foreach { cache =>
if (batch.partitionLeaderEpoch >= 0 && cache.latestEpoch.forall(batch.partitionLeaderEpoch > _))
cache.assign(batch.partitionLeaderEpoch, batch.baseOffset)
}
updateProducerState(producerStateManager, batch)
}
}
} catch {
case e@ (_: CorruptRecordException | _: InvalidRecordException) =>
warn("Found invalid messages in log segment %s at byte offset %d: %s. %s"
.format(log.file.getAbsolutePath, validBytes, e.getMessage, e.getCause))
}
// Kafka 会将日志段当前总字节数(直接从.log文件获取),和刚刚累加的已读取字节数进行比较
// 如果发现前者比后者大,说明日志段写入了一些非法消息,需要执行截断操作
// 日志文件写入了消息的部分字节然后broker宕机。磁盘是块设备,它可不能保证消息的全部字节要么全部写入,要么全都不写入。
// 因此Kafka必须有机制应对这种情况,即校验+truncate。
// ps:truncate是强行截取log文件的指定大小,从末尾处直接截取。如果是因为网络抖动导致中间某些字节丢失或写入错误字节,
// 会出现消息集合CRC校验值发生变更,这个检查不是在log这一层级执行的,而是在底层的消息集合或消息批次这个level执行的。
val truncated = log.sizeInBytes - validBytes
if (truncated > 0)
debug(s"Truncated $truncated invalid bytes at the end of segment ${log.file.getAbsoluteFile} during recovery")
log.truncateTo(validBytes)
offsetIndex.trimToValidSize()
// A normally closed segment always appends the biggest timestamp ever seen into log segment, we do this as well.
timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestampSoFar, skipFullCheck = true)
timeIndex.trimToValidSize()
truncated
}
日志截断:
日志截断主要发生在节点恢复时、或者定时任务,需要将当前节点的数据和当前集群的主副本保持一致,将自己的本地日志裁剪成与 Leader 一模一样的消息序列。
从一定的 offset 处进行截断删除这个 offset 之前的日志数据。截断需要找一个参考 offset,老版根据hw值有截断造成数据丢失问题,新版引入 epoch 机制,根据集群 epoch 值做参考 offset。
(1)恢复期间的日志截断:
Log.loadSegments() => 节点启动 locally 执行
Log.recoverLog() => 日志恢复入口,遍历每个日志段
LogSegment.truncateTo() => 段日志截断入口,根据传入 offset 对一系列文件做截断。
(2)定时任务的日志截断:
ReplicaFetcherThread.truncate()
Partition.truncateTo() =>
LogManager.truncateTo() =>
Log.truncateTo() =>
LogSegment.truncateTo() => 段日志截断入口,根据传入 offset 对一系列文件做截断。
def truncateTo(offset: Long): Int = {
val mapping = translateOffset(offset)
offsetIndex.truncateTo(offset)
timeIndex.truncateTo(offset)
txnIndex.truncateTo(offset)
// After truncation, reset and allocate more space for the (new currently active) index
offsetIndex.resize(offsetIndex.maxIndexSize)
timeIndex.resize(timeIndex.maxIndexSize)
val bytesTruncated = if (mapping == null) 0 else log.truncateTo(mapping.position)
if (log.sizeInBytes == 0) {
created = time.milliseconds
rollingBasedTimestamp = None
}
bytesSinceLastIndexEntry = 0
if (maxTimestampSoFar >= 0)
loadLargestTimestamp()
bytesTruncated
}
日志刷新:
系统会定时执行日志刷新任务,也就是os缓存异步刷盘
LogManager.flushDirtyLogs() => 定时任务执行刷盘
Log.flush() => 遍历当前节点所有 log 文件,调用刷新
LogSegment.flush() => 刷新所有文件,包括 log、index、timeIndex、txnIndex
def flush(): Unit = {
LogFlushStats.logFlushTimer.time {
log.flush()
offsetIndex.flush()
timeIndex.flush()
txnIndex.flush()
}
}
4.日志log文件 FileRecords
里面记录的消息,读取和写入最终都是在这个文件里面执行,也就是每个 LogSegment 下面的 .log 文件。
public class FileRecords extends AbstractRecords implements Closeable {
private final boolean isSlice;//标识是否为日志文件分片
private final int start;
private final int end;
private final Iterable<FileLogInputStream.FileChannelRecordBatch> batches;
private final AtomicInteger size;//如果是分片则表示分片的大小(end - start),如果不是分片则表示整个日志文件的大小
private final FileChannel channel;//读写对应的日志文件的通道
private volatile File file;//File file
}
写入日志:
LogSegment.append() => 写入分段日志
FileRecords.append() => 文件写入
MemoryRecords.writeFullyTo() => 文件写入
public int append(MemoryRecords records) throws IOException {
if (records.sizeInBytes() > Integer.MAX_VALUE - size.get())
throw new IllegalArgumentException("Append of size " + records.sizeInBytes() +
" bytes is too large for segment with current file position at " + size.get());
int written = records.writeFullyTo(channel);
size.getAndAdd(written);
return written;
}
读取日志:
LogSegment.read() => 读取log
FileRecords.slice() => 读取日志
FileRecords.batchesFrom() => 读取文件,根据 start、end 字节
FileRecords.batchIterator() => 读取文件,构造 FileLogInputStream()
public FileRecords slice(int position, int size) throws IOException {
int availableBytes = availableBytes(position, size);
int startPosition = this.start + position;
return new FileRecords(file, channel, startPosition, startPosition + availableBytes, true);
}
日志截断:
LogSegment.truncateTo() => 根据 offset 计算到目标的字节位置
FileRecords.truncateTo() => 根据字节位置进行文件截断
public int truncateTo(int targetSize) throws IOException {
int originalSize = sizeInBytes();
if (targetSize > originalSize || targetSize < 0)
throw new KafkaException("Attempt to truncate log segment " + file + " to " + targetSize + " bytes failed, " +
" size of this log segment is " + originalSize + " bytes.");
if (targetSize < (int) channel.size()) {
channel.truncate(targetSize);
size.set(targetSize);
}
return originalSize - targetSize;
}