这篇文章是RocksDB官方文档的译文,原文地址:https://github.com/facebook/rocksdb/wiki/RocksDB-Basics
RocksDB项目起源于Facebook的一个实验项目,该项目旨在开发一个与快速存储器(尤其是闪存)存储数据性能相当的数据库软件,以应对高负载服务。
这是一个c++库,可用于存储键和值,可以是任意大小的字节流。它支持原子读和写。
RocksDB具有高度灵活的配置功能,可以通过配置使其运行在各种各样的生产环境,包括纯内存,Flash,硬盘或HDFS。它支持各种压缩算法,并提供了便捷的生产环境维护和调试工具。
RocksDB借鉴了开源项目LevelDB的重要代码和Apache HBase项目的重要思想。最初的代码来源于开源项目leveldb 1.5分叉。它借鉴了了Facebook的代码和思想。
RocksDB的主要设计目标是保证存取快速存储器和高负载服务器更高效,保证充分利用Flash或RAM子系统提供的高速率读写,
支持高效的查找和范围scan,支持高负载的随机读、高负载的更新操作或两者的结合。其架构应该支持高并发读写和容量大增时系统的一致性。
RocksDB内置了用于生产环境中部署和调试的工具和实用程序。大多数主要参数应该是可调整的,以便可以被不同的应用程序在不同的硬件中使用。。
这个软件的新版本应该是向后兼容的,因此,当升级到新版本时现有的应用程序不需要改变。
RocksDB是一个嵌入式键值存储器,其中键和值是任意的字节流。RocksDB中的所有数据是按序存放的。常见操作包括Get(key), Put(key), Delete(key) and Scan(key)。
RocksDB有三个基本结构:RocksDB memtable,sstfile和logfile。memtable是一个内存数据结构——新数据会插入到memtable和日志文件(可选)。日志文件是
顺序写入的,位于磁盘。当memtable写满后,数据会被刷新到磁盘上的sstfile文件,同时相应的日志文件可以安全地删除。sstfile中的数据经过排序的,目的
是为了加快键查找。
键和值被视为纯字节流.没有大小的限制。Get接口允许应用程序从数据库中获取一个键值。MultiGet接口允许应用程序从数据库中检索多个键值。MultiGet接口返回的key-value对都是相互匹配的。
所有数据库中的数据都按顺序存放。应用程序可以指定一个key比较方法来定义key的排序顺序。迭代器允许应用程序对数据库进行RangeScan。迭代器可以先定位一个指定的键,然后应用程序就可以从这个定位点开始一个一个扫描key。迭代器还可以用来对key做反向迭代。创建迭代器是会创建当前数据库的一个快照视图,因此,通过迭代器返回的所有键都来自同一个数据库视图。
Snapshot允许应用程序创建一个快照视图。Get和迭代器可以从一个指定的快照读取数据。在某种意义上,Snapshot和迭代器都提供了某个时间点上数据库的快照视图,但两者的实现是不同的。短暂的扫描最好通过迭代器而耗时较长的扫描最好通过快照。迭代器记录了数据库当前视图对应文件——直到迭代器被释放才删除这些删除。
而快照并不能阻止文件删除;相反,compaction流程知道当前的快照并且不会删除任何现有快照中的key。
数据库重启后,快照将丢失。重载RocksDB(通过服务器重启)会释放所有先前的快照。
大多数LSM引擎无法支持一个高效RangeScan,因为它需要查找每一个数据文件。但大多数应用程序不会对key进行随机扫描,而更多的是扫描给定前缀的key。
RocksDB利用了这种优势。应用程序可以通过prefix_extractor指定一个key的前缀。RocksDB用此来保存每个key前缀的bloom,指定了前缀(通过ReadOptions)的迭代器将使用bloom二进制位来避免查找不包含指定key前缀的文件。
Put操作向数据库插入单个key-value。如果键已经存在,旧值将被覆盖。Writer操作允许将多个keys-values原子地插入到数据库中。数据库保证同一个Writer操作中的所有keys-values要么全部出入,要么都不插入。如果其中任何一个键已经存在于数据库中,旧值将被覆盖。
Put操作数据会存储在内存中的缓冲区称为memtable,也会选择性地插入到事务日志。每一个Put操作都有一组标志(通过WriteOptions设置),这些标志指定Put操作数据是否应该插入到事务日志。WriteOptions也可以指定在put操作提交前一个同步调用是否写入事务日志。
在内部,RocksDB使用batch-commit机制批量写入事务日志,这样它可以使用一个同步调用提交多个事务。
RocksDB使用校验和检测数据是否正确。每个块(通常是4k到128k大小)都有自己的校验和。一块数据一旦写入将不会修改。
RocksDB通过硬件支持动态获取校验和的计算结果,以避免需要是自己计算校验和。
Compactions可以删除同一key的多个副本,副本是应用程序覆盖现有key是产生的。可以删除key。通过配置可以让Compaction也多线程方式运行。
LSM的写数据的整体吞吐量直接取决于Compaction的速度,特别是当数据存储在SSD或RAM这种存储器中。RocksDB可以处理多个线程并发的omopaction请求。
多线程Compaction场景下写数据的速率比单线程场景下的速率快10倍。
整个数据库存储在一组sstfiles。memtable写满时,它的内容会被写入Level-0层的一个文件中,在此过程中,重复和被覆盖的key会被删除。
一些文件被定期压缩合并形成更大的文件——这就是所谓的compaction。
RocksDB支持两种不同形式的compaction。普遍的做法是将所有文件按时间顺序保存在L0。compaction选择几个彼此相邻的文件并将它们合并成一个新文件L0。所有文件可以有重叠的key。
分层形式的compaction将数据存储在数据库中的多个层中。最新的数据存储在L0和最旧的数据存储在Lmax。L0层中的文件可以有重叠的key,但其他层中文件的key不能重叠。一次compaction过程就是选择Ln层的一个文件及它在Ln+1层的所有重叠文件进行压缩合并形成Ln+1层的新文件。相比层形式的compaction方法,
普遍的compaction方法写数据性能较低,但空间利用率较高。
MANIFEST文件记录了数据库的状态。compaction在添加新文件和从数据库删除现有的文件后,会将这些操作记录到MANIFEST文件。事务日志是被批量提交到MANIFEST文件中的,目的是为了减少对MANIFEST文件的重复同步访问。
一些应用程序可能需要在Compaction过程中处理某些key。例如,一个支持TTL的数据库可能删除过期的key,这可以通过定义一个Compaction过滤器完成。
如果应用程序想要不断删除旧数据,可以使用Compaction过滤掉丢弃过期的记录。RocksDB Compaction过滤器可以允许应用程序去修改对应key的value或作为Compaction过程的一部分直接丢弃key。
数据库可以在只读的模式下打开。只读模式下应用程序不能修改任何数据。这可以保证更高的读取性能,因为避免了代码执行路径的切换和锁的使用。
RocksDB的详细日志被写入到名为LOG*的文件中。这些日志用于调试和分析运行中的系统。可以按配置的指定周期记录日志。
RocksDB支持snappy,zlib,bzip2 lz4和lz4_hc压缩算法。对不同层的数据可以配置不同的压缩算法。一般来说,90%的数据保存在Lmax层。
一个典型的安装可能是L0-L2层不配置压缩算法,中间层用snappy压缩算法,而Lmax层采用zlib压缩。
RocksDB将事务日志保存在logfile文件中以防止系统崩溃。系统启动时会重新处理日志文件。logfile和_sstfile_s可以存放在不同目录下,比如下面的场景,
当你希望将所有数据文件存储在非持久但快速的存储设备中,同时把事务日志保存在存取速度慢但持久的存储设备中。
RocksDB支持全量备份和增量备份。RocksDB是一个LSM数据库引擎,因此一旦被创建,数据文件从不会被覆盖,这使得获取某个时间点数据库快照很容易。DisableFileDeletions接口可以禁止RocksDB删除数据文件。Comopaction进行中,数据库中的废弃数据不会被删除。备份接口GetLiveFiles / GetSortedWalFiles可以提取数据库中现有文件并将数据复制到备份位置。
备份完成后,EnableFileDeletions接口可以使能数据文件删除功能;数据库现在就可以回收不需要的所有文件。
增量备份和复制需要能够识别数据库的最近修改。GetUpdatesSince接口允许应用程序tail(理解同linux tail命令) RocksDB事务日志。它可以连续不断地获取RocksDB事务日志并将它们发送带远程备份端或远程复制端。
复制系统通常向Put操作增加元数据,这些元数据可以用来检测管道复制中的数据循环,也可以用于给事务打时间戳和按顺序排序事务。
这些元数据只存储在事务日志,而不存储在数据文件中。PutLogData接口用来添加元数据,GetUpdatesSince用于获取元数据。
RocksDB事务日志是在数据库目录中创建的。不再需要的日志文件会被移动到归档目录。归档文件目录存在的原因是因为一个复制速度较慢的复制流可能需要从日志文件中检索过去的事务。
GetSortedWalFiles接口返回事务日志文件的列表。
RocksDB一个常见的用法是应用程序将数据集划分为逻辑分区或分片。这种技术的有利于应用程序保持负载均衡和快速恢复故障。这意味着一个服务器进程必须能够同时操作多个RocksDB数据库。
这是通过环境变量Env实现的。除此之外,一个线程池与Env相关联。如果应用程序想要在多个数据库实例间共享同一个线程池,那么它应该使用相同的Env对象打开这些数据库。
同样,多个数据库实例可以共享同一缓存块。
RocksDB使用LRU缓存块读取数据。缓存块被划分为两个单独的缓存:第一个缓存数据未压缩,第二个缓存数据压缩。如果缓存块配置了压缩,那么数据库不会在操作系统缓冲区中缓存数据。
表缓存用于存放缓存打开的sstfiles文件描述符。应用程序可以指定表缓存的最大大小。
LSM数据库的性能极大地依赖于Compaction算法及其实现。RocksDB支持两种Compaction压缩算法:层次类型和普遍类型的。开发人员可以开发和测试其他的Compaction算法。出于这个原因,RocksDB提供了关闭内置Compaction算法的接口和应用开发者自己Compaction算法的接口。
如果设置了disable_auto_compaction选项,本地Compaction算法将被禁止使用。GetLiveFilesMetaData接口允许外部组件查看数据库中的每个数据文件并决定合并和Compaction哪些数据文件。
DeleteFile接口允许应用程序删除过时的数据文件。
一些应用程序架构以非阻塞方式访问数据库的,也就是说数据检索请求不需要从存储器读取数据。RocksDB在块缓存中缓存了数据库部分数据,如果应用程序从块缓存中查找到需要的数据,就从块缓存中读取数据;如果在块缓存中
没有找到,RocksDB返回适当的错误代码给应用程序。通过错误码,应用程序知道数据检索调用可能会阻塞存储IO(也许在一个不同的线程上下文中),从而利用普通的Get/Next去获取数据。。
内存表的默认实现是跳跃表skiplist。skiplist一个有序集合,这种结构有利于进行数据范围扫描。有些应用程序不交错写数据或扫描数据,有些应用程序不做范围扫面。
对于这些应用程序,有序集合无法提供最佳性能。出于这个原因,RocksDB提供可插入接口允许应用程序自己实现内存表。数据库一般包含三种内存表:跳跃表内存表,向量内存表和prefix-hash内存表。
向量内存表适合批量加载数据。每次都是在向量的末尾插入一个新元素,每当要将内存表中数据写入到磁盘时,向量中的元素经排序后写入L0层的文件中。prefix-hash内存表适用于快速gets、puts和按key前缀扫描。
RocksDB支持配置任意数量的memtables。当内存表写满后,它将成为一个不可变的内存表,这时会启动后台线程开始将内存表中的数据刷新到磁盘。与此同时,新的数据会被写入新分配的内存表。如果新分配内存表写满到了容量极限,它也转化成不可变内存表并被插入到输出管道中。后台线程继续将所有管道的不可变内存表刷新到磁盘。这种管道写数据方式增加了RocksDB写数据的吞吐量。特别是当操作缓慢的存储设备时,这种方式更高效。
当内存表被刷新到磁盘时,inline-compaction进程会删除输出流中的重复记录。同样,后来的删除操作会隐藏先来的插入操作,此插入操作不会被写入输出文件。这个特性降低了磁盘上的数据大小同时增加了数据写入量。当RocksDB用作生产者消费者队列尤其是当队列中元素存活时间非常短时,这个特性非常重要。
有许多有趣的工具,用于支持生产环境上的数据库。sst_dump用于转储所有keys-values到sst文件中。ldb的工具可以put、get和scan数据库的内容。ldb也可以转储MANIFEST文件
的内容,也可以用来改变数据库中配置的层数。这可以用来手动压缩数据库。
提供了很多测试数据库特定特性的单元测试。make check命令运行所有单元测试。这些单元测试用于测试RocksDB的特定特性,
不用于大规模数据正确性的测试场景。db_stress用来测试大规模数据的正确性。
RocksDB性能是通过db_bench做基准测试的。db_bench是RocksDB源代码的一部分,典型场景的性能测试结果可以参考wiki。