写点什么

LevelDB 多版本和压缩

  • 2025-08-11
    浙江
  • 本文字数:3968 字

    阅读完需:约 13 分钟

作为高性能键值存储库,LevelDB 的高效运行离不开多版本机制与压缩策略的支撑。多版本如何实现数据的并发读写与历史回溯?压缩机制怎样平衡存储效率与访问性能?当数据遭遇异常,恢复机制又如何保障可靠性?本文将围绕这三大核心维度,拆解 LevelDB 的底层设计智慧。

文件类型:

多版本

版本管理中相关的类

  • Version 标识了一个版本,主要信息是这个版本包含的 SSTable 信息;

  • VersionSet 是一个版本集,里面保存了 Version 的一个双链表,其中有一个 Version 是当前版本,因为只有一个实例,还保存了其它的一些全局的元数据,Dummy Version 是链表头;

  • VersionEdit 保存了要对 Version 做的修改,在一个 Version 上应用一个 VersionEdit,可以生成一个新的 Version;

  • Builder 是一个帮助类,帮助 Version 上应用 VersionEdit,生成新版本

VersionSet 是一个双链表结构,每个 Version 代表了一个版本

将当前的 版本 和 VersionEdit 合并之后,就得到了一个新的版本 VersionEdit 就是一次 Minor Compaction、或者是 Major Compaction 的结果

版本管理的工作流程:

  • VersionSet 里保存着当前版本,以及被引用的历史版本;

  • 当有 Compaction 发生时,会将更改的内容写入到一个 VersionEdit 中;

  • 利用 Builder 将 VersionEdit 应用到当前版本上面生成一个新的版本;

  • 将新版本链接到 VersionSet 的双链表上面;

  • 将新的版本设置为当前版本;

  • 将旧的当前版本 Unref,就是引用计数减 1。

版本控制中的引用计数:

  • 每个 Version 包含了一个引用,当前版本的 ref 为 1

  • 当有迭代器需要遍历数据库时,这个版本的 ref ++ 等于 2

  • 其他线程不断写入,触发 compaction,生成新版本

  • 新版本 + 1,老版本 - 1

  • 因为 老版本为 1,所以不会被删除,直到迭代接受,ref = 0

  • 如果 Version 的 ref 为 0,则可以删除了

实际存储的时候,是存入dbname/MANIFEST-[0-9]+ 这样的文件 CURRENT 指向最新的 manifest 文件 Version 相当于 一个 manifest 文件,VersionSet 则管理多个 manifest 文件 VersionSet 头结点指向最新的 manifest 文件


FileMetaDatasstable 的元信息

Version

位置:db/version_set.h

读取数据库时,就需要 Version 里面的信息

VersionEdit

VersionEdit 中包含了两个函数

这个就是一个 编码,一个解码将 dst 中的信息,写入到所有变量中,以及从 变量中解码信息到 Slice 中

VersionSet

VersionSet::Builder 辅助类

主要变量:

LevelFileNumIterator

主要变量

其中 new 出来的 两次迭代器,就使用到了 LevelFileNumIterator

builder 版本变迁过程

总结一下

  • 整体是 VersionSet 的双链表结构,一个版本由 Version 表示

  • VersionEdit 是一次变更,比如 sstable 新增,删除(通过压缩产生),通过 应用 VersionEdit 就可以得到一个新版本

  • builder 会加速变更,可以将多个 VersionEdit 一次性应用

  • 每个 Version 内部关联了一个 FileMetaData,表示一个 sstable 的元数据,内部包含了 min、max key 方便定位

  • MANIFEST 实际关联了一个 log 文件,还会将变更的内容当做 log 记录下来,等启动恢复的时候,就会读取这些 log 文件

压缩

压缩,包含两种

  • Minor Compaction,mem-table 写满会后刷盘,写入到 level-0 中

  • Major Compaction,某个层的文件太多,需要将部分文件推入更高层

Minor Compaction 过程

Minor Compaction 时序图

WriteLevel0Table 过程

  • 得到一个新的 FileMetaData 编号,就是 VersionSet->next_file_number_++

  • 调用 BuildTable,其生成的 TableFileName 是根据 FileMetaData 的编号得到的

  • 获取 min、max key,然后确定将 ss-table 推入哪个层级,调用 PickLevelForMemTableOutput

  • 将 压缩状态存入 CompactionStats[level] 中,这里的 level 固定为 PickLevelForMemTableOutput 返回的层级

