kafka 索引项设计

284 阅读2分钟

这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战

首先 kafka 从本质来说,是一个存储日志的工具。因为先是有存储能力,然后在这个基础上赋予消息队列的功能。而存储功能,在 kafka 主要的功能是查找 log ,因为不同的 consumer 开始消费的位置都不一样,既然存储下来,就整体查询 log,从指定的地方开始消费。

存储中,kafka 采用 index+log+稀疏索引 的结构:

  1. OffsetIndex → <logOffset, physics position>
  2. TimeIndex → <timestamp, offset>
  3. 稀疏索引 → 新写入 4KB 数据新增一条 index record

和索引有关的代码:

  1. AbstractIndex ⇒ 最顶层的抽象类,封装了所有的索引的公共操作
  2. OffsetIndex ⇒ 位移索引
  3. TimeIndex ⇒ 时间戳索引
  4. TransactionIndex ⇒ 事务索引

先说 AbstractIndex 结构**:**

abstract class AbstractIndex(
	@volatile var file: File, 
	val baseOffset: Long, 
	val maxIndexSize: Int = -1, 
	val writable: Boolean) extends Closeable {
    ......
}
  1. file ⇒ 对应磁盘文件中的一个 index file
  2. baseOffset ⇒ 索引对象对应日志段对象的起始位移值。其实就是 index file name 对应的数字
  3. maxIndexSize ⇒ 磁盘索引文件的最大长度。默认情况,10MB
  4. writable ⇒ 打开方式:读写/只读

说完基本结构,那 kafka 中的索引底层的实现原理是什么?

内存映射文件,即 Java 中的 MappedByteBuffer。

好处:

  1. 对于索引这种小文件。使用内存映射文件的速度快于普通文件读写
  2. 同时在 Linux 中,内存映射区域实际上是 page cache

AbstractIndex 中,mmap 变量就是这个 MappedByteBuffer 类型。先来看看他的使用:

/** The maximum number of entries this index can hold */
@volatile
private[this] var _maxEntries: Int = mmap.limit() / entrySize

/** The number of entries in this index */
@volatile
protected var _entries: Int = mmap.position() / entrySize

def isFull: Boolean = _entries >= _maxEntries
  • _entries ⇒ 当前索引对象有多少个索引项
  • _maxEntries ⇒ 当前索引对象最多能容纳多少个索引项
  • 判断当前索引文件是否写满

所以继承类控制 index entry 的主要逻辑,就是向 mmap 中添加对应的字段即可。

write index entry

向索引文件中写入新的索引项 ⇒ OffsetIndex 中的 append():

def append(offset: Long, position: Int): Unit = {
  inLock(lock) {
    // 1:判断索引文件未写满
    require(!isFull, "Attempt to append to a full index (size = " + _entries + ").")
    // 2:满足一个条件即可写入索引项
    // _1. 当前索引文件为空
    // _2. 写入的offset > 上一个写入索引项
    if (_entries == 0 || offset > _lastOffset) {
      trace(s"Adding index entry $offset => $position to ${file.getAbsolutePath}")
			// 3-1:向mmap中写入相对位移值
      mmap.putInt(relativeOffset(offset))
			// 3-2:向mmap中写入物理位置信息
      mmap.putInt(position) 
      // 4:更新元数据统计信息,当前索引项计数器: _entries; 当前索引项最新位移值: _lastOffset
      _entries += 1
      _lastOffset = offset
      // 5:执行校验
      require(_entries * entrySize == mmap.position(), entries + " entries but file position in index is " + mmap.position() + ".")
    } else {
      throw new InvalidOffsetException(s"Attempt to append an offset ($offset) to position $entries no larger than" +
        s" the last offset appended (${_lastOffset}) to ${file.getAbsolutePath}.")
    }
  }
}

其实写入就是:

  1. 判断 offset 是否允许写入
  2. mmap 写入相对位移值以及物理位置信息