介绍¶
RocksDB 是 Facebook 的一个实验项目,目的是希望能开发一套能在服务器压力下,真正发挥高速存储硬件(特别是 Flash 存储)性能的高效数据库系统。这是一个 C++ 库,允许存储任意长度二进制 kv 数据。支持原子读写操作。
RocksDB 依靠大量灵活的配置,使之能针对不同的生产环境进行调优,包括直接使用内存,使用 Flash,使用硬盘或者 HDFS。支持使用不同的压缩算法,并且有一套完整的工具供生产和调试使用。
RocksDB 大量复用了 levedb 的代码,并且还借鉴了许多 HBase 的设计理念。原始代码从 leveldb 1.5 上 fork 出来。同时 Rocksdb 也借用了一些 Facebook 之前就有的理念和代码。
假设与目标¶
性能¶
RocksDB 最初的设计理念就是其应该在高速存储设备以及服务器压力下能有很好的性能表现。他应该能榨取 Flash 或者 RAM 子系统提供的所有读写速度潜能。他应该能支持高速的点查询和区间查询。可以通过配置支持很高的随机查询负荷,很高的更新负荷或者两者兼有。其架构应能很简单地对读放大,写放大和存储空间放大进行调优。
生产环境支持¶
RocksDB 设计阶段开始就附带内置的工具集合供生产环境部署和调试。主要的参数都应该可以调节以适应不用的硬件上跑的不同的应用程序。
兼容性¶
新版本总是保持向后兼容,已有的应用程序不需要为 RocksDB 升级进行变更。参考 RocksDB版本兼容性
高度分层架构¶
RocksDB 是一种可以存储任意二进制 kv 数据的嵌入式存储。RocksDB 按顺序组织所有数据,他们的通用操作是 Get(key), NewIterator(), Put(key, value), Delete(Key) 以及 SingleDelete(key)。
RocksDB 有三种基本的数据结构:mentable,sstfile 以及 logfile。mentable 是一种内存数据结构——所有写入请求都会进入 mentable,然后选择性进入 logfile。logfile 是一个在存储上顺序写入的文件。当 mentable 被填满的时候,他会被刷到 sstfile 文件并存储起来,然后相关的 logfile 会在之后被安全地删除。sstfile 内的数据都是排序好的,以便于根据 key 快速搜索。
sstfile 的详细格式参考 这里
特性¶
列族 (Column Families)¶
RocksDB 支持将一个数据库实例按照许多列族进行分片。所有数据库创建的时候都会有一个用 "default" 命名的列族,如果某个操作不指定列族,他将操作这个 default 列族。
RocksDB 在开启 WAL 的时候保证即使 crash,列族的数据也能保持一致性。通过 WriteBatch API,还可以实现跨列族的原子操作。
更新操作¶
调用 Put API 可以将一个键值对写入数据库。如果该键值已经存在于数据库内,之前的数据会被覆盖。调用 Write API 可以将多个 key 原子地写入数据库。数据库保证在一个 write 调用中,要么所有键值都被插入,要么全部都不被插入。如果其中的一些 key 在数据库中存在,之前的值会被覆盖。
Gets,Iterators 以及 Snapshots¶
键值对的数据都是按照二进制处理的。键值都没有长度的限制。Get API 允许应用从数据库里面提取一个键值对的数据。MultiGet API 允许应用一次从数据库获取一批数据。使用 MultiGet API 获取的所有数据保证相互之间的一致性(版本相同)。
数据库中的所有数据都是逻辑上排好序的。应用可以指定一种键值压缩算法来对键值排序。Iterator API 允许对 database 做 RangeScan。Iterator 可以指定一个 key,然后应用程序就可以从这个 key 开始做扫描。Iterator API 还可以用来对数据库内已有的 key 生成一个预留的迭代器。一个在指定时间的一致性的数据库视图会在 Iterator 创建的时候被生成。所以,通过 Iterator 返回的所有键值都是来自一个一致的数据库视图的。
Snapshot API 允许应用创建一个指定时间的数据库视图。Get,Iterator 接口可以用于读取一个指定 snapshot 数据。当然,Snapshot 和 Iterator 都提供一个指定时间的数据库视图,但是他们的内部实现不同。短时间内存在的/前台的扫描最好使用 iterator,长期运行/后台的扫描最好使用 snapshot。Iterator 会对整个指定时间的数据库相关文件保留一个引用计数,这些文件在 Iterator 释放前,都不会被删除。另一方面,snapshot 不会阻止文件删除; 作为交换,压缩过程需要知道有 snapshot 正在使用某个版本的 key,并且保证不会在压缩的时候删除这个版本的 key。
Snapshot 在数据库重启过程不能保持存在:reload RocksDB 库会释放所有之前创建好的 snapshot。
事务 (transaction)¶
RocksDB 支持多操作事务。其分别支持乐观模式和悲观模式,参考 事务
前缀迭代器¶
多数 LSM 引擎无法支持高效的 RangeScan API,因为他需要对每个文件都进行搜索。不过多数程序也不需要对数据库进行纯随机的区间扫描;多数情况下,应用程序只需要扫描指定前缀的键值即可。RocksDB 就利用了这一点。应用可以指定一个键值前缀,配置一个前缀提取器。针对每个键值前缀,RockDB 会将其哈希结果存储到 bloom。通过 bloom,迭代器在扫描指定前缀的键值的时候,就可以避免扫描那些没有这种前缀键值的文件了。
持续性¶
RocksDB 有一个事务日志。所有写操作(包括 Put,Delete 和 Merge)都会被存储在 memtable 的内存缓冲区中同时可选地插入到事务日志里面。一旦重启,他会重新处理所有记录在事务日志里的日志。
事务日志可以通过配置,存储到跟 SST 文件不同的目录去。对于那些将所有数据存储在非持续性快速存储介质的情况,这是非常有必要的。同时,你可以通过往较慢但是持续性好的存储介质上写事务日志,来保证数据不会丢失。
每次写操作都有一个标志位,通过 WriteOptions 来设置,允许指定这个 Put 操作是不是需要写事务日志。WriteOptions 同时允许指定在 Put 返回成功前,是不是需要调用 fsync。
在 RocksDB 内部,使用批处理机制实现了通过一次 fsync 的调用将批量事务写入日志中。
错误容忍¶
RocksDB 使用校验和来检查存储的正确性。每个 SST 文件块(一般在 4K 到 128K 左右)都有一个校验和。一个块一旦被写到存储介质,将不再做修改。RocksDB 会动态探测硬件是否支持校验和计算,如果允许,将会使用这种支持。
多线程压缩¶
如果应用程序对已经存在的 key 进行了覆盖,就需要使用压缩将多余的拷贝删除。压缩还会处理删除的键值。如果配置得当,压缩可以通过多线程同时进行。
整个数据库按顺序存储在一系列的 sstfile 里面。当 memtable 写满,他的内容就会被写入一个在 Level-0(L0)的文件。被刷入 L0 的时候,RocksDB 删除在 memtable 里重复的被覆盖的键值。有些文件会被周期性地读入,然后合并为一些更大的文件——这就叫压缩。
一个 LSM 数据库的写吞吐量跟压缩发生的速度有直接的关系,特别是当数据被存储在高速存储介质,如 SSD 和 RAM 的时候。RocksDB 可以配置为通过多线程进行压缩。当使用多线程压缩的时候,跟单线程压缩相比,在 SSD 介质上的数据库可以看到数十倍的写速度增长。
压缩方式¶
一次全局压缩发生在完整的排序好的数据,他们要么是一个 L0 文件,要么是 L1+ 的某个 Level 的所有文件。一次压缩会取部分按顺序排列好的连续的文件,把他们合并然后生成一些新的运行数据。
分层压缩会将数据存储在数据库的好几层。越新的数据,会在越接近 L0 层,越老的数据越接近 Lmax 层。L0 层的文件会有些重叠的键值,但是其他层的数据不会。一次压缩过程会从 Ln 层取一个文件,然后把所有与这个文件的 key 有交集的 Ln+1 层的文件都处理一次,然后生成一个新的 Ln+1 层的文件。与分成压缩模式相比,全局压缩一般有更小的写放大,但是会有更大的空间,读放大。
先进先出型压缩会将在老的文件被淘汰的时候删除它,适用于缓存数据。
我们还允许开发者开发和测试自己定制的压缩策略。为此,RocksDB 设置了合适的钩子来关停内建的压缩算法,然后使用其他 API 来允许应用使用他们自己的压缩算法。选项 disable_auto_compaction 如果被设置为真,将关闭自带的压缩算法。GetLiveFilesMetaData API 允许外部部件查找所有正在使用的文件,并且决定哪些文件需要被合并和压缩。有需要的时候,可以调用 CompactFiles 对本地文件进行压缩。DeleteFile 接口允许应用程序删除已经被认为过期的文件。
元数据存储¶
数据库的 MANIFEST 文件会记录数据库的状态。压缩过程会增加新的文件,然后删除原有的文件,然后通过 MAINFEST 文件来持久化这些操作。MANIFEST 文件里面需要记录的事务会使用一个批量提交算法来减少重复 syncs 带来的代价。
避免低速¶
我们还可以使用后台压缩线程将 memtable 里的数据刷入存储介质的文件上。如果后台压缩线程忙于处理长时间压缩工作,那么一个爆发写操作将很快填满 memtable,使得新的写入变慢。可以通过配置部分线程为保留线程来避免这种情况,这些线程将总是用于将 memtable 的数据刷入存储介质。
压缩过滤器 (compaction filter)¶
某些应用可能需要在压缩的时候对键的内容进行处理。比如,某些数据库,如果提供了生存时间(time-to-live,TTL) 功能,可能需要删除已经过期的 key。这就可以通过程序定义的压缩过滤器来完成。如果程序希望不停删除已经晚于某个时间的键,就可以使用压缩过滤器丢掉那些已经过期的键。RocksDB 的压缩过滤器允许应用程序在压缩过程修改键值内容,甚至删除整个键值对。例如,应用程序可以在压缩的同时进行数据清洗等。
只读模式¶
一个数据库可以用只读模式打开,此时数据库保证应用程序将无法修改任何数据库相关内容。这会带来非常高的读性能,因为它完全无锁。
数据库调试日志¶
RocksDB 会写很详细的日志到 LOG* 文件里面。 这些信息经常被用于调试和分析运行中的系统。日志可以配置为按照特定周期进行翻滚。
数据压缩¶
RocksDB 支持 snappy,zlib,bzip2,lz4,lz4_hc 以及 zstd 压缩算法。RocksDB 可以在不同的层配置不同的压缩算法。通常,90% 的数据会落在 Lmax 层。一个典型的安装会使用 ZSTD(或者 Zlib,如果没有 ZSTD 的话)给最下层做压缩算法,然后在其他层使用 LZ4(或者 snappy,如果没有 LZ4)。参考 压缩算法
全量备份,增量备份以及复制¶
RocksDB 支持增量备份。BackupableDB 会生成 RocksDB 备份样本,参考 How to backup RocksDB?
增量拷贝需要能找到并且附加上最近所有对数据库的修改。GetUpdatesSince 允许应用获取 RocksDB 最后的几条事务日志。它可以不断获得 RocksDB 里的事务日志,然后把他们作用在一个远程的备份或者拷贝。
典型的复制系统会希望给每个 Put 加上一些元数据。这些元数据可以帮助检测复制流水线是不是有回环。还可以用于给事务打标签,排序。为此,RocksDB 支持一种名为 PutLogData 的 API,应用程序可以用这个给每个 Put 操作加上元数据。这些元数据只会存储在事务日志而不会存储在数据文件里。使用 PutLogData 插入的元数据可以通过 GetUpdatesSince 接口获得。
RocksDB 的事务日志会创建在数据库文件夹。当一个日志文件不再被需要的时候,他会被放倒归档文件夹。之所以把他们放在归档文件夹,是因为后面的某些复制流可能会需要获取这些已经过时的日志。使用 GetSotredWalFiles 可以获得一个事务日志文件列表。
在同一个线程支持多个嵌入式数据库¶
一个 RocksDB 的常见用法是,应用内给他们的数据进行逻辑上的分片。这项技术允许程序做合适的负载均衡以及快速出错恢复。这意味着一个服务器进程可能需要同时操作多个 RocksDB 数据库。这可以通过一个名为 Env 的环境对象来实现。例如说,一个线程池会和一个 Env 关联。如果多个应用程序希望多个数据库实例共享一个进程池(用于后台压缩),那么就应该用同一个 Env 对象来打开这些数据库。
类似的,多个数据库实例可以共享同一个缓存块。
缓存块——已经压缩以及未压缩的数据¶
RocksDB 对块使用 LRU 算法来做读缓存。这个块缓存会被分为两个独立的 RAM 缓存:第一部分缓存未压缩的块,然后第二部分缓存压缩的块。如果配置了使用压缩块缓存,用户应该同时配置直接 IO,而不使用操作系统的页缓存,以避免对压缩数据的双缓存问题。
表缓存¶
表缓存是一种用于缓存打开的文件描述符的结构体。这些文件描述符都是 sstfile 文件。一个应用可以配置表缓存的最大大小。
IO 控制¶
RocksDB 允许用户配置 IO 应该如何执行。他们可以要求 RocksDB 对读文件调用 fadvise,文件 sync 的周期,活着允许直接 IO。参考 IO
StackableDB¶
RocksDB 内带一套封装好的机制,允许在数据库的核心代码上按层添加功能。这个功能通过 StackabDB 接口实现,比如 RocksDB 的存活时间功能,就是通过 StackableDB 接口实现的,他并不是 RocksDB 的核心接口。这种设计让核心代码可模块化,并且整洁易读。
memtable¶
插件式的 Memtable:
RocksDB 的 memtable 的默认实现是跳表(skiplist)。跳表是一个有序集,如果应用的负载主要是区间查询和写操作的时候,非常高效。然而,有些程序压力不是主要写操作和扫描,他们可能根本不用区间扫描。对于这些应用,一个有序集可能不能提供最好的性能。为此,RocksDB 提供一个插件式 API,允许应用提供自己的 memtable 实现。这个库自带三种 memtable 实现:skiplist 实现,vector 实现以及前缀哈希实现的 memtable。vector 实现的 memtable 适用于需要大批量加载数据到数据库的情况。每次写入新的数据都是在 vector 的末尾追加新数据,当我们需要把 memtable 的数据刷入 L0 的时候,我们才进行一次排序。一个前缀哈希的 memtable 对 get,put,以及键值前缀扫描拥有较好的性能。
memtable 流水线:
RocksDB 允许为一个数据库设定任意数量的 memtable。当 memtable 写满的时候,他会被修改为不可变 memtable,然后一个后台线程会开始将他的内容刷入存储介质中。同时,新写入的数据将会累积到新申请的 memtable 里面。如果新的 memtable 也写入到他的数量限制,他也会变成不可变 memtable,然后插入到刷存储流水线。后台线程持续地将流水线里的不可变 memtable 刷入存储介质中。这个流水线会增加 RocksDB 的写吞吐,特别是当他在一个比较慢的存储介质上工作的时候。
memtable 写存储的时候的垃圾回收工作:
当一个 memtable 被刷入存储介质,一个内联压缩过程会删除输出流里的重复纪录。类似的,如果早期的 put 操作最后被 delete 操作隐藏,那么这个 put 操作的结果将完全不会写入到输出文件。这个功能大大减少了存储数据的大小以及写放大。当 RocksDB 被用于一个生产者——消费者队列的时候,这个功能是非常必要的,特别是当队列里的元素存活周期非常短的时候。
合并操作符¶
RocksDB 天然支持三种类型的记录,Put 记录,Delete 记录和 Merge 记录。当压缩过程遇到 Merge 记录的时候,他会调用一个应用程序定义的,名为 Merge 操作符的方法。一个 Merge 可以把许多 Put 和 Merge 操作合并为一个操作。这项强大的功能允许那些需要读——修改——写的应用彻底避免读。它允许应用把操作的意图记录为一个 Merge 记录,然后 RocksDB 的压缩过程会用懒操作将这个意图应用到原数值上。这个功能在 合并操作符 里面有详细说明。
工具¶
有一系列有趣的工具可以用于支持生产环境的数据库。sst_dump 工具可以导出一个 sst 文件里的所有 kv 键值对。ldb 工具可以对数据库进行 get,put,scan 操作。ldb 工具同时还可以导出 MANIFEST 文件的内容,还可以用于修改数据库的层数。还可以用于强制压缩一个数据库。
测试用例¶
我们有大量的单元测试用于测试数据库的特定功能。make check 命令可以跑所有的单元测试用例。测试用例会触发 RocksDB 的特定功能,但是不是用于测试压力下数据的正确性。db_stress 测试数据库在压力下的准确性。
性能¶
RocksDB 的性能通过一个名为 db_bench 的工具进行测量。db_bench 是 RocksDB 的源码的一部分。在 Flash 存储下,一些典型的工作负荷下的性能结果可以参考 这里。同时 RocksDB 在纯内存环境的表现参考 这里。