BuildTable 过程:

  • 从 skip-list 的第一个元素开始遍历,一直到结束,将这些 key-value 加入到 TableBuilder

  • 同时记录 FileMetaData 的最大、最小、key 数量、文件 size

  • 将数据写入到磁盘

PickLevelForMemTableOutput

  • 如果跟 0 层的有重叠,则推入 0 层

  • 检查方式是,对于 level0,需要遍历所有文件,检查每个文件跟 min、max key 是否有重叠

  • 非 level 0 的,则使用 二分查找 当前层的所有文件

  • 还会检查 level 0、level 1 在其祖父层级(对应的就是 level 2、level 3 中,是否重叠太多

  • 默认的最大单个文件大小为 2M,如果一次重叠的数量 > 20M,PickLevelForMemTableOutput 会继续往更深层级查找

  • 不过限制了最大查找层级为 2,这也是做了一个权衡

Major Compaction 的过程

Compaction 类

ManualCompaction 类

Compaction 的触发方式

  • Manual Compaction

  • Size Compaction

  • Seek Compaction

手动压缩的触发函数:

Size compation

  • 实现函数为: void VersionSet::Finalize(Version* v)

  • 对于 leve 0,如果 > 4 就会触发

  • 超过 0 的,如果超过了 10^n MB 就会触发

  • 由于 ss-table 的不变性,只有版本变更的时候才会变化

  • 在每次版本变更之后,调用 Finalize,计算出比例最大的 level,下次就从这里开始

  • 由于一次压缩要很久,这里会记录一个 level 的 key,std::string compact_pointer_[config::kNumLevels];

  • 下次再压缩这一层的时候,就从这个 key 开始继续压缩

Seek Compaction

  • 为读做的优化,DB::GET、DBIter 时候,如果读取了超过一个 ss-table 则需要优化

  • 对于读取 超过一个 的 ss-table,对于第一个 ss-table 做一个 减操作,如果等于 0 了,就需要做压缩

  • 所以 seek 是精确到某个 ss-table 的

  • 迭代的时候,是每读取 2M 数据,就模拟一个 key,做对应的 ss-table–

  • 初始值,static_cast((f->file_size / 16384U))

  • 一次 Seek 花费 10ms;

  • 读写 1MB 的花费 10ms (100MB/s);

  • 而 Compaction 1MB 的数据花费 25MB 左右的 IO

执行过程

  • 执行优先级是 Manual、Size、Seek

  • 前两者是根据 start、end key 选择某一个 level 的多个文件

  • 后者只有确定的一个文件

  • 根据 level 层的文件,再确定 level + 1 层的文件,之后再反向 level 层扩大 level 的输入范围

  • 最后调用多路合并,进行压缩

扩大输入文件集合

  • 确定起始 key、结束 key

  • 在 level i 层中,查找与起始输入文件有 key 重叠的文件,如图中红线所标注,最终构成 level i 层的输入文件;

  • 利用 level i 层的输入文件,在 level i+1 层找寻有 key 重叠的文件,结果为绿线标注的文件,构成 level i,i+1 层的输入文件;

  • 最后利用两层的输入文件,在不扩大 level i+1 输入文件的前提下,查找 level i 层的有 key 重叠的文件,结果为蓝线标准的文件,构成最终的输入文件;

归并的过程:

  • 迭代按照 Internal Key 的顺序进行,多个 key 则按 seq number 降序排列

  • 相同的 key 只有第一个有效,因为它的 seq 是最大的

  • 碰到一个删除时,并且它的 seq_number <= 最新快照,会判断更高层是否有这个 key

  • 如果有则无法丢弃这个删除操作,因为一旦丢弃,更高层又可见了,不存在可则可以删除

  • 如果 ss-table 达到 2M,或者和上层文件重叠超过 10 个,这两个条件任意一个触发就会 生成一个 ss-table 写磁盘

数据删除的处理

  • 如果是覆盖写入,则当做一个删除处理,即按照降序排序,前面的覆盖后面的,后面重复的 key 可以忽略

  • 如果是删除标记,并且 level + 1 层没有这个 key,则可以删除

  • 如果这个 key 的 seq_number <= 最老快照的序列号,则可以删除,否则不能删除(因为可能被使用中)

level 0 的压缩过程

  • 选择 key 800 - 2500

  • 因为 level 0 有重叠,遍历所有文件,找到 key 重叠的文件,确定 start、end key

  • 选择 level 1 层,根据 start key、end key 确定 level 1 层的文件

  • 将 level 0 和 level 1 的文件做多路归并

level > 0 的压缩过程

  • 选择 key 800 - 2500

  • 因为是完全有序的,可以根据二分搜索到确定的范围,确定出 level 层的文件

  • 再确定出 level + 1 层的文件

MVCC 数据清理

  • 遍历所有文件

  • 小于 VersionSet 的 log_number_ 的文件

  • 或者不在 遍历列表中的文件,则可以删除

恢复

LevelDB 打开需要做以下事情:

  • 如果数据库目录不存在,创建目录

  • 加文件锁,锁住整个数据库;

  • 读取 MANIFEST 文件,恢复系统关闭时的元数据,也就是版本信息,或者新建 MAINFEST 文件;

  • 如果上一次关闭时,MemTable 里有数据,或者 Immutable MemTable 写入到 SSTable 未完成,那么需要做数据恢复,从 WAL 恢复数据;

  • 创建数据库相关的内存数据结构,如 Version、VersionSet 等

DBImpl::Recover 做了以下事情

  • 创建数据库目录;

  • 对这个数据库里面的 LOCK 文件加文件锁,单进程的

  • 如果数据库不存在,那么调用 DBImpl::NewDB 创建新的数据库;

  • 调用 VersionSet::Recover 来读取 MANIFEST,恢复版本信息;

  • 根据版本信息,搜索数据库目录,找到关闭时没有写入到 SSTable 的日志,按日志写入顺序逐个恢复日志数据

  • DBImpl::RecoverLogFile 会创建一个 MemTable,开始读取日志信息,将日志的数据插入到 MemTable

  • 并根据需要调用 DBImpl::WriteLevel0Table 将 MemTable 写入到 SSTable 中

异常恢复

  • 如果元数据丢了,但是数据还在,也可以恢复的

  • 把所有 sstable 当做 level 0 层,然后不断做压缩

  • 由于 sstable 数量会远大于 0,这样会大致大量的 compaction

  • 经过很多次 compation 最后会稳定下来,形成新的 元数据

  • 旧的文件则会删除

参考

  • 论文《Less hashing, same performance:Building a better Bloom filter》

  • github index

  • Bloom Filters by Example

  • 漫谈 LevelDB 数据结构(一):跳表(Skip List)

  • 漫谈 LevelDB 数据结构(二):布隆过滤器(Bloom Filter)

  • 漫谈 LevelDB 数据结构(三):LRU 缓存( LRUCache)

  • LevelDB 源码剖析

  • SF-Zhou’s Blog

  • leveldb-handbook

  • 庖丁解 LevelDB

  • rust 使用

  • LevelDB 使用介绍

  • LeveLDB 维基百科

  • dbdb.io 的 LevelDB 介绍

  • MariaDB 的插件

  • 书籍:精通 LevelDB

  • leveldb 实现解析

  • POSIX™ 1003.1 Frequently Asked Questions (FAQ Version 1.18)

  • Spurious wakeup

  • Memory barrier

  • Memory Barriers: a Hardware View for Software Hackers

  • Bean Li 的 LevelDB 文章汇总

  • LevelDB 设计与实现 - Compaction

  • LevelDB 设计与实现 - MVCC 等

  • leveldb 源码剖析 系列

  • HBase file format with inline blocks (version 2)

相关文章

  • LevelDB 辅助工具类

  • LevelDB SSTable 模块

  • LevelDB MemTable 模块

  • LevelDB Log 模块

  • LevelDB 公开的接口

用户头像

一站式多云AI原生数智平台 2022-12-05 加入

数新智能是一家专注于一站式多云AI原生数智平台和数据价值流通的服务商。公司自主研发的核心产品主要包括赛博数据平台CyberData、赛博智能平台CyberAI、赛博数智引擎CyberEngine、AI一体机CyberGPT。

评论

发布
暂无评论
LevelDB 多版本和压缩_数新网络官方账号_InfoQ写作社区