背景
前面一篇文章已经说了日志段和日志的关系,日志通过ConcurrentSkipListMap 保存所有日志段对象,他是线程安全的,健值是可排序的。今天我们来探讨下日志段管理功能。
1、增加日志
Log 类中定义了添加日志段的方法addSegment.
def addSegment(segment: LogSegment): LogSegment = this.segments.put(segment.baseOffset, segment)
通过源码可以看出,实质上就是调用ConcurrentSkipListMap 的put 方法进行操作。
2、删除日志
kafka 本身有很多日志的留存策略,包括基于时间维度的、基于空间维度的和基于Log Start Offset 维度的,根据这些规则确定哪些日志可以删除。
源码中删除的入口是deleteOldSegments():
def deleteOldSegments(): Int = {
if (config.delete) {
deleteRetentionMsBreachedSegments() + deleteRetentionSizeBreachedSegments() + deleteLogStartOffsetBreachedSegments()
} else {
deleteLogStartOffsetBreachedSegments()
}
}
deleteRetentionMsBreachedSegments、deleteRetentionSizeBreachedSegments、deleteLogStartOffsetBreachedSegments三种删除策略分别对应上面三种留存策略。
由于每种留存策略调用的删除方法是deleteOldSegments,我们先来说明下它。
private def deleteOldSegments(predicate: (LogSegment, Option[LogSegment]) => Boolean, reason: String): Int = {
lock synchronized {
val deletable = deletableSegments(predicate)
if (deletable.nonEmpty)
info(s"Found deletable segments with base offsets [${deletable.map(_.baseOffset).mkString(",")}] due to $reason")
deleteSegments(deletable)
}
}
这段代码主要是两个步骤,1、使用传入的函数计算哪些日志段对象能够被删除。2、调用deleteSegments()方法删除这些日志段。
我们首先来看下deletableSegments:
private def deletableSegments(predicate: (LogSegment, Option[LogSegment]) => Boolean): Iterable[LogSegment] = {
if (segments.isEmpty) {
Seq.empty
} else {
val deletable = ArrayBuffer.empty[LogSegment]
var segmentEntry = segments.firstEntry
while (segmentEntry != null) {
val segment = segmentEntry.getValue
val nextSegmentEntry = segments.higherEntry(segmentEntry.getKey)
val (nextSegment, upperBoundOffset, isLastSegmentAndEmpty) = if (nextSegmentEntry != null)
(nextSegmentEntry.getValue, nextSegmentEntry.getValue.baseOffset, false)
else
(null, logEndOffset, segment.size == 0)
if (highWatermark >= upperBoundOffset && predicate(segment, Option(nextSegment)) && !isLastSegmentAndEmpty) {
deletable += segment
segmentEntry = nextSegmentEntry
} else {
segmentEntry = null
}
}
deletable
}
}
1、首先第一步判断,如果没有日志对象直接返回。
2、获取日志段其实位移的第一个Entry日志段,满足下列条件就停止遍历:
(1)predicate 为false。
(2) 扫描到包含Log对象高水位值所在的日志段对象。
(3)最新的日志段对象不包含任何消息。
最新日志段对象是segments 中key 最大的对象,也就是Active segment 是不允许删除的。
满足以上几个条件的就是可以删除的日志段。
我们再来看下 deleteSegments(deletable),这是一个真正删除日志段的方法:
private def deleteSegments(deletable: Iterable[LogSegment], reason: SegmentDeletionReason): Int = {
maybeHandleIOException(s"Error while deleting segments for $topicPartition in dir ${dir.getParent}") {
val numToDelete = deletable.size
if (numToDelete > 0) {
// we must always have at least one segment, so if we are going to delete all the segments, create a new one first
// 不允许删除所有日志段对象。如果一定要做,先创建出一个新的来,然后再把前面的日志段删掉
if (segments.size == numToDelete)
roll()
lock synchronized {
//确保Log 对象没有被关闭
checkIfMemoryMappedBufferClosed()
// remove the segments for lookups
//删除给定的日志段对象以及对应的物理文件
removeAndDeleteSegments(deletable, asyncDelete = true, reason)
//尝试更新日志的Log Start Offset值
maybeIncrementLogStartOffset(segments.firstEntry.getValue.baseOffset, SegmentDeletion)
}
}
numToDelete
}
}
最后更新Log Start Offset 的原因是,对外可见的消息位移由于删除,可能发生了变化。
3、查询Segment
源码中需要查询日志段对象的地方太多了,但主要都是利用了 ConcurrentSkipListMap 的现成方法。
- segments.firstEntry:获取第一个日志段对象;
- segments.lastEntry:获取最后一个日志段对象,即 Active Segment;
- segments.higherEntry:获取第一个起始位移值≥给定 Key 值的日志段对象;
- segments.floorEntry:获取最后一个起始位移值≤给定 Key 值的日志段对象。关键位移值管理。
所以这部分的源码我们只需要了解map 接口提供的接口就行。
今天主要讲解了LogSegment 的一些管理方法,留一个思考题,文章中没有提到更新的操作,你知道是什么原因吗?欢迎留言一起讨论。