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的详细格式参考这里
RocksDB支持将一个数据库实例按照许多列族进行分片。所有数据库创建的时候都会有一个用"default"命名的列族,如果某个操作不指定列族,他将操作这个default列族。
RocksDB在开启WAL的时候保证即使crash,列族的数据也能保持一致性。通过WriteBatch API,还可以实现跨列族的原子操作。
调用Put API可以将一个键值对写入数据库。如果该键值已经存在于数据库内,之前的数据会被覆盖。调用Write API可以将多个key原子地写入数据库。数据库保证在一个write调用中,要么所有键值都被插入,要么全部都不被插入。如果其中的一些key在数据库中存在,之前的值会被覆盖。
键值对的数据都是按照二进制处理的。键值都没有长度的限制。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。
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的数据刷入存储介质。
某些应用可能需要在压缩的时候对键的内容进行处理。比如,某些数据库,如果提供了生存时间(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文件。一个应用可以配置表缓存的最大大小。
RocksDB允许用户配置IO应该如何执行。他们可以要求RocksDB对读文件调用fadvise,文件sync的周期,活着允许直接IO。参考IO
RocksDB内带一套封装好的机制,允许在数据库的核心代码上按层添加功能。这个功能通过StackabDB接口实现,比如RocksDB的存活时间功能,就是通过StackableDB接口实现的,他并不是RocksDB的核心接口。这种设计让核心代码可模块化,并且整洁易读。
插件式的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在纯内存环境的表现参考这里。