存储引擎 - bitcask

84 阅读4分钟

做了好几年的数据库运维平台开发了,虽然不是做内核开发的,但是多多少少接触了一下,流复制和灾备相关的,很多人入门应该都是流复制吧。

数据库里有很多东西可以学习。所以这里整理记录一下学习过程和内容。

数据库的核心是存储和提取数据,存储引擎是大家最关心的内容。先学习一下常见的存储引擎,不同的存储引擎都有各自的特点和侧重点。今天聊一下最简单的 bitcask

bitcask是嵌入式的存储引擎,它的构成只有2部分:

  1. 数据
  2. 索引

增删改都是采用 Append 日志记录的方式追加到数据文件末尾,这样所有的写操作都是顺序 IO。增加和更改都是一样的 Set 语义。删除的也是 Set 语义,只不过使用的是一个特殊值,一般叫做墓碑。写入数据最终落盘的格式为:

  • CRC
  • Timestamp
  • Key Size
  • Value Size
  • Key
  • Value

当文件的大小达到阈值时关闭这个文件,切换到一个新的文件上进行追加日志。所以写操作只会发生在一个数据文件上,这个数据文件被称为 Active Data File,与之对应的其它数据文件被称为 Older Data File。 即刻 这么看来,数据存储本质就是 WAL,那么必然会遇到 WAL 经典的问题 —— 如何避免无限制的增长。为什么这么关心增长这个问题呢?因为会导致:

  1. 读效率降低,无法充分利用局部性和系统缓存
  2. Recovery 时长变慢
  3. 备份时长变慢
  4. 维护过多无效数据,资源利用率低,且容易打满磁盘

既然是经典问题,那么必然有经典的解决方案,在关系型数据库中会通过 Checkpoint 来定期刷盘,回收无意义的日志。bitcask 稍微有点不同,因为它的数据就是存储在 WAL 中的,所以它也可以通过借鉴 Checkpoint 这种思路来解决问题,当然它本身是一种简单的 LSM 引擎,在 LSM 中这种机制叫 Merge。

由于所有需要写操作都是 Set语义 和 Append 操作, Merge 对于同一个 Key 只需要保留最后一次的值即可。将所有的 Older Data File(Non-Active and Immutable)汇总为一个精简的快照,之后删除刚才的那些 Older Data File。比如:

  1. 强制切换到一个新的空白的 Active Data File 用于后续的写入操作
  2. 遍历所有的 Older Data File 进行合并,这个过程也需要生成新的索引
  3. 使用新的索引
  4. 删除归档完的 Older Data File

接下来看一下索引,论文中叫做 Keydir ,设计的比较简单, 使用 Hash 表,且都保存在内存中。每次 Append 一条记录后对索引做一次 Upsert 操作。索引的 Value 结构为:

  • File Id
  • Value Size
  • Value Position
  • Timestamp

论文中对索引提到了一个启动优化,上面提到 Merge 的过程除了保留最新数据,还需要生成新的索引,因此除了快照文件还会生成一个 Hint 文件。文件里记录的也是日志格式的数据,但是有 3 点不同:

  1. 不包含 CRC
  2. Key 的值是索引的位置
  3. Value 的值是 Key

新进程打开时如果有 Hint 文件会使用 Hint 加速索引的构建过程。

总结一下,最大的缺点就是所有索引都在内存中,恢复的速度取决于数据规模和合并操作。优点就是写入很快,全部顺序写,读取效率也比较高,只有一次 IO 访问,注意是效率高不是速度快,由于没有 DBMS 的 Buffer Pool 组件,因此速度的快慢取决于:

  • 数据是否在系统缓存中
  • 磁盘的读取性能

不过论文提到的是思路,很多细节都可以自己打磨,比如:

  • 每次写入都落盘比较慢,可以使用组提交,定时提交优化
  • 每次切换 Active Data File 时可以启动后台进程进行类似 Merge 的操作(不需要切换新的 Active Data File 和 更新内存索引),加速恢复过程

总的来说,很适合作为入门学习,快去打造属于你自己的 bitcask 吧。