先看下索引的类图设计:
![](https://i-blog.csdnimg.cn/blog_migrate/a899cdec13134bfc153e19b1c245df50.png)
abstract class AbstractIndex(@volatile private var _file: File, val baseOffset: Long, val maxIndexSize: Int = -1,
val writable: Boolean) extends Closeable {
import AbstractIndex._
// Length of the index file
@volatile
private var _length: Long = _
protected def entrySize: Int
protected def _warmEntries: Int = 8192 / entrySize
protected val lock = new ReentrantLock
}
entrySize 代表的是该索引条目所需要的字节数. 比如在 OffsetIndex 中,entrySize 占 8 字节,而 TimeIndex 中,占 12 字节. 但是 AbstractIndex 中,baseOffset 占用 8 字节,所以说,在索引文件中,存储的都是相对字节数.
_warmEntries 冷热数据分割的地方.
对于索引,在 Kafka 中,采用了 mmap(内存映射),加快读写.
补充点知识:
普通 java IO 读取磁盘上的文件,通常来说需要 3 次拷贝:
1.从磁盘拷贝到内核缓冲区
2.从内核缓冲区到JVM进程的直接缓冲区
3.从 JVM 进程的直接缓冲区到堆内存的拷贝
![](https://i-blog.csdnimg.cn/blog_migrate/9ddd2603c7fed39a845b0423dad4b580.jpeg)
什么是零拷贝?
零拷贝指的是用户态和内核态的拷贝次数为 0 !!!
传统方式涉及的上下文切换:
1.read 导致用户态到内核态的切换,数据通过 DMA 从磁盘拷贝到内核缓冲区.
2.数据从内核缓冲区拷贝到用户态,状态从内核态切换到用户态.
3.从用户态切换到内核态,数据从用户态拷贝内核缓冲区.
4.DMA 将数据从内核态拷贝到网卡,返回到用户态.
也就是说会有四次状态切换和四次拷贝.
零拷贝的原理:
1.transferTo 方法触发 DMA 将数据拷贝到内核缓冲区.
2.DMA 将数据从内核缓冲区拷贝到网卡.
但是上下文的切换还是有 2 次.
堆外内存属于用户态缓冲区,切记!!!
所以对于 Java 程序而言(
使用堆内开辟的空间),其实拷贝了 3 次:
1.DMA 从磁盘读取数据到内核缓冲区
2.内核缓冲区到堆外内存
3.堆外内存到堆内内存
再回到 Kafka AbstractIndex 中来:
mmap 代表的是一个内存映射文件,底层是 MappedByteBuffer.
关于 AbstractIndex 文件中,最最核心的就是那个二分法查找.
Kafka 中对于 entry 分为了两部分数据,一部分是冷数据部分,另一部分是热数据部分.
但是,如果新增了一页,热数据部分会更新吗?
其实应该不存在这个问题的,因为每次都是查询的最后几页的数据.
val firstHotEntry = Math.max(0, _entries - 1 - _warmEntries)
再回到 OffsetIndex.
private def relativeOffset(buffer: ByteBuffer, n: Int): Int = buffer.getInt(n * entrySize)
private def physical(buffer: ByteBuffer, n: Int): Int = buffer.getInt(n * entrySize + 4)
这两个方法看的一脸蒙圈,为什么是这样的了?那么首先回到 OffsetIndex 的存储结构.
![](https://i-blog.csdnimg.cn/blog_migrate/76642aaadb877778772147b83d407b6e.png)
对于 OffsetIndex 而言,每个 entry 占用 8 字节,前四字节是相对位移,后四字节是该条消息的实际物理地址.
所以,这也就是 OffsetPosition 的物理结构.
case class OffsetPosition(offset: Long, position: Int) extends IndexEntry {
override def indexKey = offset
override def indexValue = position.toLong
}
对于 OffsetIndex 而言,做了一些优化,每个 OffsetIndex 在创建的时候,都已经保存了 baseOffset. 所以 key 存储的是相对位移.
indexValue 存储的是该条记录在文件中的物理地址.
indexKey 存储的是该条记录实际的偏移量.
再看下 TimeIndex.
TimeIndex 占用 12 字节,前 8 字节存储时间戳,后 4 字节存储位置.
基于上面的结论,就知道为什么 TimeIndex 返回的是这个结构了.
override def parseEntry(buffer: ByteBuffer, n: Int): TimestampOffset = {
TimestampOffset(timestamp(buffer, n), baseOffset + relativeOffset(buffer, n))
}
参考: