Kafka 索引设计

先看下索引的类图设计:
 
 
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 进程的直接缓冲区到堆内存的拷贝
 
 
什么是零拷贝?
 
零拷贝指的是用户态和内核态的拷贝次数为 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 的存储结构.
 
 
对于 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))
}
 
 
参考:
 
 
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值