Log 的常见操作分为 4 大部分。
1. 高水位管理操作:高水位的概念在 Kafka 中举足轻重,对它的管理,是 Log 最重要的功能之一。
2. 日志段管理:Log 是日志段的容器。高效组织与管理其下辖的所有日志段对象,是源码要解决的核心问题。
3. 关键位移值管理:日志定义了很多重要的位移值,比如 Log Start Offset 和 LEO 等。确保这些位移值的正确性,是构建消息引擎一致性的基础。
4. 读写操作:所谓的操作日志,大体上就是指读写日志。
LogOffsetMetadata对象
@volatile private var highWatermarkMetadata: LogOffsetMetadata = LogOffsetMetadata(logStartOffset)
高水位的起始值是Log Start Offset的值,每个 Log 对象都会维护一个 Log Start Offset 值,当首次构建高水位时,它会被赋值成 Log Start Offset 值。先看看LogOffsetMetadata是什么对象:
case class LogOffsetMetadata(messageOffset: Long,
segmentBaseOffset: Long = Log.UnknownOffset, relativePositionInSegment: Int = LogOffsetMetadata.UnknownFilePosition)
messageOffset:消息位移值。
segmentBaseOffset:保存该位移值所在日志段的起始位移。日志段起始位移值辅助计算两条消息在物理磁盘文件中位置的差值,即两条消息彼此隔了多少字节。这个计算有个前提条件,即两条消息必须处在同一个日志段对象上,不能跨日志段对象。否则它们就位于不同的物理文件上,计算这个值就没有意义了。这里的 segmentBaseOffset,就是用来判断两条消息是否处于同一个日志段的。
relativePositionSegment:保存该位移值所在日志段的物理磁盘位置。这个字段在计算两个位移值之间的物理磁盘位置差值时非常有用。在读取日志的时候需要计算位置之间的字节数,假设每次读取时只能读 1MB 的数据,那么,源码肯定需要关心两个位移之间所有消息的总字节数是否超过了 1MB。
LogOffsetMetadata的定义如下:
case class LogOffsetMetadata(messageOffset: Long,
segmentBaseOffset: Long = Log.UnknownOffset,
relativePositionInSegment: Int = LogOffsetMetadata.UnknownFilePosition) {
// check if this offset is already on an older segment compared with the given offset
def onOlderSegment(that: LogOffsetMetadata): Boolean = {
if (messageOffsetOnly)
throw new KafkaException(s"$this cannot compare its segment info with $that since it only has message offset info")
this.segmentBaseOffset < that.segmentBaseOffset
}
// check if this offset is on the same segment with the given offset
// 这个方法就是用来判断给定的两个 LogOffsetMetadata 对象是否处于同一个日志段的。
// 判断方法很简单,就是比较两个 LogOffsetMetadata 对象的 segmentBaseOffset 值是否相等。
def onSameSegment(that: LogOffsetMetadata): Boolean = {
if (messageOffsetOnly)
throw new KafkaException(s"$this cannot compare its segment info with $that since it only has message offset info")
this.segmentBaseOffset == that.segmentBaseOffset
}
// compute the number of messages between this offset to the given offset
def offsetDiff(that: LogOffsetMetadata): Long = {
this.messageOffset - that.messageOffset
}
// compute the number of bytes between this offset to the given offset
// if they are on the same segment and this offset precedes the given offset
// 计算两个offset的物理位置差值
def positionDiff(that: LogOffsetMetadata): Int = {
if(!onSameSegment(that))
throw new KafkaException(s"$this cannot compare its segment position with $that since they are not on the same segment")
if(messageOffsetOnly)
throw new KafkaException(s"$this cannot compare its segment position with $that since it only has message offset info")
this.relativePositionInSegment - that.relativePositionInSegment
}
// decide if the offset metadata only contains message offset info
def messageOffsetOnly: Boolean = {
segmentBaseOffset == Log.UnknownOffset && relativePositionInSegment == LogOffsetMetadata.UnknownFilePosition
}
override def toString = s"(offset=$messageOffset segment=[$segmentBaseOffset:$relativePositionInSegment])"
}
高水位管理操作
A. 获取和设置高水位值
// getter method:读取高水位的位移值
def highWatermark: Long = highWatermarkMetadata.messageOffset
private def updateHighWatermarkMetadata(newHighWatermark: LogOffsetMetadata): Unit = {
if (newHighWatermark.messageOffset < 0)
throw new IllegalArgumentException("High watermark offset should be non-negative")
lock synchronized { // 保护Log对象修改的Monitor锁
// 赋值新的高水位值
highWatermarkMetadata = newHighWatermark
// 处理事务状态管理器的高水位值更新逻辑
producerStateManager.onHighWatermarkUpdated(newHighWatermark.messageOffset)
// First Unstable Offset是Kafka事务机制的一部分
maybeIncrementFirstUnstableOffset()
}
trace(s"Setting high watermark $newHighWatermark")
}
B. 更新高水位值
源码定义了两个更新高水位值的方法:updateHighWatermark 和 maybeIncrementHighWatermark。从名字上来看,前者是一定 要更新高水位值的,而后者是可能会更新也可能不会。
def updateHighWatermark(hw: Long): Long = {
// 新高水位值一定介于[Log Start Offset,Log End Offset]之间
val newHighWatermark = if (hw < logStartOffset)
logStartOffset
else if (hw > logEndOffset)
logEndOffset
else
hw
// 调用Setter方法来更新高水位值
updateHighWatermarkMetadata(LogOffsetMetadata(newHighWatermark))
newHighWatermark
}
def maybeIncrementHighWatermark(newHighWatermark: LogOffsetMetadata, isClusterBackup:Boolean = false): Option[LogOffsetMetadata] = {
// 新高水位值不能越过Log End Offset
if (newHighWatermark.messageOffset > logEndOffset)
throw new IllegalArgumentException(s"High watermark $newHighWatermark update exceeds current " +
s"log end offset $logEndOffsetMetadata")
lock.synchronized {
// 获取老的高水位值
// fetchHighWatermarkMetadata如果没有就直接从log里面存的hw取到offsetMetadata
val oldHighWatermark = fetchHighWatermarkMetadata
// Ensure that the high watermark increases monotonically. We also update the high watermark when the new
// offset metadata is on a newer segment, which occurs whenever the log is rolled to a new segment.
// 新高水位值要比老高水位值大以维持单调增加特性,否则就不做更新!
// 另外,如果新高水位值在新日志段上,也可执行更新高水位操作
if (oldHighWatermark.messageOffset < newHighWatermark.messageOffset ||
(oldHighWatermark.messageOffset == newHighWatermark.messageOffset && oldHighWatermark.onOlderSegment(newHighWatermark) ||
(isClusterBackup && oldHighWatermark.messageOffset > newHighWatermark.messageOffset && newHighWatermark.messageOffset >= 0))) {
updateHighWatermarkMetadata(newHighWatermark)
// 返回老的高水位值
Some(oldHighWatermark)
} else {
None
}
}
}
updateHighWatermark 方法,主要用在 Follower 副本从 Leader 副本获取到消息后更新高水位值。一旦拿到新的消息,就必须要更新高水位值;而 maybeIncrementHighWatermark 方法,主要是用来更新 Leader 副本的高水位值。需要注意的是,Leader 副本高水位值的更新是有条件的——某些情况下会更新高水位值,某些情况下可能不会。
Follower 副本成功拉取 Leader 副本的消息后必须更新高水位值,但 Producer 端向 Leader 副本写入消息时,分区的高水位值就可能不需要更新——因为它可能需要等待其他 Follower 副本同步的进度。
C. 读取高水位值
关于高水位值管理的最后一个操作是 fetchHighWatermarkMetadata 方法。它不仅仅是获取高水位值,还要获取高水位的其他元数据信息,即日志段起始位移和物理位置信息。下面是它的实现逻辑:
private def fetchHighWatermarkMetadata: LogOffsetMetadata = {
checkIfMemoryMappedBufferClosed() // 读取时确保日志不能被关闭
val offsetMetadata = highWatermarkMetadata // 保存当前高水位值到本地变量,避免多线程访问干扰
if (offsetMetadata.messageOffsetOnly) { //没有获得到完整的高水位元数据
lock.synchronized {
// 通过读日志文件的方式把完整的高水位元数据信息拉出来
val fullOffset = convertToOffsetMetadataOrThrow(highWatermark)
// 然后再更新一下高水位对象
updateHighWatermarkMetadata(fullOffset)
fullOffset
}
} else {// 否则,直接返回即可
offsetMetadata
}
}