做了好几年的数据库运维平台开发了,虽然不是做内核开发的,但是多多少少接触了一下,流复制和灾备相关的,很多人入门应该都是流复制吧。
数据库里有很多东西可以学习。所以这里整理记录一下学习过程和内容。
数据库的核心是存储和提取数据,存储引擎是大家最关心的内容。先学习一下常见的存储引擎,不同的存储引擎都有各自的特点和侧重点。今天聊一下最简单的 bitcask。
bitcask是嵌入式的存储引擎,它的构成只有2部分:
- 数据
- 索引
增删改都是采用 Append 日志记录的方式追加到数据文件末尾,这样所有的写操作都是顺序 IO。增加和更改都是一样的 Set 语义。删除的也是 Set 语义,只不过使用的是一个特殊值,一般叫做墓碑。写入数据最终落盘的格式为:
- CRC
- Timestamp
- Key Size
- Value Size
- Key
- Value
当文件的大小达到阈值时关闭这个文件,切换到一个新的文件上进行追加日志。所以写操作只会发生在一个数据文件上,这个数据文件被称为 Active Data File,与之对应的其它数据文件被称为 Older Data File。 即刻 这么看来,数据存储本质就是 WAL,那么必然会遇到 WAL 经典的问题 —— 如何避免无限制的增长。为什么这么关心增长这个问题呢?因为会导致:
- 读效率降低,无法充分利用局部性和系统缓存
- Recovery 时长变慢
- 备份时长变慢
- 维护过多无效数据,资源利用率低,且容易打满磁盘
既然是经典问题,那么必然有经典的解决方案,在关系型数据库中会通过 Checkpoint 来定期刷盘,回收无意义的日志。bitcask 稍微有点不同,因为它的数据就是存储在 WAL 中的,所以它也可以通过借鉴 Checkpoint 这种思路来解决问题,当然它本身是一种简单的 LSM 引擎,在 LSM 中这种机制叫 Merge。
由于所有需要写操作都是 Set语义 和 Append 操作, Merge 对于同一个 Key 只需要保留最后一次的值即可。将所有的 Older Data File(Non-Active and Immutable)汇总为一个精简的快照,之后删除刚才的那些 Older Data File。比如:
- 强制切换到一个新的空白的 Active Data File 用于后续的写入操作
- 遍历所有的 Older Data File 进行合并,这个过程也需要生成新的索引
- 使用新的索引
- 删除归档完的 Older Data File
接下来看一下索引,论文中叫做 Keydir ,设计的比较简单, 使用 Hash 表,且都保存在内存中。每次 Append 一条记录后对索引做一次 Upsert 操作。索引的 Value 结构为:
- File Id
- Value Size
- Value Position
- Timestamp
论文中对索引提到了一个启动优化,上面提到 Merge 的过程除了保留最新数据,还需要生成新的索引,因此除了快照文件还会生成一个 Hint 文件。文件里记录的也是日志格式的数据,但是有 3 点不同:
- 不包含 CRC
- Key 的值是索引的位置
- Value 的值是 Key
新进程打开时如果有 Hint 文件会使用 Hint 加速索引的构建过程。
总结一下,最大的缺点就是所有索引都在内存中,恢复的速度取决于数据规模和合并操作。优点就是写入很快,全部顺序写,读取效率也比较高,只有一次 IO 访问,注意是效率高不是速度快,由于没有 DBMS 的 Buffer Pool 组件,因此速度的快慢取决于:
- 数据是否在系统缓存中
- 磁盘的读取性能
不过论文提到的是思路,很多细节都可以自己打磨,比如:
- 每次写入都落盘比较慢,可以使用组提交,定时提交优化
- 每次切换 Active Data File 时可以启动后台进程进行类似 Merge 的操作(不需要切换新的 Active Data File 和 更新内存索引),加速恢复过程
总的来说,很适合作为入门学习,快去打造属于你自己的 bitcask 吧。