在Kafka中使用FileMessageSet管理日志文件,它对应着磁盘上一个真正的日志文件。FileMessageSet继承了MessaeSet抽象类,MessageSet。保存的数据格式分为三部分:8字节的ofset和4字节的size以及size子集的message 数据,前两个部分被称为LogOverhead。
Kafka使用Message表示消息,Message使用ByteBuffer保存数据,其格式以及各个部分的含义:
CRC32: 消息的校验码,4个字节
magic: 魔数标识,与消息格式有关,取值0或者1.magic为0的时候,消息的offset使用绝对offset且消息格式没有timestamp部分;当magic为1的时候,消息的offset使用相对的offset且消息格式存在timestamp部分,所以magic不同消息长度也不一样。
attributes: 消息的属性,1个字节。其中0-2表示消息使用的压缩类型:0表示为无压缩,1表示gzip压缩,2表示snappy压缩
3表示时间戳类型:0表示创建时间,1表示追加时间
timestamp: 时间戳,其含义由attribute的第三位确定
key length: 消息 key的长度
key: 消息的key
value length: 消息value的长度
value: 消息的value
MessageSet定义了两个比较关键的方法:
writeTo: 将当前消息写入channel
iterator: 顺序读取MessageSet的消息
这两个方法说明了MessageSet有顺序读写的消息特性。
一 FileMessageSet 核心字段
file: 指向磁盘上日志文件
channel:FileChannel类型,用于读写对应的日志文件
start:FileMessageSet对象除了表示一个完整的日志文件,还可以表示日志文件的分片,start表示分片的开始位置
end:表示分片的结束位置
isSlice:表示当前FileMessageSet是否为日志文件的分片
_size:FileMessageSet大小,单位是字节,如果是分片则表示分片大小
二 FileMessageSet核心的方法
2.1 openChannel
打开给定文件的通道,对于Windows NTFS和一些老版本的Linux文件系统,设置preallocate(预分配)为true,我们也可以通过在构造FileMessageSet的时候,指定该文件是否只读,如果mutable为ture,表示可读可写
def openChannel(file: File, mutable: Boolean, fileAlreadyExists: Boolean = false, initFileSize: Int = 0, preallocate: Boolean = false): FileChannel = {
if (mutable) {// 判断文件是否可写
if (fileAlreadyExists) // 如果文件存在,打开可写通道
new RandomAccessFile(file, "rw").getChannel()
else {
if (preallocate) {// 不能存在且预分配了,初始化文件大小为默认大小为512 * 1025 *1024
val randomAccessFile = new RandomAccessFile(file, "rw")
randomAccessFile.setLength(initFileSize)
randomAccessFile.getChannel()//打开可写通道
}
else // 如果没有开启预分配,则直接打开可写通道
new RandomAccessFile(file, "rw").getChannel()
}
}
else // 如果文件不可写,创建只读的FileChannel
new FileInputStream(file).getChannel()
}
在FileMessageSet初始化的过程中,会移动FileChannel的position指针,这是为了实现每次写入的消息都在日志文件的尾部,从而避免重启服务后的写入操作覆盖之前的操作。对于新创建的且进行了预分配的日志文件,他的end会出事为0,所以也是从起始写入数据的
2.2 append 添加message,进行文件的写入
首先使用ByteBufferMessageSet#writeFullyTo进行消息的添加,添加完后改变FileMessageSet的大小
def append(messages: ByteBufferMessageSet) {
val written = messages.writeFullyTo(channel)
_size.getAndAdd(written)// 改变FileMessageSet的大小
}
def writeFullyTo(channel: GatheringByteChannel): Int = {
buffer.mark()
var written = 0
while (written < sizeInBytes)
written += channel.write(buffer)
buffer.reset()
written
}
2.3查找指定的消息
根据offset查找:
根据给定的offset查找文件中大于或者等于这个给定offset的最后一个offset的实际物理位置和消息大小
比如给定offset为10,文件在offset为10的后面还有3条消息,offset是16,45,100,那么我们要找的就是100这个最后的offset对应的消息
实际物理位置和这个消息的大小
def searchForOffsetWithSize(targetOffset: Long, startingPosition: Int): (OffsetPosition, Int) = {
var position = startingPosition // 获得起始位置
// 创建一个12字节的缓冲区,用于读取LogOverhead
val buffer = ByteBuffer.allocate(MessageSet.LogOverhead)// 获取offset和message数据的size的字节数12
val size = sizeInBytes() // 获取FileMessageSet的大小
// 从开始位置逐条遍历消息
while(position + MessageSet.LogOverhead < size) {
buffer.rewind()// 重置ByteBuffer的position指针,准备读入数据
// 读取LogOverhead。这里会确保startingPosition位于一个消息的开头,否则读取到的并不是LogOverhead
channel.read(buffer, position)
// 没有读取到12字节的LogOverhead抛出异常
if(buffer.hasRemaining)
throw new IllegalStateException("Failed to read complete buffer for targetOffset %d startPosition %d in %s"
.format(targetOffset, startingPosition, file.getAbsolutePath))
buffer.rewind()// 重置ByteBuffer的position指针,准备从ByteBuffer读取数据
val offset = buffer.getLong()// 获取消息的offset 8个字节
val messageSize = buffer.getInt()// 获取消息的大小 4个字节
if (messageSize < Message.MinMessageOverhead)
throw new IllegalStateException("Invalid message size: " + messageSize)
if (offset >= targetOffset)// 如果offset 大于如果给定的offset
// 将offset和它对应position封装成OffsetPosition对象返回
return (OffsetPosition(offset, position), messageSize + MessageSet.LogOverhead)
// 移动position准备读取下一个消息
position += MessageSet.LogOverhead + messageSize
}
null // 找不到offset大于等于给定的offset的消息
}
根据时间查找:查找消息的时间大于或者等于目标时间的消息,即根据时间查找消息
def searchForTimestamp(targetTimestamp: Long, startingPosition: Int): Option[TimestampOffset] = {
// 返回一个从startingPosition读取,读取sizeInBytes大小的数据的FileMessageSet
val messagesToSearch = read(startingPosition, sizeInBytes)
// 开始遍历消息(因为事先了iterator),所以可以迭代
for (messageAndOffset <- messagesToSearch) {
// 获取message
val message = messageAndOffset.message
// 如果消息时间戳大于或者等于,目标时间戳,表示找打一个消息
if (message.timestamp >= targetTimestamp) {
message.compressionCodec match {
case NoCompressionCodec =>
return Some(TimestampOffset(messageAndOffset.message.timestamp, messageAndOffset.offset))
case _ =>
// Iterate over the inner messages to get the exact offset.
for (innerMessageAndOffset <- ByteBufferMessageSet.deepIterator(messageAndOffset)) {
val timestamp = innerMessageAndOffset.message.timestamp
if (timestamp >= targetTimestamp)
return Some(TimestampOffset(innerMessageAndOffset.message.timestamp, innerMessageAndOffset.offset))
}
throw new IllegalStateException(s"The message set (max timestamp = ${message.timestamp}, max offset = ${messageAndOffset.offset}" +
s" should contain target timestamp $targetTimestamp but it does not.")
}
}
}
None
}
2.4 read 读取数据,返回一个FileMessageSet
// 返回一个给定开始位置和大小的FileMessageSet的视图,即我要从哪里开始读,读多少数据
def read(position: Int, size: Int): FileMessageSet = {
if(position < 0)
throw new IllegalArgumentException("Invalid position: " + position)
if(size < 0)
throw new IllegalArgumentException("Invalid size: " + size)
new FileMessageSet(file,
channel,
start = this.start + position,
end = {
// Handle the integer overflow
if (this.start + position + size < 0)
sizeInBytes()
else
math.min(this.start + position + size, sizeInBytes())
})
}
2.5 剪裁文件truncateTo
主要负责将日志文件剪裁截断到目标大小,副本切换的时候用得到
def truncateTo(targetSize: Int): Int = {
// 当前日志文件大小
val originalSize = sizeInBytes
// 给定的大小 > 当前日志文件大小或者给定的大小 < 0就会抛出异常
if(targetSize > originalSize || targetSize < 0)
throw new KafkaException("Attempt to truncate log segment to " + targetSize + " bytes failed, " +
" size of this log segment is " + originalSize + " bytes.")
// 开始截取,并且移动position,就相当于1000字节的文件,我把它截取到800字节的位置,其他的我不要
if (targetSize < channel.size.toInt) {
channel.truncate(targetSize)
channel.position(targetSize)
_size.set(targetSize)// 更新_size
}
originalSize - targetSize // 返回剪裁掉的字节数
}
2.6 还有个中的方法就是iterator,返回一个迭代器
在FileMessageSet读取消息的流程:
# 读取LogOverhead,然后按照size分配合适的ByteBuffer
# 再读取message data部分,最后将message data和 offset封装成MessageOffset返回