├── img └── iterator-struct.png ├── doc ├── Low-Priority-Write.md ├── Logger.md ├── Snapshot.md ├── DeleteRange.md ├── Sub-Compaction.md ├── WAL-Recovery-Modes.md ├── Dictionary-Compression.md ├── Allocating-Some-Indexes-and-Bloom-Filters-using-Huge-Page-TLB.md ├── Checkpoints.md ├── Tailing-Iterator.md ├── SeekForPrev.md ├── Time-to-Live.md ├── Atomic-flush.md ├── Managing-Disk-Space-Utilization.md ├── Write-Buffer-Manager.md ├── WelcomeToRocksDB.md ├── Implement-Queue-Service-Using-RocksDB.md ├── Choose-Level-Compaction-Files.md ├── Compaction-Filter.md ├── Single-Delete.md ├── Compression.md ├── Rate-Limiter.md ├── Persistent-Read-Cache.md ├── RocksDB-Repairer.md ├── Simulation-Cache.md ├── FIFO-compaction-style.md ├── Background-Error-Handling.md ├── How-we-keep-track-of-live-SST-files.md ├── Manual-Compaction.md ├── Statistics.md ├── Write-Stalls.md ├── IO.md ├── MemTable.md ├── Indexing-SST-Files-for-Better-Lookup-Performance.md ├── EventListener.md ├── Data-Block-Hash-Index.md ├── Iterator.md ├── Direct-IO.md ├── Delete-Stale-Files.md ├── Write-Ahead-Log-File-Format.md ├── Memory-usage-in-RocksDB.md ├── Column-Families.md ├── Write-Ahead-Log.md ├── Creating-and-Ingesting-SST-files.md ├── Setup-Options-and-Basic-Tuning.md ├── Perf-Context-and-IO-Stats-Context.md ├── RocksDB-Options-File.md ├── Rocksdb-BlockBasedTable-Format.md ├── RocksDB-Bloom-Filter.md ├── Leveled-Compaction.md ├── Prefix-seek.md ├── Iterator-Implementation.md ├── Compaction-Stats-and-DB-Status.md ├── Option-String-and-Option-Map.md ├── Partitioned-Index-Filters.md ├── Block-Cache.md ├── MANIFEST.md ├── RocksJava-Basics.md ├── Compaction.md ├── How-to-backup-RocksDB.md ├── Terminology.md ├── Administration-and-Data-Access-Tool.md ├── WriteUnprepared-Transactions.md ├── Two-Phase-Commit-Implementation.md ├── OverView.md ├── RocksJava-Performance-on-Flash-Storage.md └── Transactions.md └── README.md /img/iterator-struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnzeng/rocksdb-doc-cn/HEAD/img/iterator-struct.png -------------------------------------------------------------------------------- /doc/Low-Priority-Write.md: -------------------------------------------------------------------------------- 1 | 用户有的时候需要在后台做大量的写操作。其中一个例子就是他们希望加载大量数据的时候。另一个就是他们在做数据镜像的时候。 2 | 3 | 处理这些案例的最好的办法就是[创建并导入sst文件](Creating-and-Ingesting-SST-files.md)。然而,总有一些情况,用户不能并行处理批量导入数据和往数据库写入数据。这时,他们会遇到这个问题:如果他们让后台的写进程全力写入,可能会触发DB的限流机制(参考[写瘫痪]()),这不仅仅会导致后台写入瘫痪,还会把用户的在线查询也拉垮。 4 | 5 | 低优先级写可以帮助用户管理类似的案例。从5.6版本之后,用户开启后台写的时候可以设置WriteOptions.low_pri=true。RocksDB会用更加激进的写限流来处理低优先级写,保证高优先级的写入不会瘫痪。 6 | 7 | 当DB运行的时候,RocksDB会通过监控还没处理的L0的文件以及已经追加的待压缩的字节数来苹果我们是不是有压缩压力。如果RocksDB认为有压缩压力,他会强迫低优先级写入进入睡眠,这样低优先级写入的比例就会保持较小的水平。这样,总的写速率会在遇到总体写限流之前早早下降,更好的保证高优先级写入的质量。 8 | 9 | 在[两阶段提交]()中,在准备阶段的低优先级写入被关闭,而不是提交阶段。 10 | 11 | 12 | -------------------------------------------------------------------------------- /doc/Logger.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | RocksDB支持一种通用消息日志基础设施。RocksDB能满足多种使用场景 —— 从低功耗移动系统到高端分布式服务器。这套框架帮助我们针对不同的需求拓展日志消息设施。相较于一个运行在服务器的严苛环境的应用,移动应用可能需要相对简单的日志机制。他还提供了将RocksDB日志消息集成到嵌入式应用的方法。 4 | 5 | # 已有的日志系统 6 | 7 | [Logger](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/env.h#L663) 类提供了一个接口定义了RocksDB的日志消息。几个Logger的实现如下: 8 | 9 | 10 | 实现 | 使用 11 | ------- | ------- 12 | NullLogger | 日志输出到/dev/null 13 | StderrLogger | 把日志输出到std::err 14 | HdfsLogger | 日志输出到HDFS 15 | PosixLogger | 日志输出到POSIX文件系统 16 | AutoRollLogger | 当文件到达一定大小后,自动翻滚。服务器的常用选择 17 | WinLogger | 为Windows操作系统定制 18 | 19 | # 自定义Logger 20 | 21 | 我们鼓励用户通过拓展已有的实现自己写日志系统。 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /doc/Snapshot.md: -------------------------------------------------------------------------------- 1 | 一个快照会捕获在创建的时间点的DB的一致性视图。快照在DB重启之后将消失。 2 | 3 | # API 使用 4 | 5 | - 通过GetSnapshot API创建一个快照 6 | - 通过设置ReadOptions::snapshot来读取快照的内容 7 | - 当读取结束,调用ReleaseSnapshot释放相关资源 8 | 9 | # 实现 10 | 11 | ## Flush/compaction Representation 12 | 13 | 一个快照相当于一个SnapshotImpl类的小型对象。他只持有部分简单的字段,比如快照生成的时候的seqnum。 14 | 15 | Snapshot会存储在一个DBImpl持有的链表里。其中一个好处是,我们可以在获取DB互斥锁钱分配好这个链表的节点。然后在持有互斥锁的时候,我们只需要更新链表指针。更进一步,ReleaseSnapshot可以对所有的快照以任意顺序被调用。使用链表,我们不需要移动所有节点就可以删除一个节点了。 16 | 17 | ## 伸缩性 18 | 19 | 使用链表的唯一问题是,尽管他是顺序排列的,他还是不可以使用二分查找。在flush/comapction的时候,如果我们需要找到一个key在最早的哪个snapshot中是可见的,我们必须煮个扫描快照链表。当许多快照存在的时候,这个扫描会非常明显的拖慢flush/compaction到一个写失速的点。我们在有成百上千个快照的时候就观察到了这种问题。 20 | 21 | -------------------------------------------------------------------------------- /doc/DeleteRange.md: -------------------------------------------------------------------------------- 1 | DeleteRange这个操作,被设计出来替换下面这种用户需要删除一整段key的场景。 2 | 3 | ```cpp 4 | ... 5 | Slice start, end; 6 | // set start and end 7 | auto it = db->NewIterator(ReadOptions()); 8 | 9 | for (it->Seek(start); cmp->Compare(it->key(), end) < 0; it->Next()) { 10 | db->Delete(WriteOptions(), it->key()); 11 | } 12 | ... 13 | ``` 14 | 15 | 这种场景需要执行一个范围扫描,这就导致无法做到原子化操作,并且无法满足性能敏感的写场景。为了解决这个问题,RocksDB提供了一个院子操作来解决这个任务: 16 | 17 | ```cpp 18 | ... 19 | Slice start, end; 20 | // set start and end 21 | db->DeleteRange(WriteOptions(), start, end); 22 | ... 23 | ``` 24 | 25 | 底层,他会创建一个范围墓碑,表现上就是一个kv对,会显著提升写速度。范围扫描的读性能则与 扫描-删除 模式向兼容(更加详细的性能分析,参考[DeleteRange Blog](https://rocksdb.org/blog/2018/11/21/delete-range.html))。 26 | 27 | -------------------------------------------------------------------------------- /doc/Sub-Compaction.md: -------------------------------------------------------------------------------- 1 | 这里我们将解释一下子压缩过程,他被用于Leveled和universal风格压缩。 2 | 3 | # 目标 4 | 5 | 自压缩的目标是通过分片到多个线程,来加速一个压缩任务。 6 | 7 | # 何时? 8 | 9 | 当下面的条件中的一个成立的时候会发生: 10 | 11 | - L0 -> Lo, o>0。 12 | - 为什么? L0->Lo不能跟另一个L0->Lo并行运行,因此分片是惟一的加速方法。 13 | - universal压缩,除了 L0 -> L0。 14 | - 人工Leveled压缩,L0 -> Lo, o>0 15 | - 为什么?用户触发的人工压缩通常不能并行,所以可以通过分片来处理 16 | 17 | 注意:自压缩过程如果没有从目标Level Lo的文件需要合并,就不会启动。参考Compaction::ShouldFormSubcompactions了解更多 18 | 19 | # 如何? 20 | 21 | 目前一个基于启发式的实现,运行良好。启发式可以再许多方向上进行改善。 22 | 23 | 选择基于输入文件/层的自然边界 的 边界。 24 | 25 | - L0的文件的第一个和最后一个key 26 | - 非0 和 非最后 一层 的第一个和最后一个key 27 | - 最后一层的每个SST文件的第一个key 28 | 29 | 使用Versions::ApproximateSize 来估算每个边界内的数据大小。 30 | 31 | 合并边界来估计空的和小于平均的范围。 32 | 33 | - 找到每个范围的平均大小 34 | - 从开始处,贪婪合并连续的范围,直到他们的大小超过平均值。 35 | 36 | -------------------------------------------------------------------------------- /doc/WAL-Recovery-Modes.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | 每个应用都是独特的,并且可能都需要一些来自RocksDB的并发保护。RocksDB中每个提交的记录都会被持久化。没有提交的消息会被记录到WAL(Write-Ahead-Log.md)。当rocksdb被安全地关闭,所有未提交的记录都会在关闭前提交,因此一致性总是可以保证的。当rocksDB被kill掉或者机器重启,RocksDB在重启的时候需要将自己恢复到一致性的状态。 4 | 5 | 恢复过程中一个非常重要的操作是重放WAL中没有提交的记录。不同的WAL恢复模式会有不同的重放行为。 6 | 7 | # WAL恢复模式 8 | 9 | ## kTolerateCorruptedTailRecords 10 | 11 | 在这种模式,WAL重放忽略任何日志尾部的错误。这么做的原因是,在一个不完整的关机下,日志尾部可能有些没有写完的数据。这是一个启发式的模式,系统无法区分到底是日志尾部数据损坏还是没有写完。其他的IO错误,会被认为是数据损坏。 12 | 13 | 这个模式对大部分应用都是可以接受的,因为他在一个不完整的关机之后重启RocksDB和一致性之间,提供了一个合理的平衡。 14 | 15 | ## kAbsoluteConsistency 16 | 在这个模式下,WAL重放的时候,任何IO错误都会被认为数据错误。这个模式对于那些不能接受任何一个数据丢失以及/或者有其他方式来恢复数据的应用,是非常理想的。 17 | 18 | ## kPointInTimeConsistency 19 | 20 | 在这个模式,WAL重放会在遇到一个IO错误的时候停止。系统会恢复到一个一致性能得到保证的特定时间点。这对于有复制的系统来说很好用。其他复制集的数据可以用来重放从“特定时间点”之后的数据以恢复系统。 21 | 22 | ## kSkipAnyCorruptedRecord 23 | 24 | 在这种模式,读取日志过程中,任何IO错误都会被忽略。系统尝试恢复尽可能多的数据。适用于灾难恢复。 25 | 26 | -------------------------------------------------------------------------------- /doc/Dictionary-Compression.md: -------------------------------------------------------------------------------- 1 | # 目的 2 | 3 | 动态字典压缩算法在压缩小数据的时候会有问题。在默认的使用下,压缩字典从空白开始,并且通过每次单独的输入数据传递进行构建。因此小的输入会导致一个小的字典,所以无法构建一个好的压缩率。 4 | 5 | 在RocksDB,每一个机遇块的表(SST文件)里面的块都是独立压缩的。那些不同构建一个合适数据块大小的字典的压缩算法构造的块的大小默认是4KB。字典压缩功能会通过多个块采样,预先设置好字典,这样会在有重复数据的时候提升压缩率。 6 | 7 | # 使用 8 | 9 | 把rocksdb::CompressionOptions::max_dict_bytes(在include/rocksdb/options.h里面)设置为一个非零的数字,用于声明每个字典文件的最大大小。 10 | 11 | rocksdb::CompressionOptions::zstd_max_train_bytes也可被用于设定ZSTD压缩的训练字典的最大Byte数。使用ZSTD的字典训练,可以获得一个比只使用max_dict_bytes更好的压缩率。训练数据会用于生成一个最大大小为max_dict_bytes的字典。 12 | 13 | # 实现 14 | 15 | 当目标层是最底层的时候,在一个子压缩任务的第一个输出文件会被用于构建字典的采样。每个样本64Byte,并且会统一/随机地从文件中提取。当有间隔的提取样本的时候,我们假设输出文件会达到他的可能的最大大小。否则,某些采样区间会跑到数据文件的区间外面,这样他们就没法被纳入到字典,然后他的大小就会小于max_dict_bytes。 16 | 17 | 一旦生成结束,这个字典就会在剩下的子压缩流程压缩/解压缩数据块前被加载到压缩库中。字典会被存储在文件的元数据块,这样解压的时候才能找到。 18 | 19 | # 限制 20 | 21 | - 统一随机取样不够优秀 22 | - 要求每个文件都有这些重复的数据,因为我们是从子压缩流程的第一个文件里面生成字典,并应用于后面的文件。 23 | 24 | -------------------------------------------------------------------------------- /doc/Allocating-Some-Indexes-and-Bloom-Filters-using-Huge-Page-TLB.md: -------------------------------------------------------------------------------- 1 | # 什么时候需要他 2 | 3 | 当DB使用成吨的GB级别的内存的时候,很大可能当程序需要访问内存数据的时候,程序会遇到TLB未命中的情况和从映射取数据的时候缓存未命中的情况。当使用memtable和表读取器提供的基于哈希表的索引和bloom过滤器的时候,用户的感觉会更明显,因为数据的局部性非常糟糕。这些索引以及bloom都非常适合放在巨型页TLB中。当你看到TLB数据大量溢出,并且有巨型页功能支持的时候,考虑打开这个功能吧。 4 | 5 | 现在只在Linux支持这个功能。 6 | 7 | # 如何使用 8 | 9 | ## 需求单 10 | 11 | - 你需要在linux中预留出巨型页 12 | - 知道可以使用的巨型页的大小 13 | 14 | 参考Linux的Documentation/vm/hugetlbpage.txt获得更多细节 15 | 16 | ## 配置 17 | 18 | 这里介绍这个功能在哪里,如何打开: 19 | 20 | - memtable的bloom过滤器:设置Options.memtable_prefix_bloom_huge_page_tlb_size为巨型页的大小 21 | - 哈希链表的memtable索引以及bloom过滤器:当调用NewHashLinkListRepFactory来创建一个memtable工厂对象的时候,把巨型页的大小通过huge_page_tlb_size传入 22 | - PlainTableReader的索引及bloom过滤器。当调用NewPlainTableFactory或者NewTotalOrderPlainTableFactory创建表工厂对象的时候,通过huge_page_tlb_size传入巨型页的大小 23 | 24 | TLB: Translation lookaside buffer,地址转译缓冲。用于加快逻辑地址与物理地址转译速度的缓冲区。有硬件实现和软件实现两种。 25 | 26 | -------------------------------------------------------------------------------- /doc/Checkpoints.md: -------------------------------------------------------------------------------- 1 | Checkpoint在rocksdb中提供了一种 给运行中的数据库在一个独立的文件夹中生成快照的能力。checkpoint可以当成一个特定时间点的快照来使用,可以用只读模式打开,用于查询该时间点的数据,或者以读写模式打开,作为一个可写的快照使用。checkpoint可以用于全量和增量备份。 2 | 3 | checkpoint功能使得Rocksdb有能力为给定的Rocksdb数据库在一个特定的文件夹创建一个一致性的快照。如果快照跟原始数据在同一个文件系统,SST文件会以硬链接形式生成,否则,SST文件会被拷贝。MAINFEST文件和CURRENT文件会被拷贝。另外,如果有多个列族,在checkpoint开始和结束的时间段内的日志文件(WAL)会被拷贝,以保证提供一个跨列族的一致性的快照。 4 | 5 | 生成checkpoint(逻辑意义上的checkpoint)前,一个Checkpoint(cpp代码层面意义上的)对象需要被创建。API如下: 6 | 7 | ```cpp 8 | Status Create(DB* db, Checkpoint** checkpoint_ptr); 9 | ``` 10 | 11 | 给出Checkpoint对象和一个目录,CreateCheckpoint函数会在目标文件夹创建一个数据库的一致性的快照。 12 | 13 | ```cpp 14 | Status CreateCheckpoint(const std::string& checkpoint_dir); 15 | ``` 16 | 17 | 这个目录不应该是已经存在的,他会被这个API创建。目录需要时绝对路径。checkpoint可以被当做一个只读的DB备份,或者可以被当做一个独立的DB实例打开。当以读写模式打开,SST文件会继续以硬链接存在,只有当这些文件被淘汰的时候,这些链接才会被删除。当用户用完这个快照,用户可以删除这个目录,以删除这个快照。 18 | 19 | 在MyRocks,Checkpoints被用来做在线备份。MyRocks是Mysql使用RocksDB做存储引擎的一个分支(基于Rocksdb的Mysql)。 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /doc/Tailing-Iterator.md: -------------------------------------------------------------------------------- 1 | 自2.7版本之后,rocksdb开始支持一种特殊的迭代器(tailing iterator,尾部迭代器),该迭代器经过优化,用于处理一种特殊的需求:新数据一旦被加入到数据库,就会被尽可能快的被读取到。他的主要功能包括: 2 | 3 | - 尾部迭代器不会创建快找,它可以用于读取最新加入的数据(与此对比,普通的迭代器不会看到任何在他创建后新加入的数据) 4 | - 针对序列化读进行特别优化——这可以避免某些潜在的昂贵的SST文件和不可变memtable的搜索。 5 | 6 | 在创建迭代器的时候,设置ReadOptions::tailing为true就可以打开这个功能了。注意,目前尾部迭代器只支持正向移动(也就是说不支持Prev和SeekToLast操作是不支持的) 7 | 8 | 并不是所有的新数据都能保证被一个尾部迭代器读取到。Seek或者SeekToFirst操作可以被认为是在一个隐式创建的快照上操作——任何在这之后的写操作都可能,但是不能被保证会被找到。 9 | 10 | ## 实现细节 11 | 12 | 一个尾部迭代器提供了两个内部迭代器的合并视图: 13 | 14 | - 一个可变迭代器,只用于访问当前的mentable内容。 15 | - 一个不可变迭代器,用于读取SST文件的数据以及不可变memtable的数据。 16 | 17 | 上面这些内部迭代器都是通过声明kMaxSequenceNumber来创建的,有效地禁止基于内部序列号的过滤以及允许访问这些迭代器创建之后的数据。更进一步,每个尾部迭代器都追踪数据库的状态变更(比如memtanble刷盘以及压缩)以及在其发生的时候废弃某些内部迭代器。这使之总是能保持最新数据。 18 | 19 | 由于SST文件以及不可变memtable不会修改,一个尾部迭代器总可以通过只在可变迭代器上进行seek操作来完成。为此,他会维护不可变迭代器覆盖的(prev_key, current_key]的区间(换句话说,如果有一个key k,他要么在sst文件,要么在不可变memtable)。因此,当seek被调用,并且目标在区间内,那么不可变迭代器就已经在正确的位置,并且不必再移动。 20 | 21 | 22 | -------------------------------------------------------------------------------- /doc/SeekForPrev.md: -------------------------------------------------------------------------------- 1 | # SeekForPrev API 2 | 3 | 从4.13开始,RocksDB新加了一个Iterator::SeekForPrev()调用。这个新的API与seek不同,允许查找小于或者等于目标key的最后一个key。 4 | 5 | ```cpp 6 | // Suppose we have keys "a1", "a3", "b1", "b2", "c2", "c4". 7 | auto iter = db->NewIterator(ReadOptions()); 8 | iter->Seek("a1"); // iter->Key() == "a1"; 9 | iter->Seek("a3"); // iter->Key() == "a3"; 10 | iter->SeekForPrev("c4"); // iter->Key() == "c4"; 11 | iter->SeekForPrev("c3"); // iter->Key() == "c2"; 12 | 13 | ``` 14 | 15 | SeekForPrev的行为基本就是如下: 16 | 17 | ```cpp 18 | Seek(target); 19 | if (!Valid()) { 20 | SeekToLast(); 21 | } else if (key() > target) { 22 | Prev(); 23 | } 24 | ``` 25 | 26 | 事实上,这个API会做更多的事情。其中一个例子就是,加入我们有key:"a1", "a3", "a5", "b1",然后我们打开了prefix_extractor,使用第一个byte做前缀。如果我们希望找到小于等于"a6"的最后一个key。上面的代码不使用SeekForPrev,是不能正确给出结果的。由于在Seek("a6")之后Prev(),迭代器会进入一种无效状态。但是现在,你只需要调用SeekForPrev("a6")就能得到"a5" 27 | 28 | 同时,SeekforPrev API在前缀模式对Prev操作提供内部支持。现在,Next和Prev可以在前缀模式混用了。感谢SeekForPrev 29 | 30 | -------------------------------------------------------------------------------- /doc/Time-to-Live.md: -------------------------------------------------------------------------------- 1 | ## RocksDB可以打开生存时间(TTL)支持 2 | 3 | # 使用场景 4 | 5 | 这个API可以用于插入一个KV对的时候就需要他们在一个不那么严格的TTL时间内被删除的场景,他保证插入的键值对会在db里保存至少ttl时间,然后db会尽最大努力,在炒锅ttl时间后尽快删除他们。 6 | 7 | # 行为 8 | 9 | - TTL单位为秒 10 | - (int32_t)Timestamp(creation)在调用Put的时候会在内部被追加到value的后面 11 | - TTL值的过期操作只在压缩过程中触发:(Timestamp+ttl=5的时候被删除 15 | - 打开的时候如果read_only=true会用通常的只读模式打开。压缩器不会被触发(不管是手动的还是自动的),所以没有过期项被移除。 16 | 17 | # 限制 18 | 19 | 不声明或者传入非正的TTL会使其其行为等同于TTL=无限 20 | 21 | # !!警告!! 22 | 23 | - 直接调用DB::Open来重新打开一个通过这个API创建的数据库会得到一个错误的值(有timestamp在最后追加),并且这次打开没有ttl效果,所以请确保总是用这个API打开db 24 | - 如果传入一个小的正数TTL,请小心,因为整个数据库都会快速的被删除。 25 | 26 | # API 27 | 28 | 定义于 29 | 30 | ```cpp 31 | static Status DBWithTTL::Open(const Options& options, const std::string& name, StackableDB** dbptr, int32_t ttl = 0, bool read_only = false); 32 | ``` 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /doc/Atomic-flush.md: -------------------------------------------------------------------------------- 1 | 如果DB option的`atomic_flush`为true,则RocksDB支持多列族原子落盘。落盘多个列族的执行结果会被写入到MANIFEST文件,使用`all-or-nothing`(要么全部,要么没有)保障(逻辑上的)。使用原子落盘,要么所有,要么没有一个,所有列族的mentalbe都持久化到sst文件,然后添加到数据库。 2 | 3 | 如果多个列族的数据希望保持一致性,就可以使用这个功能。例如,想想一下,有一个元数据列族`meta_cf`,有一个数据列族`data_cf`,每次我们写入一个新的记录到`data_cf`,我们也会写入到元数据`meta_cf`。`meta_cf`和`data_cf`必须原子化落盘。如果他们其中一个落盘了,但是另一个没有,那么数据就无法保持一致性了。原子化落盘提供一个好的保障。假设在某个特定事件,kv1在`meta_c`的memtable而kv2在`data_cf`的memtable。一次两个列族的原子化落盘后,如果成功,kv1和kv2都会持久化。否则他们都不存在于数据库。 4 | 5 | 由于原子化落盘会穿越`write_thread`,我们可以保证在批量写的过程中没有落盘发生。 6 | 7 | 打开、关闭原子罗盘非常简单,使用DB options即可。如果希望以原子落盘方式打开DB, 8 | 9 | ```cpp 10 | Options options; 11 | ... // Set other options 12 | options.atomic_flush = true; 13 | DBOptions db_opts(options); 14 | DB* db = nullptr; 15 | Status s = DB::Open(db_opts, dbname, column_families, &handles, &db); 16 | 17 | ``` 18 | 19 | 目前,在手动落盘的时候,用户需要负责声明原子落盘的列族。 20 | 21 | ```cpp 22 | w_opts.disable_wal = true; 23 | db->Put(w_opts, cf_handle1, key1, value1); 24 | db->Put(w_opts, cf_handle2, key2, value2); 25 | FlushOptions flush_opts; 26 | Status s = db->Flush(flush_opts, {cf_handle1, cf_handle2}); 27 | ``` 28 | 在RocksDB内部触发的自动落盘,简单起见,我们目前简单地把DB内的所有列族都原子落盘 29 | 30 | 31 | -------------------------------------------------------------------------------- /doc/Managing-Disk-Space-Utilization.md: -------------------------------------------------------------------------------- 1 | # 概述 2 | 3 | 我们有能力使一个RocksDB实例,或者多个实例聚合在一起, 的磁盘使用量,保持在一定数值以下。在一些一个文件系统被多个应用使用,并且其他应用系统需要与预先增长的数据库隔离的情况,是非常有用的。 4 | 5 | 追踪硬盘空间使用以及,可选的,限制数据库大小,是通过rocksdb::SstFileManager来实现的。他通过调用NewSstFileManager来生成,返回的对象被赋值给DBOptions::sst_file_manager。可以在多个DB实例之间共享一个SstFileManager。 6 | 7 | # 使用 8 | 9 | ## 追踪db大小 10 | 11 | 通过调用SstFileManager::GetTotalSize(),调用者可以知道DB使用的总磁盘空间。他返回所有SST文件使用的总大小。WAL文件不会被包括进去。如果同一个SstFileManager被用于多个DB实例,GetTotalSize会返回所有实例的总大小。 12 | 13 | ## 限制DB大小 14 | 15 | 通过调用SstFileManager::SetMaxAllowedSpaceUsage()以及,可选的,SstFileManager::SetCompactionBufferSize(),可以限制磁盘空间使用的方式。两个函数都接受一个参数,该参数以byte为单位,声明希望使用的尺寸。前者设置一个硬性DB大小限制,后者声明在决定是否压缩前,内部需要保留的空间。 16 | 17 | 设置最大的DB大小可以从下面几方面影响DB行为。 18 | 19 | - 每当一个新的SST文件通过落盘或者压缩被创建,SstFileManager::OnAddFile() 都会被调用,用于更新总共被使用的大小。如果这回导致总大小大于限制,ErrorHandler::bg_error_ variable会被设置到Status::SpaceLimit(),并且创建该SST文件的DB实例会进入只读模式。更多信息,参考[后台错误处理]() 20 | - 开始一个压缩前,RocksDB会检查是否有足够的空间用来创建输出的SST文件。这是通过调用SstFileManager::EnoughRoomForCompaction()来完成的。这个函数用一种保守的方式估计输出的大小为所有输入的SST文件的总大小。如果设置有压缩buffer,还会加上这个buffer,得到的输出结果会大于SstFileManager::SetMaxAllowedSpaceUsage()设置的值,压缩不会被允许执行。压缩线程会休眠1秒之后,把列族重新加入到压缩队列。所以说这是非常有用的压缩比率阈值。 21 | 22 | -------------------------------------------------------------------------------- /doc/Write-Buffer-Manager.md: -------------------------------------------------------------------------------- 1 | 写缓冲管理器帮助用户控制多个列族及DB实例中的memtables的总的内存使用。通过这个,用户可以实现: 2 | 3 | - 尝试限制多个列族和DB实例总的memtable使用量到一定的阈值下。 4 | - 允许memtable使用块缓存 5 | 6 | 写缓冲管理器跟rate_limiter和sst_file_manager很相似。用户创建一个写缓冲管理器对象,然后把他传递给所有你希望控制总内存空间的列族和DB。参考write_buffer_manager.h中的注释来了解如何使用。 7 | 8 | # 限制memtalbe的总内存 9 | 10 | 一个内存显示量会在创建写缓冲管理器的时候被给出。RocksDB会尝试把总内存使用量控制在这个限制之下。 11 | 12 | 在5.6以及更高的版本,当你在导入数据的时候,如果总的可变memtable的大小超过限制的90%,对应列族的DB会触发一次落盘。如果实际的内存已经超过限制,即使总的可变memtable大小少于限制的90%,也会触发一次激进的落盘操作。在5.6版本之前,如果总的可变memtable超过限制,触发一次落盘。 13 | 14 | 在5.6以及更高的版本,所有在同一个管理器分配的内存都被计算入内,即使这些内存不是被memtable使用。在更早的版本,内存的计算是按照memtable实际使用的量来计算的。 15 | 16 | # 允许memtable使用块缓存 17 | 18 | 从5.6版本之后,用户可以配置RocksDB来消费memtable使用的内存,给块缓存使用。这个不管是否打开memtable内存限制,都会发生。 19 | 20 | 在大多数情况下,块缓存中实际使用的块,与块缓存中的数据块比起来,只是非常小的一个比例,所以当用户打开这个功能,块缓存容量会同时被块缓存和memtable使用。如果用户同时还打开了cache_index_and_filter_blocks,那么RocksDB中三个主要的内存使用都会被一个容量覆盖。 21 | 22 | 这里解释它是怎么实现的。没分配给memtable 1MB的内存,WriteBufferManager会放一个假的1MB项到块缓存,这样块缓存就可以正确地追踪实际使用的大小,然后淘汰块来获取必要的空间。对于memtable缩减的时候使用的空间,WriteBufferManager不会立即删除假的缓存块,他会在内存使用量显著下降的时候,缓慢地移除他们。这是因为memtable的内存使用总是在浮动的,我们不希望经常去打扰块缓存。 23 | 24 | 打开这个功能的方法: 25 | 26 | - 把你希望使用的块缓存传递给你即将使用的WriteBufferManager。 27 | - 还是要把你希望memtable使用的最大内存传递给WriteBufferManager。 28 | - 把数据块缓存和memtable内存的总大小,设置为块缓存的容量 29 | 30 | -------------------------------------------------------------------------------- /doc/WelcomeToRocksDB.md: -------------------------------------------------------------------------------- 1 | # 欢迎使用RocksDB 2 | RocksDB是使用C++编写的嵌入式kv存储引擎,其键值均允许使用二进制流。由Facebook基于levelDB开发, 提供向后兼容的levelDB API。 3 | 4 | RocksDB针对Flash存储进行优化,延迟极小。RocksDB使用LSM存储引擎,纯C++编写。Java版本RocksJava正在开发中。参见[RocksJavaBasic](https://rocksdb.org.cn/doc/RocksJava-Basics.html)。 5 | 6 | RocksDB依靠大量灵活的配置,使之能针对不同的生产环境进行调优,包括直接使用内存,使用Flash,使用硬盘或者HDFS。支持使用不同的压缩算法,并且有一套完整的工具供生产和调试使用。 7 | 8 | ## 功能 9 | - 为需要存储TB级别数据到本地FLASH或者RAM的应用服务器设计 10 | - 针对存储在高速设备的中小键值进行优化——你可以存储在flash或者直接存储在内存 11 | - 性能随CPU数量线性提升,对多核系统友好 12 | 13 | ## LevelDB所没有的功能 14 | RocksDB增加了许多新功能,参考[features not in LevelDB](https://rocksdb.org.cn/doc/Features-Not-in-LevelDB.html) 15 | 16 | ## 开始 17 | 参考左边的菜单获得完整的内容表单。多数读者希望从开发者指南的 [概述]()和[基本操作]()开始。可以跟随[安装选项以及基础调优]()进行第一次安装设置。也可以看看[FAQ]()。还有一份[调优指南]() 给高级用户。 18 | 19 | ## 报告Bug或者寻求帮助 20 | 如果你有任何疑问,请着着这个[指南]()进行BUG上报或者寻求帮助 21 | 22 | ## BLOG 23 | 这个是我们的博客 [blog](rocksdb.org/blog) 24 | 25 | ## 项目历史 26 | 27 | -[RocksDB项目历史](http://rocksdb.blogspot.com/2013/11/the-history-of-rocksdb.html) 28 | -[深入了解RocksDB](https://www.facebook.com/notes/facebook-engineering/under-the-hood-building-and-open-sourcing-rocksdb/10151822347683920) 29 | 30 | ## 链接 31 | - [样例程序](https://github.com/facebook/rocksdb/tree/master/examples) 32 | - [官方博客](http://rocksdb.org/blog/) 33 | - [stack overflow:rocksdb](https://stackoverflow.com/questions/tagged/rocksdb) 34 | - [演讲](https://rocksdb.org.cn/doc/Talks.html) 35 | 36 | ## 联系我们 37 | - [开发者讨论组]() 38 | 39 | 文档License[here]() 40 | 41 | 42 | -------------------------------------------------------------------------------- /doc/Implement-Queue-Service-Using-RocksDB.md: -------------------------------------------------------------------------------- 1 | 许多用户使用RocksDB实现了一个队列服务。在这些服务里,每个队列,新加入的内容会携带一个更大的序列号ID,而删除的时候,会删除最小的序列号ID。用户通常按序列号递增的顺序读取队列。 2 | 3 | # Key编码 4 | 5 | 你可以简单的按照``编码他,queue_id是按照固定长度编码的,而sequence_id则使用大端编码。 6 | 7 | 迭代key的时候,用户可以创建一个迭代器,找到``,然后从该处开始迭代。 8 | 9 | # 旧数据删除问题 10 | 11 | 由于最旧的数据要被删掉,在每个`queue_id`开始的地方,可能会有很大数量的”墓碑“。结果而言,下面两个查询会变得很昂贵: 12 | 13 | - Seek(``) 14 | - 当你在一个`queue_id`的最后一个seq ID的时候,尝试调用Next() 15 | 16 | 为了解决这个问题,你可以记住每个queue_id对应的第一个和最后一个seq ID,然后不要跨越这个范围进行迭代。 17 | 18 | 另一个办法是,当你在queue_id里面迭代的时候,可以在迭代中设置一个结束的key,通过让`ReadOptions.iterate_upper_bound`指向````。我们鼓励你总是设置这个,不管你是否因为删除而导致了慢查询。 19 | 20 | # 检查一个queue_id的新seqID 21 | 22 | 如果一个用户处理完一个`queue_id`中的最后一个`sequenec_id`,然后拉取新插入的数据,只需要调用Seek(``),然后调用Next(),然后看下一个key是否为同一个`queue_id`。确保`ReadOptions.iterate_upper_bound`指向``以避免删除数据导致的慢查询。 23 | 24 | 如果你希望进一步优化这种使用场景,避免每次都二分搜索整个LSM树,考虑使用`TailingIterator`(或者在某些地方考虑`ForwardIterator`)[https://github.com/facebook/rocksdb/blob/master/include/rocksdb/options.h#L1235-L1241](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/options.h#L1235-L1241) 25 | 26 | # 更快回收删除数据的空间 27 | 28 | 队列服务是`CompactOnDeletionCollector`的一个很好的使用用例,他在压缩的时候会优先考虑更多的删除动作。给[这里](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/utilities/table_properties_collectors.h#L23-L27)定义的工厂类设置`immutableCFOptions::table_properties_collector_factories` 29 | 30 | -------------------------------------------------------------------------------- /doc/Choose-Level-Compaction-Files.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | Level压缩风格是rocksdb默认的压缩风格,所以他也是用户间最常用的压缩风格。有时候用户会好奇level压缩在每次压缩的时候如何选择哪个文件进行压缩。在这wiki中,我们会在这个议题里面深入聊一下,节省你读代码的时间 4 | 5 | # 步骤 6 | 7 | 从Level 0 到最高level,以选择第一个level,Lb,满足这个层的分数大于1,以此作为压缩的基础层。 8 | 9 | 决定压缩的输出层Lo = Lb + 1 10 | 11 | 根据不同的[压缩优先选项](http://rocksdb.org/blog/2016/01/29/compaction_pri.html),找到第一个需要被压缩的,优先级最高的文件。如果这个文件或者他在Lo的父母(就是那个key范围与他有交错的文件)正在被另一个压缩任务使用,跳过这个文件,使用第二高优先级的文件,知道找到**一个**候选文件。把这个文件加到压缩**输入**。 12 | 13 | 不断拓展输入,直到我们确定 输入文件和周围的文件 有一个“清晰分离的”边界。这保证了压缩过程中,没有部分key被丢失。例如,我们有五个文件,key范围如下: 14 | 15 | f1[a1 a2] f2[a3 a4] f3[a4 a6] f4[a6 a7] f5[a8 a9] 16 | 17 | 如果我们在第三步选择f3,然后在第四步,我们需要从{f3}拓展输入到{f2,f3,f4},因为f2和f3,f3和f4,的边界是连在一起的。之所以两个文件会有一个相同的用户key,是因为在rocksdb中,文件里的InternalKey会包含用户key原始信息,key的类型以及序列号。所以文件可能会存储多个用户key相同的InternalKey。因此,如果压缩发生,所有的用户key相同的InternalKey都需要被一起压缩。 18 | 19 | 检查当前的**输入**文件不会与任何已经在压缩的文件有交集。否则,尝试查找是否有可用的人工压缩。如果没有,放弃这次压缩挑选任务。 20 | 21 | 找出Lo上与**输入**文件有交集的文件,然后根据第四部的操作拓展他们,直到我们在Lo上有一个“清晰分离的”边界。如果他们中的任何文件正在被压缩,放弃这次压缩挑选任务。否则,把它们放入**output_level_inputs**。 22 | 23 | 一个可选的优化步骤。检查我们是否可以进一步增加Lb的输入文件,同时不用改变我们挑选的Lo的文件。如果这会导致Lb包含一些用户key的InternalKey,而又排除其他的相同用户key的InternalKey,我们同样会选择不拓展,导致,“一个不清晰的分离”。这可能在用户key分散在多个文件的时候发生。前面的描述可能让人困惑,所以我给一个例子出来解释这个优化。 24 | 25 | 考虑一下例子: 26 | 27 | Lb: f1[B E] f2[F G] f3[H I] f4[J M] 28 | Lo: f5[A C] f6[D K] f7[L O] 29 | 30 | 如果我们最开始在第三步选择f2,现在我们会压缩f2(**输入**)以及f6(第四步的**output_level_inputs**)。但是我们可以安全地压缩f2,f3和f6,而不用拓展输出level。 31 | 32 | **输入**的文件和**output_level_inputs**的文件就是这次level压缩的候选文件了。 33 | 34 | 35 | -------------------------------------------------------------------------------- /doc/Compaction-Filter.md: -------------------------------------------------------------------------------- 1 | Rocksdb提供一种方法,用于自定义逻辑在后台线程删除或者修改 key/value对。这在用于自定义垃圾回收的时候非常方便,比如根据TTL删除超时的key,或者在后台删除一个区间的key。同时这可以用于更新一个已经存在的key的值。 2 | 3 | 为了使用压缩过滤器,应用程序需要实现 rocksdb/compaction_filter.h 的 CompactionFilter接口,然后在ColumnFamilyOptions里面设置。或者,应用程序可以实现CompactionFilterFactory接口,他提供一个更加灵活的方法,用于针对不同的(子)压缩任务,创建不同的压缩过滤器。压缩过滤器工厂还可以根据CompactionFilter::Context变量知道一些压缩上下文信息(这是一个全量压缩还是一个人工触发压缩)。工厂可以根据上下文返回不同的压缩过滤器。 4 | 5 | ```cpp 6 | options.compaction_filter = new CustomCompactionFilter(); 7 | // or 8 | options.compaction_filter_factory.reset(new CustomCompactionFilterFactory()); 9 | ``` 10 | 11 | 这两种提供压缩过滤器的方式来自不同的线程安全需求。如果一个压缩过滤器提供给了RocksDB,他必须是线程安全的,因为多个子压缩可能并行运行,并且他们都是用同一个压缩过滤器实例。如果一个压缩过滤器工厂被提供,每个子压缩会调用工厂来构造一个压缩过滤器实例。因此可以保证每个压缩过滤器实例都只会被一个线程访问,并且压缩过滤器不需要保证线程安全。不过线程压缩器工厂可能被多个子压缩并行访问。 12 | 13 | 压缩过滤器在落盘的时候不会被调用,尽管理论来说,落盘也是一种压缩。 14 | 15 | 有两组API可以用于压缩过滤器的实现。Filter/FilterMergeOperand API提供一个简单的回调,用于告知压缩过程是否需要过滤一个key/value对。FilterV2 API通过允许修改value或者删除从当前的key开始的一个范围的key,来拓展基础API。 16 | 17 | 每当一个子压缩看到一个输入进来的新的key,并且他的value是普通value,他就调用压缩过滤器。根据压缩过滤器的结果: 18 | 19 | - 如果他决定保留这个key,什么也不会发生。 20 | - 如果他要求过滤这个key,其值会被替换成一个删除标记。注意,如果输出的压缩层是最底层,就不需要删除标记了。 21 | - 如果要求修改值,那么新的值将会替换旧的值 22 | - 如果返回kRemoveAndSkipUntil,要求删除一个范围的key,压缩会跳过,直到skip_until(意味着skip_until会是下一个可能的压缩输出的key)。这个有点操作有些偷巧,因为这里压缩器没有给跳过的key插入删除标记。这意味着旧的版本的key值会在结果里重新出现。另一方面,简单地丢弃这些key可能更加高效,如果应用程序知道这些key没有更老的版本,或者这些旧的key重新出现可以接受。 23 | 24 | 如果压缩的输入里面有同一个key的多个版本,压缩过滤器只会对最新的版本调用一次。如果最新的版本是一个删除标记,压缩过滤器也不会调用。然而,压缩过滤器可能针对一个已经删除的key被调起,如果这次压缩的输入里面没有把这个key标记为删除。 25 | 26 | 当合并被使用,压缩过滤器会在每个合并操作的时候调起。压缩过滤器的结果会在合并操作符之前被应用到合并操作。 27 | 28 | 如果在key/value对之后创建了快照,这个key/value对酒不能被过滤,而且压缩过滤器也不会被调起。 29 | 30 | -------------------------------------------------------------------------------- /doc/Single-Delete.md: -------------------------------------------------------------------------------- 1 | 这个功能会删除一个key的最后一个版本的值,但是旧的版本的值会不会重新出现,**不确定** 2 | 3 | # 基本使用 4 | 5 | SingleDelete是一个新的数据库操作。与传统的Delete操作不同,被删除的项,会在压缩的时候与数值一起被移除。因此,与Delete类似,SingleDelete删除一个key,但是有一个前提,就是这个key存在且没有被覆盖过。成功返回OK,否则返回非OK。如果key不存在,不会报错。如果key被覆盖过(多次调用Put),那么对一个key调用SingleDelete会导致未定义行为。只有当一个key在上次调用过SingleDelete之后只被Put过一次,SingleDelete才能正确执行。这个功能目前还是为了处理某些非常特别的工作的实验性质的。下面一段代码展示了如何使用SingleDelete: 6 | 7 | ```cpp 8 | std::string value; 9 | rocksdb::Status s; 10 | db->Put(rocksdb::WriteOptions(), "foo", "bar1"); 11 | db->SingleDelete(rocksdb::WriteOptions(), "foo"); 12 | s = db->Get(rocksdb::ReadOptions(), "foo", &value); // s.IsNotFound()==true 13 | db->Put(rocksdb::WriteOptions(), "foo", "bar2"); 14 | db->Put(rocksdb::WriteOptions(), "foo", "bar3"); 15 | db->SingleDelete(rocksdb::ReadOptions(), "foo", &value); // Undefined result 16 | SingleDelete API is also available in WriteBatch. Actually, DB::SingleDelete() is implemented by creating a WriteBatch with only one operation, SingleDelete, in this batch. The following code snippet shows the basic usage of WriteBatch::SingleDelete(): 17 | rocksdb::WriteBatch batch; 18 | batch.Put(key1, value); 19 | batch.SingleDelete(key1); 20 | s = db->Write(rocksdb::WriteOptions(), &batch); 21 | 22 | ``` 23 | 24 | ## 注意 25 | 26 | - 调用者必须却表SingleDelete只用在那些没有被Delete删除,或者使用Merge写入过的key。混合使用SingleDelete,Delete和Merge会导致未定义行为(其他的key不会受影响)。 27 | - SingleDelete与cuckoo哈希表不兼容,如果你把options.memtable_factory设置为[NewHashCuckooRepFactory](https://github.com/facebook/rocksdb/blob/522de4f59e6314698286cf29d8a325a284d81778/include/rocksdb/memtablerep.h#L325),那么你就无法使用SingleDelete 28 | - 不允许连续的SingleDelete 29 | - 考虑设置write_options.sync为true 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /doc/Compression.md: -------------------------------------------------------------------------------- 1 | # 哪些内容会被压缩? 2 | 3 | 在每个SST文件里,数据块和索引块会被分别压缩。用户可以指定压缩类型。过滤块不会被压缩 4 | 5 | # 配置 6 | 7 | 压缩配置是针对每个列族的。 8 | 9 | 使用options.compression来指定使用的压缩方法。默认是Snappy。我们相信LZ4总是比Snappy好的。我们之所以把Snappy作为默认的压缩方法,是为了与之前的用户保持兼容。LZ4/Snappy是轻量压缩,所以在CPU使用率和存储空间之间能取得一个较好的平衡。 10 | 11 | 如果你想要进一步减少存储的使用并且你有一些空闲的CPU,你可以尝试设置options.bottommost_compression来使用一个更加重量级的压缩。最底层会使用这个方式进行压缩。通常最底层会保存大部分的数据,所以用户通常会选择偏向空间的设定,而不是花费cpu在各个层压缩所有数据。我们推荐使用ZSTD。如果没有,Zlib是第二选择。 12 | 13 | 如果你有大量空闲CPU并且希望同时减少空间和写放大,把options.compression设置为重量级的压缩方法。我们推荐ZSTD,如果没有就用Zlib 14 | 15 | 通过一个已经撤销的遗留选项options.compression_per_level,你可以有更好的控制每一层的压缩方式。当这个选项被使用的时候,options.compression不会再被使用,但是options.bottommost_compression仍旧有效。但是我们相信很少有这个选项有用的情况。 16 | 17 | 请注意,当你针对不同不同的层设定不同的压缩方式,一些压缩里的“不那么重要的移动”,会跟压缩方式有冲突的操作,将不会被执行,文件会被改写为新的压缩方式。 18 | 19 | 指定的压缩方式总是同时应用在索引和数据块。你可以通过把BlockBasedTableOptions.enable_index_compression设置为false来关闭索引的压缩。 20 | 21 | # 压缩层和窗口大小设定 22 | 23 | 有些压缩类型支持不同的压缩层和窗口设定。你可以通过options.compression_opts设定他们。如果设定的类型不支持这些设定,他们不会生效 24 | 25 | # 字典压缩 26 | 27 | 用户可以选择使用一个存储在文件里的字典对每个最底层的SST文件进行压缩。在某些情况下,这可以省下一些空间。参考 [字典压缩]() 28 | 29 | # 压缩库 30 | 31 | 如果你选择一个库里面不存在的压缩方式,RocksDB会后退到无压缩。RocksDB会在日志的头部打印出支持的压缩方式: 32 | 33 | ``` 34 | 2017/12/01-17:34:59.368239 7f768b5d0200 Compression algorithms supported: 35 | 2017/12/01-17:34:59.368240 7f768b5d0200 Snappy supported: 1 36 | 2017/12/01-17:34:59.368241 7f768b5d0200 Zlib supported: 1 37 | 2017/12/01-17:34:59.368242 7f768b5d0200 Bzip supported: 0 38 | 2017/12/01-17:34:59.368243 7f768b5d0200 LZ4 supported: 1 39 | 2017/12/01-17:34:59.368244 7f768b5d0200 ZSTDNotFinal supported: 1 40 | 2017/12/01-17:34:59.368282 7f768b5d0200 ZSTD supported: 1 41 | ``` 42 | 43 | 通过检查日志,来发现可能出现的兼容性问题。 44 | 45 | -------------------------------------------------------------------------------- /doc/Rate-Limiter.md: -------------------------------------------------------------------------------- 1 | 使用RocksDB的时候,用户可能因为许多原因,希望控制最大写速度在一个范围。例如,闪存写的时候如果超过特定阈值,会引发严重的读延迟峰值。因为你已经在读这篇文章,我相信你已经知道为什么你需要一个限流器。事实上,RocksDB自带一个[限流器](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/rate_limiter.h),在大多数场景,应该都是够用了。 2 | 3 | # 如何使用 4 | 5 | 通过调用NewGenericRateLimiter创建一个RateLimiter对象,可以对每个RocksDB实例分别创建,或者在RocksDB实例之间共享,以此控制落盘和压缩的写速率总量。 6 | 7 | ```cpp 8 | RateLimiter* rate_limiter = NewGenericRateLimiter( 9 | rate_bytes_per_sec /* int64_t */, 10 | refill_period_us /* int64_t */, 11 | fairness /* int32_t */); 12 | ``` 13 | 14 | 参数: 15 | 16 | - rate_bytes_per_sec:通常这是唯一一个你需要关心的参数。他控制压缩和落盘的总速率,单位为bytes/秒。现在,RocksDB并不强制限制除了落盘和压缩以外的操作(如写WAL) 17 | - refill_period_us:这个控制令牌被填充的频率。例如,当rate_bytes_per_sec被设置为10MB/s然后refill_period_us被设置为100ms,那么就每100ms会从新填充1MB的限量。更大的数值会导致突发写,而更小的数值会导致CPU过载。默认数值100,000应该在大多数场景都能很好工作了 18 | - fairness:RateLimiter接受高优先级请求和低优先级请求。一个低优先级任务会被高优先级任务挡住。现在,RocksDB把来自压缩的请求认为是低优先级的,把来自落盘的任务认为是高优先级的。如果落盘请求不断地过来,低优先级请求会被拦截。这个fairness参数保证低优先级请求,在即使有高优先级任务的时候,也会有1/fairness的机会被执行,以避免低优先级任务的饿死。默认是10通常是可以的。 19 | 20 | 尽管令牌会以refill_period_us设定的时间按照间隔来填充,我们仍然需要保证一次写爆发中的最大字节数,因为我们不希望看到令牌堆积了很久,然后在一次写爆发中一次性消耗光,这显然不符合我们的需求。GetSingleBurstBytes会返回这个令牌数量的上限。 21 | 22 | 这样,每次写请求前,都需要申请令牌。如果这个请求无法被满足,请求会被阻塞,直到令牌被填充到足够完成请求。比如: 23 | 24 | ```cpp 25 | // block if tokens are not enough 26 | rate_limiter->Request(1024 /* bytes */, rocksdb::Env::IO_HIGH); 27 | Status s = db->Flush(); 28 | ``` 29 | 30 | 如果有需要,用户还可以通过SetBytesPerSecond动态修改限流器每秒流量。参考[include/rocksdb/rate_limiter.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/rate_limiter.h) 了解更多细节 31 | 32 | # 定制 33 | 34 | 对那些RocksDB提供的原生限流器无法满足需求的用户,他们可以通过继承[include/rocksdb/rate_limiter.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/rate_limiter.h) 来实现自己的限流器。 35 | 36 | -------------------------------------------------------------------------------- /doc/Persistent-Read-Cache.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | 很长一段时间,硬盘的意思都是数据存储的持久化介质。随着SSD的引入,我们现在有一个比传统磁盘快非常多的持久化介质,虽然他的擦写次数和容量会更小,是的我们可以有机会探索tiered存储架构。开源实现,如使用SSD闪盘缓存以及供服务器应用的,使用更好的disk作为tiered存储。RocksDB Persistent读缓存是一个尝试使用tiered存储架构来应对未知设备和操作系统的RocksdB生态系统。 4 | 5 | # Tiered存储 VS Tiered缓存 6 | 7 | RocksDB用户可以通过tiered存储部署的方式,或者使用tiered缓存部署的方式 从tiered存储架构获得好处。使用tiered存储方式,你可以分散LSM的多层持久化存储的内容。使用tiered缓存方式,用户可以使用更快的持久化介质作为读缓存,以服务于LSM的高频访问部分,并且增强RocksDB的性能。 8 | 9 | Tiered缓存在数据便携性方面有一些优势,因为缓存是性能的一个增强。存储部分可以在没有缓存的情况下继续工作。 10 | 11 | # 关键功能 12 | 13 | ## 硬件无关 14 | 15 | 这个持久化读缓存是一个通用实现,他不是对任何特定设备设计的。与针对不同的硬件进行设计不同,我们给用户留了一个机制,用来描述访问设备的最佳方式,并且IO方式也可以针对不同的描述进行配置。 16 | 17 | 写代码流程可以使用下面的公式描述: 18 | 19 | ``` 20 | { Block Size, Queue depth, Access/Caching Technique } 21 | ``` 22 | 23 | 读代码流程可以通过下列公式描述: 24 | 25 | ``` 26 | { Access/Caching Technique } 27 | ``` 28 | 29 | 块大小(Block Size)描述了读/写的大小。在SSD,这通常是擦出块大小。 30 | 31 | 队列深度(Queue depth)对应于设备能展现最优性能的地方。 32 | 33 | 访问/缓存技术(Access/Caching Technique)别用于描述访问设备的最佳方式。举个例子,使用直接IO,就适合特定设备/应用,而带缓冲的方式则适合其他。 34 | 35 | ## OS无关 36 | 37 | 持久化读缓存使用RocksDB抽象来构建,并且支持所有RocksDB支持的平台。 38 | 39 | ## 可插拔式 40 | 41 | 由于这是一个缓存实现,缓存可以,也可能不可以在重启的时候可用。 42 | 43 | # 设计以及实现细节 44 | 45 | 持久化读缓存的实现有三个基本原件。 46 | 47 | ## 块查找索引 48 | 49 | 这是一个可伸缩的内存哈希索引,把一个给定的LSM块地址映射到一个缓存记录定位器。缓存记录定位器帮助定位块数据在缓存的位置。缓存记录可以被描述为{file-id, offset, size }。 50 | 51 | ## 文件查找索引/LRU 52 | 53 | 这是一个可伸缩的内存哈希索引,允许基于LRU进行淘汰。这个索引把一个给定的文件描述符映射到他的引用对象的抽象。这个对象抽象可以被用于从缓存读取数据。当我们持久化缓存的空间不够的时候,我们淘汰最近最不常用的项。 54 | 55 | ## 文件分布 56 | 57 | 缓存以一系列文件的形式存储在文件系统。每个文件包含一系列的记录,这些记录包含对应RocksDB的LSM块的数据。 58 | 59 | # API 60 | 61 | 请参考下属链接找到公开API。 62 | 63 | [https://github.com/facebook/rocksdb/blob/master/include/rocksdb/persistent_cache.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/persistent_cache.h) 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /doc/RocksDB-Repairer.md: -------------------------------------------------------------------------------- 1 | # 概述 2 | 3 | 修复器允许在灾难之后, 在对一致性没有任何妥协的情况下,尽最大努力来修复尽可能多的数据。他不能保证把数据库恢复到一个抑制状态的时间点。 4 | 5 | # 使用 6 | 7 | 注意CLI命令使用默认选项来修复你的DB,并且只增加在SST文件中找到的列族。如果你需要指定选项,比如,自定义的比较器,或者有根据列族不同的选项,或者希望声明额外的列族集合,你需要自己编写相关代码。 8 | 9 | ## 自行编写 10 | 11 | 为了自己编写代码,调用 include/rocksdb/db.h 中声明的其中一个RepairDB函数 12 | 13 | ## CLI 14 | 15 | 对于命令行使用方式,首先构建ldb,我们的管理员CLI工具: 16 | 17 | ``` 18 | $ make clean && make ldb 19 | ``` 20 | 21 | 现在使用ldb的 repaire 子命令,指明你的DB。注意他打印info日志到stderr,所以你可能希望重定向他。这里我在一个./tmp目录下的DB运行它,在这里,我不小心删除了MANIFEST文件: 22 | 23 | ``` 24 | $ ./ldb repair --db=./tmp 2>./repair-log.txt 25 | $ tail -2 ./repair-log.txt 26 | [WARN] [db/repair.cc:208] **** Repaired rocksdb ./tmp; recovered 1 files; 926bytes. Some data may have been lost. **** 27 | Looks successful. MANIFEST file is back and DB is readable: 28 | $ ls tmp/ 29 | 000006.sst CURRENT IDENTITY LOCK LOG LOG.old.1504116879407136 lost MANIFEST-000001 MANIFEST-000003 OPTIONS-000005 30 | $ ldb get a --db=./tmp 31 | b 32 | ``` 33 | 34 | 注意lost/目录。他持有所有可能在恢复过程中丢失的数据文件。 35 | 36 | # 修复过程 37 | 38 | 修复过程分为4步: 39 | 40 | - 查找文件 41 | - 把日志转化为表 42 | - 导出metadata 43 | - 写描述符 44 | 45 | ## 查找文件 46 | 47 | 修复器遍历当前目录所有文件,然后根据他们的文件名进行分类。任何无法分类的文件都会被忽略 48 | 49 | ## 把日志转化为表 50 | 51 | 每个活跃的日志文件都会被重放。该文件中所有校验和不通过的节都会被跳过。我们有意保留一致的数据。 52 | 53 | ## 导出metadata 54 | 55 | 我们扫描所有的表,以计算: 56 | 57 | - 该表最小/最大值 58 | - 该表的最大序列号 59 | 60 | 如果我们无法扫描文件,我们忽略这个表 61 | 62 | ## 写描述符 63 | 64 | 我们生成描述符内容: 65 | 66 | - 日志数量被设置为0 67 | - 下一个文件编号被设置为 1 + 我们找到的最大文件编码 68 | - 最后序列号 被设置为在所有表中找到的最大的序列号 69 | - 压缩(compaction)指针被清理 70 | - 每个表文件都会加入到level0 71 | 72 | ## 可能的优化 73 | 74 | - 指定总大小,然后选择最接近的最大层M 75 | - 根据表的最大序列号对表排序 76 | - 对于每个表:如果他跟前面的表有交集,放在level 0,否则,放在level M 77 | - 我们可以提供选项来指定时间一致性恢复和不安全恢复(可能的话,忽略文件的校验和错误) 78 | - 存储每个标的元数据(最小,最大,最大序列号。。。)到标的元段以加快ScanTable 79 | 80 | -------------------------------------------------------------------------------- /doc/Simulation-Cache.md: -------------------------------------------------------------------------------- 1 | 模拟缓存(SimCache)可以帮助用户预测块缓存 在当前工作压力下,在一个特定的模拟(内存)大小下,的性能,比如,命中率,未命中率,而不用实际使用特定数量的内存。 2 | 3 | # 动机 4 | 5 | 可以帮助用户调优块缓存大小,以及确定他们使用块缓存的效率。同事,帮助理解在高速存储下的缓存性能。 6 | 7 | # 介绍 8 | 9 | SimCache的基本思想是,把普通的块缓存封装成一个使用目标模拟大小的,只有key的块缓存。当插入的时候,我们把key查到cache,但是值只会插入到普通缓存。这样,值得大小就同事对两个缓存有影响,这样我们模拟了一个特定大小的块缓存的行为,而不需要使用实际的内存,实际内存使用只涉及key的总大小。 10 | 11 | # 如何使用SimCache 12 | 13 | 由于SimCache是一个普通块缓存的封装。用户需要使用[NewLRUCache](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/cache.h)创建一个块缓存: 14 | 15 | ``` 16 | std::shared_ptr normal_block_cache = 17 | NewLRUCache(1024 * 1024 * 1024 /* capacity 1GB */); 18 | ``` 19 | 20 | 然后使用[NewSimCache](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/utilities/sim_cache.h)封装normal_block_cache,然后把SimCache设置为rocksdb::BlockBasedTableOptions的block_cache字段,并且生成options.table_factory: 21 | 22 | ``` 23 | rocksdb::Options options; 24 | rocksdb::BlockBasedTableOptions bbt_opts; 25 | std::shared_ptr sim_cache = 26 | NewSimCache(normal_block_cache, 27 | 10 * 1024 * 1024 * 1024 /* sim_capacity 10GB */); 28 | bbt_opts.block_cache = sim_cache; 29 | options.table_factory.reset(new BlockBasedTableFactory(bbt_opts)); 30 | ``` 31 | 32 | 最后,使用该选项打开DB。然后SimCache的HIT/MISS值可以分别通过sim_cache->get_hit_counter()和sim_cache->get_miss_counter()获得。可选的,如果你不希望存储sim_cache,并且你的Rocksdb版本大于v4.12,你可以通过[rocksdb::Statistic](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/statistics.h)滴答计数器SIM_BLOCK_CACHE_HIT和SIM_BLOCK_CACHE_MISS获取这些统计信息。 33 | 34 | # 内存使用 35 | 36 | 人们可能会担心SimCache实际的内存使用,大概包括: 37 | 38 | sim_capacity * entry_size / (entry_size + block_size), 39 | 40 | - 76 <= entry_size (key_size + other) <= 104 41 | - BlockBasedTableOptions.block_size = 4096。默认值,可配置 42 | 43 | 因此,默认SimCache的内存使用为**sim_capacity * 2%**。 44 | 45 | 46 | -------------------------------------------------------------------------------- /doc/FIFO-compaction-style.md: -------------------------------------------------------------------------------- 1 | FIFO压缩风格是最简单的压缩策略。很适合用于保存不是那么重要的事件日志数据(例如查询日志)。他会周期性删除旧的数据,所以基本来说,他是一种TTL压缩风格。 2 | 3 | 在FIFO压缩里,所有的文件都在Level 0。当数据的总大小超过CompactionOptionsFIFO::max_table_files_size配置的大小时,我们删除最老的表文件。这意味着数据的写放大总是1(还有WAL的写放大) 4 | 5 | 目前,CompactRange函数只是强制触发压缩,然后如果有需要,删除旧的表文件。他忽略函数的参数(开始和结束key) 6 | 7 | 由于我们不会重写键值对,我们也不会对key执行压缩过滤器方法。 8 | 9 | 请小心使用FIFO压缩风格。与其他压缩风格不同,他可能在不通知用户的情况下删除数据。 10 | 11 | # 压缩 12 | 13 | FIFO压缩可能会导致大量L0文件。查询可能会变得很慢,因为最坏情况下,我们可能需要搜索所有的这些文件。即使是bloom过滤器也可能无法得到一个好的性能。加入有1%的假阳性结果,1000个L0文件平均会导致10个假阳性结果,然后在最坏情况下,每个查询会生成10个IO。用户可以选择使用更多的bloom位来减少假阳性结果,但是他们需要为此付出更多的内存。在某些情况下,bloom过滤器检查的CPU开支可能会过高。 14 | 15 | 为了解决这个问题,用户可以选择允许一些轻量压缩发生。这可能会让写IO变成两倍,但是可以显著减少L0的文件。某些时候对于用户来说是合理的权衡。 16 | 17 | 这个功能在5.5版本中引入。用户可以通过CompactionOptionsFIFO.allow_compaction = true来打开这个功能。他会尝试选择至少level0_file_num_compaction_trigger个从memtable落盘的文件,然后合并他们。 18 | 19 | 特别的,我们总是从最新的level0_file_num_compaction_trigger文件开始,尝试包含尽可能多的文件进行压缩。我们使用 total_compaction_size / (number_files_in_compaction - 1) 计算已经压缩的每个文件的大小。我们总是以保证这个数字最小,并且不多于options.write_buffer_size,这两个条件来挑选文件。在一个典型的工作场景,他总会压缩level0_file_num_compaction_trigger个刚落盘的文件。 20 | 21 | 例如,如果level0_file_num_compaction_trigger = 8,每个罗盘文件为100MB。那么只要达到了8个文件,他们会被压缩为一个800MB的文件。然后等我们有了8个新的100MB文件,他们会被压缩成第二个800MB的文件,以此类推。最终我们有一系列800MB,但是不超过8100MB的文件。 22 | 23 | 请注意,由于最老的文件被压缩了,FIFO删除的文件也变大了,所以可能排序好的数据会比没有压缩的数据略微少一点。 24 | 25 | ## FIFO带TTL压缩 26 | 27 | 一个新的,名为FIFO带TTL压缩的功能在RocksDB5.7被引入。 28 | 29 | 目前,FIFO压缩目前只考虑文件总大小,比如:如果db的大小超过compaction_options_fifo.max_table_files_size,丢弃最老的文件,一个个地删除知道总大小小于阈值。有时候,生产环境上打开这个,会随着有机增长把生产环境搞乱。 30 | 31 | 一个新的选项,compaction_options_fifo.ttl,被引入,用来删除超过TTL的SST文件。这个功能允许用户根据时间丢弃文件而不总是根据大小来,比如说,丢弃所有一周前或者一个月前的数据。 32 | 33 | 限制: 34 | 35 | - 这个选项目前只在max_open_files为-1时,对基于块的表格式使用。 36 | - FIFO带TTL仍旧在配置的大小范围内工作,比如说,如果观察到TTL无法让文件总数量少于配置的大小,RocksDB会暂时下降到基于大小的FIFO删除。 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /doc/Background-Error-Handling.md: -------------------------------------------------------------------------------- 1 | 当前rocksdb中,默认情况下在写操作中任何一个错误(写WAL,Memtable落盘)都会导致db实例进入只读模式,之后的用户写操作都不会被接受。变量ErrorHandler::bg_error 会被设置到对应的失败Status。DBOptions::paranoid_checks选项控制Rocksdb在检查和确认错误的强度。默认值为true。下面的表展示不同场景的错误以及对db的潜在影响。 2 | 3 | |错误原因|BG_ERROR_被设置的时机| 4 | |---|---| 5 | |同步 WAL (BackgroundErrorReason::kWriteCallback)|总是| 6 | |Memtable插入失败 (BackgroundErrorReason::kMemTable)| 总是| 7 | |Memtable落盘(BackgroundErrorReason::kFlush)|DBOptions::paranoid_checks为true| 8 | |SstFileManager::IsMaxAllowedSpaceReached() 报告memtable落盘的时候已经达到最大空间(BackgroundErrorReason::kFlush)|总是| 9 | |SstFileManager::IsMaxAllowedSpaceReached() 报告压缩期间到达最大空间 (BackgroundErrorReason::kCompaction) |总是| 10 | |DB::CompactFiles (BackgroundErrorReason::kCompaction) |DBOptions::paranoid_checks为true| 11 | |后台压缩 (BackgroundErrorReason:::kCompaction)| DBOptions::paranoid_checks为true| 12 | |写(BackgroundErrorReason::kWriteCallback)|DBOptions::paranoid_checks为 true| 13 | 14 | # 探测 15 | 16 | 一旦数据库实例进入只读模式,下面的前端操作在所有后续调用中都会返回错误: 17 | 18 | - DB::Write, DB::Put, DB::Delete, DB::SingleDelete, DB::DeleteRange, DB::Merge 19 | - DB::IngestExternalFile 20 | - DB::CompactFiles 21 | - DB::CompactRange 22 | - DB::Flush 23 | 24 | 返回的Status会包含具体的错误码,子码以及严重性。错误的严重性可以通过调用Status::severity()得到。有四个严重性等级: 25 | 26 | - Status::Severity::kSoftError——这个严重性等级的错误,不会阻止DB的写入,但是他意味着db在一个降级模式。后台压缩可能没有按时执行。 27 | - Status::Severity::kHardError——DB已经处于只读模式,但是一旦错误被修复,就可以恢复成读写模式 28 | - Status::Severity::kFatalError——DB在只读模式。只有关闭DB,修复错误的根源,然后重新打开db,才有可能回复。 29 | - Status::Severity::kUnrecoverableError——这是最高等级的严重性,并且意味着数据库将无法工作。可能可以关闭然后重新打开db,但是数据库的数据将不能保证正确。 30 | 31 | 除了上面提到的,在发生后台错误的时候,一个通知回调Status::Severity::kUnrecoverableError会被尽可能快地调用。 32 | 33 | # 恢复 34 | 35 | 有两种办法从后台错误中恢复而不需要关闭数据库: 36 | 37 | - EventListener::OnBackgroundError回调如果认为当前的错误不是那么严重,不需要停止后续的写错做,那么他可以修改错误状态。可以通过设置bg_error参数来完成这个工作。这么做可能会有风险,因为rocksdb可能无法保证DB的一致性。修改之前,检查BackgorundErrorReason以及错误的严重性。 38 | - 调用DB::Resume()来恢复DB并且使之进入读写模式。这个方法会清理错误,清理所有废弃的文件,然后重启后台落盘以及压缩任务。目前,他只支持从压缩过程中产生的后台错误。未来,我们会加入更多的场景 39 | 40 | -------------------------------------------------------------------------------- /doc/How-we-keep-track-of-live-SST-files.md: -------------------------------------------------------------------------------- 1 | 在Rocksdb,LSM树包含一个在文件系统上的SST文件的列表,以及一些WAL日志。每次压缩之后,压缩输出文件被加入到列表,而输入文件则被删除。然而,输入文件并不是一定能满足立即删除的条件,因为有些get或者迭代器可能会需要保留这些文件,直到他们的操作结束,或者迭代器被释放。在这一页剩余的内容中,我们介绍我们是如何维护这些信息的。 2 | 3 | LSM树的文件列表在一个名为version的数据结构内保存。在一次压缩或者落盘的最后,一个新的version会被创建,以存储更新后的LSM树。在同一时间,只有一个“CURRENT”版本表示最新的数据的LSM树的文件。新的get请求或者新的迭代器 在整个读取过程或者迭代的生命周期内,会使用当前版本。所有被get或者迭代器使用的版本需要被保留。一个过期版本,没有被任何get或者迭代器使用,就需要被丢弃。所有没有被任何其他版本使用的文件需要被删除。比如: 4 | 5 | 如果我们从一个带有三个文件的版本开始: 6 | 7 | v1 = {f1,f2,f3}(current) 8 | 磁盘的文件:f1,f2,f3 9 | 10 | 然后现在有一个迭代器使用这个创建: 11 | 12 | v1 = {f1,f2,f3}(current,iterator1使用) 13 | 磁盘的文件:f1,f2,f3 14 | 15 | 现在一个落盘发生,增加了f4,一个新的版本被创建: 16 | 17 | v2={f1, f2, f3, f4} (current) 18 | v1={f1, f2, f3} (iterator1使用) 19 | 磁盘的文件:f1,f2,f3,f4 20 | 21 | 现在一个压缩发生了,压缩了f2,f3和f4,生成新的文件 f5,这是一个新的版本v3被创建: 22 | 23 | v3={f1, f5} (current) 24 | v2={f1, f2, f3, f4} 25 | v1={f1, f2, f3} (iterator1使用) 26 | 磁盘的文件:f1,f2,f3,f4,f5 27 | 28 | 现在v2既不是最新的数据,也没有被任何人使用,所以他可以被删除,f4也是。而v1仍旧不能被删除,因为他仍旧被iterator1需要: 29 | 30 | v3={f1, f5} (current) 31 | v1={f1, f2, f3} (iterator1使用) 32 | 磁盘的文件: f1, f2, f3, f5 33 | 34 | 假设现在iterator被销毁: 35 | 36 | v3={f1, f5} (current) 37 | v1={f1, f2, f3} 38 | 磁盘的文件: f1, f2, f3, f5 39 | 40 | 现在v1既没人用,也不是最新的数据,所以他可以被删除,同时还有f2和f3: 41 | 42 | v3={f1, f5} (current) 43 | 磁盘的文件: f1, f5 44 | 45 | 这个逻辑通过引用计数来实现。SST文件和version都有一个引用计数。当我们创建一个version的时候,我们增加所有文件的引用计数。如果一个version不在被需要,该version的所有文件都会减少他们的引用计数。如果一个文件的引用计数降低到0,这个文件就可以被删除。 46 | 47 | 类似的,每个version都有一个引用计数。当一个version被创建,他就是一个最新的版本,所以他有引用计数1.如果这个版本不在是最新的,他的引用计数就会减少。任何人需要在这个版本上工作,都会使其引用计数加一,并且在用完之后减一。当一个版本的引用计数为0,他就应该被移除。一个version要么是最新的,要么有人在使用,这样他的引用计数就不是0,他就需要被保留。 48 | 49 | 有时,一个读者直接保留一个version的引用,比如压缩的时候保留源version。更多的时候,一个读者通过一个名为super version的数据结构间接持有version,他会持有memtable的引用计数以及一个version —— 一个完整的DB视图。一个读者只需要增加,然后减少一个引用计数,而super version会持有version的引用计数。这同时使得更进一步的优化,在大多数时候避免为引用计数上锁,变得可能。参考这个[博客]()。 50 | 51 | Rocksdb使用VersionSet数据结构维护所有的version数据结构,同时用于记住谁是“current”版本。由于每个列族有一个独立的LSM树,他也有自己的version列表,以及一个名为“current”的版本。但是每个DB只有一个VersionSet,为所有列族维护version。 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /doc/Manual-Compaction.md: -------------------------------------------------------------------------------- 1 | 通过DB::CompactRange 或者 DB::CompactFiles,可以手动触发压缩。这是给高级用户开发自定义压缩策略使用的,包括但不限于以下使用方法: 2 | 3 | - 通过在读取大量数据之后,把文件压缩到最底层,来优化读多写少的工作压力。 4 | - 强制数据进入压缩过滤器,以固定数据。 5 | - 迁移到一个新的压缩配置。例如,如果修改了层数量,可以调用`CompactRange`来把数据压缩到最底层,然后把文件移动到目标层。 6 | 7 | 下面的例子展现如何使用这些API。 8 | 9 | ```cpp 10 | Options dbOptions; 11 | 12 | DB* db; 13 | Status s = DB::Open(dbOptions, "/tmp/rocksdb", &db); 14 | 15 | // Write some data 16 | ... 17 | Slice begin("key1"); 18 | Slice end("key100"); 19 | CompactRangeOptions options; 20 | 21 | s = db->CompactRange(options, &begin, &end); 22 | ``` 23 | 24 | 或者 25 | 26 | ```cpp 27 | CompactionOptions options; 28 | std::vector input_file_names; 29 | int output_level; 30 | ... 31 | Status s = db->CompactFiles(options, input_file_names, output_level); 32 | 33 | ``` 34 | 35 | 36 | # CompactRange 37 | 38 | begin和end参数定义需要压缩的key的范围。根据db使用的压缩风格而有不同的行为。在universal和FIFO压缩风格,begin和end参数会被忽略,所有文件都会被压缩。另外,每一层的文件都会被压缩,并且留在本层。对于leveled压缩风格,所有包含有key范围的文件都会被压缩到最底层。如果begin或者end为NULL,这意味着使用第一个的key或者最后一个的key。 39 | 40 | 如果多于一个线程调用了人工压缩,只有一个会真正被调度,而其他线程会等待已经调度的压缩完成。如果CompactRangeOptions::exclusive_manual_compaction被设置为true,调用会禁止自动压缩工作的调度,然后等待已经开始的自动压缩工作停止。 41 | 42 | CompactRangeOptions支持以下选项: 43 | 44 | - CompactRangeOptions::exclusive_manual_compaction。 为true时,如果人工压缩开始了,就不会有其他压缩进行了。默认为true 45 | - CompactRangeOptions::change_level,CompactRangeOptions::target_level。 两个选项一起决定压缩后的文件会放在那一层。如果target_level为-1,压缩文件会移动到层号最小的,max_bytes能满足放下所有文件的层。中间的层必须为空。例如,如果文件初始化被压缩到L5,然后L2是最小的、能存放所有数据的层,那么如果L3和L4是空的,他们会被放在L2,或者如果L3不是空的,就放在L4。如果target_level为正,压缩后的文件会放在那个层,并且中间层为空。如果中间的层没有空的,那么压缩的文件会被放在他们原来的层。 46 | - CompactRangeOptions::target_path_id 压缩输出会被放在options.db_paths[target_path_id]目录。 47 | - CompactRangeOptions::bottommost_level_compaction,当设置为BottommostLevelCompaction::kSkip,或者设置为 BottommostLevelCompaction::kIfHaveCompactionFilter ,并且这个列族有压缩过滤器被定义,那么最底层的文件不会被压缩 48 | 49 | ## CompactFiles 50 | 51 | 这个接口会压缩所有输入文件到一系列输出文件,然后放在output_level中。输出文件的大小取决于数据的大小以及CompactionOptions::output_file_size_limit的设定。这个API在ROCKSDB_LITE里面不支持。 52 | 53 | -------------------------------------------------------------------------------- /doc/Statistics.md: -------------------------------------------------------------------------------- 1 | DB的Statistics提供一个历史累计数据。他提供来自DB属性以及[性能和IO状态上下文]()的不同的功能:历史性能累计统计,DB属性提供当前状态;DB统计提供一个针对所有操作的聚合的试图,而性能和IO状态上下文允许我们查看每个单独的操作。 2 | 3 | # 使用 4 | 5 | 函数CreateDBStatistics()创建一个统计对象。 6 | 7 | 这里有一个例子展示如何把它传递给DB: 8 | 9 | ```cpp 10 | Options options; 11 | options.statistics = rocksdb::CreateDBStatistics(); 12 | ``` 13 | 14 | 技术上来说,你可以创建一个统计对象然后传递给多个DB。然后统计对象会包含所有这些DB的数据的聚合结果。注意,在跨多个DB的时候,有些统计值是未定义的,因此他们没有任何意义,比如说"rocksdb.sequence.number" 15 | 16 | 高级用户可以自行实现自己的统计类。参考最后一章节 17 | 18 | # 统计等级和性能损耗 19 | 20 | 统计带来的损耗通常很小,但是不可以忽视。我们通常会观察到5%~10%的损耗。 21 | 22 | 统计都使用原子整数来实现(原子递增)。更进一步,统计测量时间间隔需要发起调用来获得当前时间。原子递增和及时函数都会带来损耗,损耗的大小根据平台有所差异。 23 | 24 | 我们有三个统计等级 kExceptDetailedTimers, kExceptTimeForMutex 和 kAll。 25 | 26 | - kAll:收集所有数据,包括计算互斥操作的耗时。如果获取时间在你所在的平台非常昂贵,他会极大减小很多线程的容量,特别是写线程。 27 | - kExceptTimeForMutex:收集所有数据,但是不收集互斥锁内部时间的计数器。rocksdb.db.mutex.wait.micros计数器不会被测量。通过测量这个计数器,我们在DB互斥中调用时间函数。如果时间函数比较慢,这会显著减小写吞吐。 28 | - kExceptDetailedTimers:收集所有信息,除了在互斥锁内的时间**和**压缩的时间。 29 | 30 | # 获取统计数据 31 | 32 | ## 统计类型 33 | 34 | 有两种类型的统计数据,滴答器和矩形图。 35 | 36 | 滴答器类型以64bit无符号整形表示。数值不会减小或者重置。滴答统计被用于测量计数器(例如 "rocksdb.block.cache.hit"),累计字节(例如"rocksdb.bytes.written")或者时间(例如“rocksdb.l0.slowdown.micros”)。 37 | 38 | 矩阵图类型测量所有操作的统计分布。大多数矩阵图用于DB操作的耗时分布。以“rocksdb.db.get.micros”为例,我们测量每个Get的时间,然后计算他们的分布状态。 39 | 40 | ## 打印人类可读的字符串 41 | 42 | 我们可以通过对所有计数器调用ToString()得到人类可读的字符串。 43 | 44 | ## 在info日志中周期性导出统计日志 45 | 46 | 统计会自动导出到info日志,时间间隔为options.stats_dump_period_sec。注意,只有一次压缩之后才会有导出。所以如果数据库很长一段时间不做写入,统计数据可能不会导出,不管options.stats_dump_period_sec是多少。 47 | 48 | ## 代码中访问统计数据 49 | 50 | 我们还可以通过统计对象直接访问特定的统计数据。滴答器类型列表可以在枚举Tickers里面找到。通过调用statistics.getTickerCount(),我们可以获取一个滴答器的值。类似的,一个矩阵图统计可以通过statistics.histogramData()调用,传入具体的枚举;或者调用statistics.getHistogramString()来获得。 51 | 52 | ## 时间间隔的统计 53 | 54 | 所有的统计都是从打开DB就开始累加的。如果你需要根据时间间隔来监控或者报告,你可以周期性地检查数值,然后,通过计算当前值和之前的值,来计算时间间隔值。 55 | 56 | # 自定义统计 57 | 58 | Statistics是一个抽象类,用户可以实现自己的类,然后传递给options.statistics。如果你需要把RocksDB的统计数据整合到自己的统计系统里面,这就很有用了。当你实现一个自定义的统计的时候,请注意RocksDB对recordTick()和measureTime()的调用量。如果不小心实现,自定义的统计很容易就会变成性能瓶颈。 59 | 60 | -------------------------------------------------------------------------------- /doc/Write-Stalls.md: -------------------------------------------------------------------------------- 1 | 当落盘或者压缩无法跟上写入速度的时候,RocksDB有额外的系统来降低写速度。如果没有这个系统,如果用户持续写入超出硬件可以处理的数据,数据会: 2 | 3 | - 增加空间放大,可能导致磁盘不足 4 | - 增加读放大,显著降级读性能。 5 | 6 | 主要的思路是降低进来的写入速度到数据库可以处理的级别。然而,有时候数据库可能会对于一个突发写爆发过于敏感,或者低估硬件的处理能力,所以你可能获得非预期的慢速度或者查询超时。 7 | 8 | 为了确认您的db是否正在一个写失速状态,你可以查看: 9 | 10 | - LOG文件,会包含触发写失速的info日志。 11 | - LOG中的[压缩状态]() 12 | 13 | 失速会因为以下原因被触发: 14 | 15 | - **太多的memtable**。当等待落盘的mentable数量大于或等于max_write_buffer_number时,写入会完全停止,以等待落盘完成。另外,如果max_write_buffer_number大于3,并且等待落盘的memtable数量大于或者等于max_write_buffer_number - 1,写入进入失速状态。这是,你会在日志里看到类似于这个的内容: 16 | 17 | ``` 18 | Stopping writes because we have 5 immutable memtables (waiting for flush), max_write_buffer_number is set to 5 19 | ``` 20 | 21 | ``` 22 | Stalling writes because we have 4 immutable memtables (waiting for flush), max_write_buffer_number is set to 5 23 | ``` 24 | 25 | - **太多level-0的SST文件**。当level 0的SST文件达到level0_slowdown_writes_trigger的时候,写入进入失速状态。当level 0的SST文件达到level0_stop_writes_trigger,写入完全停止,以等待level 0压缩到level 1,以减小level 0的文件数。在这些场景,你会在LOG看到一下info日志 26 | 27 | ``` 28 | Stalling writes because we have 4 level-0 files 29 | ``` 30 | 31 | ``` 32 | Stopping writes because we have 20 level-0 files 33 | ``` 34 | 35 | - **太多等待压缩的字节**。当预计的等待压缩的字节数达到soft_pending_compaction_bytes,写失速开始。当预计的等待压缩字节数达到hard_pending_compaction_bytes,写入会完全停止,等待压缩。在这种情况,你会在LOG看到以下内容: 36 | 37 | ``` 38 | Stalling writes because of estimated pending compaction bytes 500000000 39 | ``` 40 | 41 | ``` 42 | Stopping writes because of estimated pending compaction bytes 1000000000 43 | ``` 44 | 45 | 不管什么时候,只要失速条件被触发,RocksDB会减小写速率到delayed_write_rate,并且如果预计等待压缩的字节数仍旧增加,可能减小速率到低于delayed_write_rate。一个需要注意的地方时,减速/停止触发器和待压缩字节限制是根据每个列族配置的,但是写失速会应用于整个DB,这意味着如果一个列族触发了写失速,整个DB都会失速。 46 | 47 | 有几个选项你可以调优以处理写失速。如果你有一些工作场景可以容忍写失速,但是某些不能,你可以设置一些写入到[低优先级写]()以避免哪些延迟敏感的写入失速。 48 | 49 | 如果写失速是因为落盘被触发,你可以试试: 50 | 51 | - 增加max_background_flushes以得到更多的落盘线程 52 | - 增加max_write_buffer_number已获得更小的等待落盘的memtable 53 | 54 | 如果写失速是因为太多level 0文件或者过多等待压缩的数据触发,压缩不够快,追不上写入了。注意任何减小写放大会减小压缩的时候需要写的字节数,因此可以加快压缩。可以是的选项: 55 | 56 | - 增加max_background_compactions来获得更多压缩线程 57 | - 增加write_buffer_size已获得更大的memtable,以减小写放大 58 | - 增加min_write_buffer_number_to_merge。 59 | 60 | 你也可以设置停止/减速开关以及等待压缩字节数限制到大数字,以避免触发写失速。另外,如果你正在加载一大批数据,看一下[FAQ](RocksDB-FAQ.md)中关于"如何最快速加载数据到RocksDB"的内容。 61 | 62 | -------------------------------------------------------------------------------- /doc/IO.md: -------------------------------------------------------------------------------- 1 | RocksDB提供一组选项给用户来决定IO应该如何执行 2 | 3 | # 控制写IO 4 | 5 | ## 范围Sync 6 | 7 | RocksDB的数据文件通常通过追加的形式生成。文件系统会选择把写入缓冲起来直到脏页达到一个阈值,然后把所有这些页面都写出。这可能会造成突发写IO,导致线上的IO等待过久,导致高查询延迟。你可以要求RocksDB周期性通知OS把已经存在的脏页写出,具体做法就是为SST文件设置options.bytes_per_sync,为WAL文件设置options.wal_bytes_per_sync。在底层,每当一个文件到达这个大小的时候,他就会调用Linux的sync_file_range。当前使用的页不会被包含在范围Sync中。 8 | 9 | ## 限流器 10 | 11 | 你可以通过options.rate_limiter控制RocksDB的总写文件速率,一次保留足够的IO带宽给在线查询。参考[限流器]()。 12 | 13 | ## 最大写缓冲 14 | 15 | 往一个文件追加数据的时候,除非声明了需要fsync,否则RocksDB在写入文件系统前会有内部的文件缓冲区。这个缓冲区的最大大小可以通过options.writable_file_max_buffer_size来控制。在[直接IO模式]()或者在一个没有页缓存的文件系统,可以通过这个参数进行调优。在非直接IO模式,加大这个缓冲区的大小仅仅减少了write系统调用的次数,并且通常不会改变IO行为,所以,除非这个就是你想要的,通常应该把这个值设定为0来节省内存 16 | 17 | # 控制读IO 18 | 19 | ## fadvise 20 | 21 | 当打开一个SST文件进行读取,用户设定options.advise_random_on_open = true(默认).可以决定RocksDB是否会使用FADV_RANDOM来调用fadvise。如果值为false,则打开文件的时候没有fadvise会被调用。如果主要的查询是Get或者是非常小范围的迭代,把这个选项设置为true通常更好,因为预读取在这些场景没什么帮助。另一方面,options.advise_random_on_open = false通过告知文件系统做底层预读取,通常会有更好的性能。 22 | 23 | 不幸的是,如果两种情况同时兼而有之,就没有一个好的设定了。有一个正在进行中的项目,希望通过给RocksDB内部迭代器设定预读取来解决这个问题。 24 | 25 | ## 压缩输入 26 | 27 | 压缩输入比较特别。他们是长的序列化读取,所以使用用户读取选项的fadvise选项通常不是特别好。另外,通常,尽管没有保证,压缩输入文件通常会很快被删除。RocksDB提供多个方式来解决这些问题: 28 | 29 | ### fadvise提示 30 | 31 | RocksDB会对任何压缩输入文件根据options.access_hint_on_compaction_start调用fadvise。当一个文件被选为压缩输入的时候,这个可以覆盖随机fadvise设定。 32 | 33 | ### 给压缩输入使用不同的文件描述符 34 | 35 | 如果options.new_table_reader_for_compaction_inputs = true,RocksDB会使用不同的文件描述符来打开压缩输入。这可以避免混淆普通数据文件的fadvise设定和压缩输入文件的。这个选项的限制是,RocksDB不会仅仅创建一个新的文件描述符,而是重新读索引,过滤器和其他元数据块,然后把他们存储在内存,这会带来额外的IO以及更多的内存使用。 36 | 37 | ### 压缩输入文件的预读取 38 | 39 | 如果options.compaction_readahead_size为0,你可以自己做预读取。这个选项被设置的时候,options.new_table_reader_for_compaction_inputs会自动变为true。这个选项允许用户保持options.access_hint_on_compaction_start为NONE。 40 | 41 | 如果直接IO被打开,或者文件系统不支持预读取,设置这个是不好的。 42 | 43 | # 直接IO 44 | 45 | 与上面的通过文件系统控制IO,你还可以通过option use_direct_reads与/或use_direct_io_for_flush_and_compaction,在RocksDB里面打开直接IO,来直接控制IO,如果直接IO被打开,上面的部分或者全部选项,都可能不生效。更多细节参考[直接IO]() 46 | 47 | # 内存映射 48 | 49 | options.allow_mmap_reads与options.allow_mmap_writes 允许rocksdb在读取或者写入的时候分别mmap整个数据文件,这种做法的好处是可以减少pried和write的时候的系统调用,并且,在很多情况下,可以减少内存拷贝。如果DB运行在ramfs,options.allow_mmap_reads通常可以显著提升性能。他们也可以在块设备的文件系统上被使用。然而,根据我们之前的经验,文件系统通常没法做到完美维护这类内存映射,有时候还会导致慢查询。在这种情况下,我们建议你只在必须的情况下,谨慎尝试。 50 | 51 | -------------------------------------------------------------------------------- /doc/MemTable.md: -------------------------------------------------------------------------------- 1 | MemTable是一个内存数据结构,他保存了落盘到SST文件前的数据。他同时服务于读和写——新的写入总是将数据插入到memtable,读取在查询SST文件前总是要查询memtable,因为memtable里面的数据总是更新的。一旦一个memtable被写满,他会变成不可修改的,并被一个新的memtable替换。一个后台线程会把这个memtable的内容落盘到一个SST文件,然后这个memtable就可以被销毁了。 2 | 3 | 影响memtable的最重要的几个选项是: 4 | 5 | - memtable_factory: memtable对象的工厂。通过声明一个工厂对象,用户可以改变底层memtable的实现,并提供事先声明的选项。 6 | - write_buffer_size:一个memtable的大小 7 | - db_write_buffer_size:多个列族的memtable的大小总和。这可以用来管理memtable使用的总内存数。 8 | - write_buffer_manager:除了声明memtable的总大小,用户还可以提供他们自己的写缓冲区管理器,用来控制总体的memtable使用量。这个选项会覆盖db_write_buffer_size 9 | - max_write_buffer_number:内存中可以拥有刷盘到SST文件前的最大memtable数。 10 | 11 | 默认的memtable实现是基于skiplist的。除了默认的memtable实现,用户可以使用其他memtable实现,例如HashLinkList,HashSkipList或者Vector,以加快查询速度。 12 | 13 | # Skiplist Memtable 14 | 15 | 基于Skiplist的memtable在多数情况下都有较好读,写,随机访问以及序列化扫描性能。除此之外,他还提供其他memtable没有的有用的功能,比如[并发插入]()以及[带Hint插入]() 16 | 17 | # HashSkiplist Memtable 18 | 19 | 正如他们的名字暗示的,HashSkipList用一张哈希表组织数据,每个哈希桶内都是一个的skiplist,而HashLinkList则是用一张哈希表组织数据,每个哈希桶内则是使用一个排序好的链表。两种类型都是为了减少查询的时候的比较次数。一种好的使用例子是使用PlainTable SST格式结合他们,然后把数据存储在RAMFS里。 20 | 21 | 当做数据查询或者插入一个key的时候,目标key的前缀通过Options.prefix_extractor被提取出来,用于找到具体的哈希桶。在哈希桶里面,所有的比较都是完整(内部)key比较,跟SkipList的memtable一样。 22 | 23 | 使用基于哈希的memtable最大的限制就是做多个前缀扫描的时候需要拷贝和排序,这非常慢并且浪费内存 24 | 25 | # 落盘 26 | 27 | 有三种场景会导致memtable落盘被触发: 28 | 29 | - Memtable的大小在一次写入后超过write_buffer_size。 30 | - 所有列族中的memtable大小超过db_write_buffer_size了,或者write_buffer_manager要求落盘。在这种场景,最大的memtable会被落盘 31 | - WAL文件的总大小超过max_total_wal_size。在这个场景,有着最老数据的memtable会被落盘,这样才允许携带有跟这个memtable相关数据的WAL文件被删除。 32 | 33 | 就结果来说,memtable可能还没写满就落盘了。这是为什么生成的SST文件小于对应的memtable大小。压缩是另一个导致SST文件变小的原因,因为memtable里的数据是没有压缩的。 34 | 35 | # 并发插入 36 | 37 | 如果不支持对memtable进行并发插入,从多个线程过来的并发写会按顺序应用到memtable中。并发memtable插入是默认打开的,可以通过allow_concurrent_memtable_write选项来关闭,尽管只有skip-list的memtable支持这个功能。 38 | 39 | # 带Hint插入 40 | 41 | # 原地更新 42 | # 对比 43 | 44 | |Memtable类型|SkipList|HashSkipList|HashLinkList|Vector| 45 | |---|---|---|---|---| 46 | |最佳使用场景|通用|带特殊key前缀的范围查询|带特殊key前缀,并且每个前缀都只有很小数量的行|大量随机写压力| 47 | |索引类型|二分搜索|哈希+二分搜索|哈希+线性搜索|线性搜索| 48 | |是否支持全量db有序扫描?|天然支持|非常耗费资源(拷贝以及排序一生成一个临时视图)|同HashSkipList|同HashSkipList| 49 | |额外内存|平均(每个节点有多个指针)|高(哈希桶+非空桶的skiplist元数据+每个节点多个指针)|稍低(哈希桶+每个节点的指针)|低(vector尾部预分配的内存)| 50 | |Memtable落盘|快速,以及固定数量的额外内存|慢,并且大量临时内存使用|同HashSkipList|同HashSkipList| 51 | |并发插入|支持|不支持|不支持|不支持| 52 | |带Hint插入|支持(在没有并发插入的时候)|不支持|不支持|不支持| 53 | 54 | 55 | -------------------------------------------------------------------------------- /doc/Indexing-SST-Files-for-Better-Lookup-Performance.md: -------------------------------------------------------------------------------- 1 | 对于一个Get()请求,RocksDB遍历可变memtable,不可变memtable列表,以及SST文件,以查找目标key。SST文件被组织成许多层。 2 | 3 | 在Level 0,文件基于他们落盘的时间进行排序。他们key范围(通过FileMetaData.smallest和FileMetaData.largest来定义)通常会相互交叉覆盖。所以他需要查找所有L0文件。 4 | 5 | 压缩会被周期性地调度起来,从上层捡取一些文件然后跟下层的一些文件合并他们。就结果而言,键值对会被从L0逐渐移动到在LSM树的下层。压缩会对键值对排序,然后把它们切分到文件中。从level 1开始往下,SST文件根据key进行排序。他们的key范围互相无交集。为了检查一个key是否落在一个SST文件中,Rocksdb不需要检查每个SST文件,只需要进行针对FileMetaData.largest进行一次二分搜索,就能定位到一个备选文件,该文件**可能**包含目标key。这把复杂度从O(N)降低到O(log(N))。然而,log(N)对于最底层仍旧是非常大的。对于一个扇出比例为10,层数为3的,可以有1000个文件。这需要10个比较来确定一个候选文件。对于一个需要[每秒进行好几百万操作](https://rocksdb.org.cn/doc/RocksDB-In-Memory-Workload-Performance-Benchmarks.html)的内存数据库,这个开销是非常大的。 6 | 7 | 针对这个问题,一个可以观察到的事实是:在LSM树构建后,一个SST文件在某一层的位置是固定的。更进一步,他相对于下一层的位置,也是固定的。基于这个观点,我们可以实施[分级层叠](http://en.wikipedia.org/wiki/Fractional_cascading)类的优化来降低二分搜索范围。这里是一个例子: 8 | 9 | ``` 10 | file 1 file 2 11 | +----------+ +----------+ 12 | level 1: | 100, 200 | | 300, 400 | 13 | +----------+ +----------+ 14 | file 1 file 2 file 3 file 4 file 5 file 6 file 7 file 8 15 | +--------+ +--------+ +---------+ +----------+ +----------+ +----------+ +----------+ +----------+ 16 | level 2: | 40, 50 | | 60, 70 | | 95, 110 | | 150, 160 | | 210, 230 | | 290, 300 | | 310, 320 | | 410, 450 | 17 | +--------+ +--------+ +---------+ +----------+ +----------+ +----------+ +----------+ 18 | ``` 19 | 20 | Level 1有2个文件,level 2 有8个文件。现在,我们希望检索key 80。一个基于FileMetaData.largest 的二分搜索告诉你,文件1是候选者。然后key 80会与FileMetaData.smallest和 FileMetaData.largest 进行比较以确定他是不是在范围内。比较显示,80小于文件的FileMetaData.smallest(100),所以文件1不可能包含key 80。我们继续检查level 2。通常,我们需要在level 2的8个文件中做二分搜索。但是因为我们已经知道目标key 80小于100,且只有一个文件1到文件3可以包含小于100的key,我们可以在搜索的时候很安全地移除其他文件。就结果而言,我们把搜索空间从8个文件降低到了3个。 21 | 22 | 我们看看另一个例子。我们希望获得key 230。一个level 1的二分搜索定位到文件2(这也暗示着key 230大于文件1的FileMetaData.largest 200)。一个与文件2的范围比较显示,目标key小于文件2的FileMetaData.smallest 300。尽管,我们不能再level 1中找这个key,但是我们得到了启示,key在范围200和300之间。在level 2任何与[200,300]没有交集的文件都可以安全地被移除。就结果而言,我们只需要检索level 2的文件5和6. 23 | 24 | 受这个概念启发,我们在压缩的时候预建立指针,从level 1的文件指向一个范围到level 2.例如,level 1的文件1指向文件3(在level 2)的左边,以及文件4的右边。文件2会指向level 2的文件6和文件7.查询的时候,这些指针会被用于根据比对结果,决定实际的二分搜索范围。 25 | 26 | 我们的压力测试显示,这个优化对[这里](https://rocksdb.org.cn/doc/RocksDB-In-Memory-Workload-Performance-Benchmarks.html)提到的设置,增加了大概5%左右的查询QPS 27 | 28 | 29 | -------------------------------------------------------------------------------- /doc/EventListener.md: -------------------------------------------------------------------------------- 1 | # 事件监听器 2 | 3 | 事件监听器EventListener类包含一系列的回调函数,当特定的RocksDB事件发生时,比如一次落盘或者压缩工作结束,就会被调用。这些回调接口可以被用于开发一些自定义功能,比如统计信息收集或者外部压缩算法。可用的监听器回调可以再[include/rocksdb/listener.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/listener.h)中找到 4 | 5 | ## 如何使用? 6 | 7 | 在ColumnFamilyOptions中,有一系列的被调用监听器,允许开发者加入自定义的监听器来监听特定rocksdb实例或者一个列族中的事件。 8 | 9 | ```cpp 10 | // A vector of EventListeners which call-back functions will be called 11 | // when specific RocksDB event happens. 12 | std::vector> listeners; 13 | ``` 14 | 15 | 如果详见听一个rocksdb实例或者一个列族,可以通过简单增加自定义的EventListener到ColumnFamilyOptions::listeners,然后使用这个options来打开DB。 16 | 17 | ```cpp 18 | // listen to a column family of a rocksdb instance 19 | ColumnFamilyOptions cf_options; 20 | ... 21 | cf_options.listeners.emplace_back(new MyListener()); 22 | ``` 23 | 24 | 如果希望监听一个实例的多个列族,你可以对每个列族使用独立的事件监听实例。 25 | 26 | ```cpp 27 | // one listener for each column family 28 | for (size_t i = 0; i < cf_options.size(); ++i) { 29 | cf_options[i].listeners.emplace_back(new MyListener()); 30 | } 31 | ``` 32 | 33 | 或者对所有的列族使用同样的监听器实例: 34 | 35 | ```cpp 36 | // one same listener for all column families. 37 | EventListener my_listener = new MyListener(); 38 | for (size_t i = 0; i < cf_options.size(); ++i) { 39 | cf_options[i].listeners.emplace_back(my_listener); 40 | } 41 | ``` 42 | 43 | 注意,在所有的例子,除非在这个文档特别声明,所有的EventListener回调函数必须以一种线程安全的形式开发,不管这个EventListener是不是只给一个列族使用(例如,想象一下OnCompactionCompleted的使用例子,他可以被一个列族的多个线程调用,因为一个列族可以同一时间完成多个压缩任务) 44 | 45 | ## 监听特定时间 46 | 47 | 所有的EventListener的默认行为都是无操作。这允许开发者只关注他们关心的事情。为了监听一个特定事件,只需要实现相关的回调接口即可。例如,下面的EventListener计算从DB打开开始,落盘工作结束的次数,只需要实现OnFlushCompleted即可: 48 | 49 | ```cpp 50 | class FlushCountListener : public EventListener { 51 | public: 52 | FlushCountListener() : flush_count_(0) {} 53 | void OnFlushCompleted( 54 | DB* db, const std::string& name, 55 | const std::string& file_path, 56 | bool triggered_writes_slowdown, 57 | bool triggered_writes_stop) override { 58 | flush_count_++; 59 | } 60 | private: 61 | std::atomic_int flush_count_; 62 | }; 63 | ``` 64 | 65 | ## 多线程 66 | 67 | 所有的EventListener回调会被事件发生的线程调用。例如,这里有一个RocksDB后台落盘线程,他做完实际的落盘工作之后就调用 EventListener::OnFlushCompleted()。这允许开发者从EventListener通过线程本地的数据收集线程独立的统计数据。 68 | 69 | ## 上锁 70 | 71 | 所有的EventListener都被设计为在没有持有任何线程DB互斥锁的情况下调用。这是为了防止 在使用复杂的EventListener回调的时候 潜在的死锁和性能问题。然而,所有的EventListener回调函数都不应该花费太多的时间,否则RocksDB可能会被上锁。例如,在EventListener回调中,不建议做DB::CompactFiles() (因为这会运行挺长一段时间)或者在一个线程发起非常多的DB::Put()(因为Put可能在某些场合会block)。然而,在不运行EventListener回调的线程执行DB::CompactFiles()和DB::Put()是安全的。 72 | 73 | 74 | -------------------------------------------------------------------------------- /doc/Data-Block-Hash-Index.md: -------------------------------------------------------------------------------- 1 | RocksDB在一个数据块中查找一个key的时候,使用二分搜索。然而,为了找到数据所在的正确的位置,需要多次key解析和压缩。每个二分搜索导致的CPU缓存未命中,都会导致更高的CPU使用率。在生产环境,我们曾发现这个二分搜索占用了非常可观的CPU使用量。 2 | 3 | 一个数据块中的哈希索引被设计和开发出来,用于优化点查询中的CPU使用率。使用db_bench的性能测试显示,点查询中的一个主要方法,DataBlockIter::Seek(),的CPU使用率减低了21.8%,在纯缓存压力下,RocksDB的总体吞吐增加了10%,带来的额外开销是4.6%的内存。 4 | 5 | # 如何使用 6 | 7 | 这个功能加入了两个新的选项:BlockBasedTableOptions::data_block_index_type和BlockBasedTableOptions::data_block_hash_table_util_ratio。 8 | 9 | 哈希索引默认是关闭的,除非设置了BlockBasedTableOptions::data_block_index_type为kDataBlockBinaryAndHash。哈希表的使用率通过BlockBasedTableOptions::data_block_hash_table_util_ratio来配置,同样只有data_block_index_type = kDataBlockBinaryAndHash的时候有效。 10 | 11 | ```cpp 12 | // the definitions can be found in include/rocksdb/table.h 13 | 14 | // The index type that will be used for the data block. 15 | enum DataBlockIndexType : char { 16 | kDataBlockBinarySearch = 0, // traditional block type 17 | kDataBlockBinaryAndHash = 1, // additional hash index 18 | }; 19 | 20 | DataBlockIndexType data_block_index_type = kDataBlockBinarySearch; 21 | 22 | // #entries/#buckets. It is valid only when data_block_hash_index_type is 23 | // kDataBlockBinaryAndHash. 24 | double data_block_hash_table_util_ratio = 0.75; 25 | ``` 26 | 27 | # 需要注意的事情 28 | 29 | ## 自定义比较器 30 | 31 | 哈希索引会哈希不同的key(不同的内容,以及字节序列)到不同的哈希值。这就假设了比较器不会把两个内容不同的key认为是相等的。 32 | 33 | 默认的字节比较器把key按照字典序排列,并且能跟哈希索引友好合作,因为不同的key不会被认为是相等的。然而,有些特别构造的比较器可能会这么做。例如,比如说StringToIntComparator可以把一个字符串转换成整形,然后使用整形来进行比较,key "16" 和 "0x10"在StringToIntComparator看来是相等的,但是他们大概率有两个不同的哈希值。后续的查找其中一种格式的key可能无法找到另一种格式的key。 34 | 35 | 我们加入了一个新的方法给比较器接口: 36 | 37 | ```cpp 38 | virtual bool CanKeysWithDifferentByteContentsBeEqual() const { return true; } 39 | ``` 40 | 41 | 每个比较器的实现应该覆盖这个函数,并且声明这个比较器应该有的行为。如果一个比较器可能认为不同的key是相等的,这个函数返回true,这样,哈希索引的功能就不会打开了,反之亦此。 42 | 43 | 注意:为了使用哈希索引功能,你应该 1)有一个比较器,不会认为两个不同的key相等 2)覆盖CanKeysWithDifferentByteContentsBeEqual方法,返回false,这样才能打开哈希索引 44 | 45 | ## 小util_ratio对数据块缓存的影响 46 | 47 | 把哈希索引加入到数据块的末尾会消耗数据块缓存的空间,导致实际有效的数据块大小变小,并且增加数据块缓存未命中率。因此,一个非常小util_ratio会导致在一个巨大的数据块缓存未命中,额外的IO会抵消通过哈希缓存索引带来的吞吐增长。另外,当允许压缩(compression),哈希未命中率会增加数据块的解压缩操作,同样也会消耗CPU。因此,如果util_ratio过小,CPU可能反而会增长。最好的util_ratio根据工作压力,数据缓存比率,磁盘贷款,延迟而定。在我们的经验,我们认为util_ratio在0.5 ~ 1之间是比较好的范围,既减小cpu使用,又增加吞吐 48 | 49 | # 限制 50 | 51 | 由于我们用uint8_t来存储二分搜索索引,比如,重启间隔索引,重启间隔的总大小不能大于253(我们保留255和254作为特别标记)。对于有大量重启间隔的块,哈希索引不会被创建,点查询不会使用传统的二分搜索进行。 52 | 53 | 数据块索引只支持点查询。我们不支持区间查询。区间查询会下推到BinarySeek。 54 | 55 | RocksDB支持非常多类型的记录,比如Put,Delete,Merge等等(参考[这里]()了解更多)。目前我们只支持Put和Delete,不支持Merge。我们内部有一个限制的支持记录类型集合: 56 | 57 | ``` 58 | kPutRecord, <=== 支持 59 | kDeleteRecord, <=== 支持 60 | kSingleDeleteRecord, <=== 支持 61 | kTypeBlobIndex, <=== 支持 62 | ``` 63 | 64 | 对于不支持的记录,搜索过程会下降到传统的二分搜索。 65 | 66 | -------------------------------------------------------------------------------- /doc/Iterator.md: -------------------------------------------------------------------------------- 1 | # 迭代器 2 | 3 | ## 一致性视图 4 | 5 | 如果ReadOptions.snapshot被给出,那么迭代器会从一个快照里面返回数据。如果这是一个nullptr,迭代器隐式创建一个迭代器创建的时间节点的快照。该隐式快照会通过[固定资源]()来提供数据。隐式快照无法转换为显式快照。 6 | 7 | ## 错误处理 8 | 9 | Iterator::status()会返回迭代过程中的错误。错误包括IO错误,校验和错误,不支持的操作,内部错误,或者其他错误。 10 | 11 | 如果没有错误,会返回Status::OK()。如果不是OK,迭代器会马上失效。换句话说,如果Iterator::Valid()为true,status()会被保证为OK(),所以你不检查status也是可以的: 12 | 13 | ```cpp 14 | for (it->Seek("hello"); it->Valid(); it->Next()) { 15 | // Do something with it->key() and it->value(). 16 | } 17 | if (!it->status().ok()) { 18 | // Handle error. it->status().ToString() contains error message. 19 | } 20 | 21 | ``` 22 | 23 | 换句话说,如果Iterator::Valid()为false,有两种可能:(1)我们已经读完所有数据了。这个时候,status()是OK();(2)确实出错了。这种情况下,status()不会是OK()。在迭代器失效的时候检查一下status是非常好的习惯。 24 | 25 | Seek()和SeekForPrev()会丢弃之前的状态。 26 | 27 | 注意,在5.13.x和之前的版本(在2018年5月17日合并的[PR](https://github.com/facebook/rocksdb/pull/3810)之前),status()和Valid()的行为是有所不同的: 28 | 29 | - Valid()可能在status()不为ok的时候返回true。这可以用来跳过一些损坏的数据。现在我们不支持这个功能了。正确的损坏数据处理方式是使用RepairDB() (参考db.h)。 30 | - Seek()和SeekForPrev()不一定总是会丢弃之前的状态。Next()和Prev()不一定会给你非ok的状态。 31 | 32 | ## 迭代边界 33 | 34 | 你可以通过在调用NewIterator的时候,给传入的option设定ReadOptions.iterate_upper_bound来为你的迭代范围设置一个上边界。通过这个设定,RocksDB就不用继续查找这个key之后的内容了。在一些情况下,可以节省一些IO和计算。在特定的工作载荷下,它带来的改善是显著的。这个选项可以同在正向和反向迭代。 35 | 36 | 参考该选项的注释以了解更多内容。 37 | 38 | ## 迭代器使用的资源以及迭代器更新 39 | 40 | 迭代器自身并不怎么使用内存,但是他会阻止一些资源释放。包括: 41 | 42 | - 迭代器创建时使用的memtable和sst文件。即使其中一些memtable和sst文件在落盘或者压缩之后被删除了,他们还会为了保证迭代器工作而被保留。 43 | - 当前迭代点的数据快。这些块会被保存在内存,要么在块缓存,要么在堆(如果没有快缓存的话)。注意,尽管大多数块都是很小的,但在一些极端情况下,如果值非常大,那么单个块可能非常大。 44 | 45 | 所以使用迭代器最好是短期使用,这样资源可以被正确释放。 46 | 47 | 迭代器在构建的时候会有一些花销。在一些应用场景(特别是纯内存的场景),人们可能会希望通过重用迭代器来减少构建的开销。如果你也这么做,请记住,迭代器是可能过期的,他会阻止一些资源的释放。所以请一定要记得在他们一段时间(例如,1秒钟)没有使用后销毁,或者更新他们。如果你希望处理这个过期的迭代器,在5.7版本前,你只能销毁,然后重建这个迭代器。5.7版本之后,你可以使用接口Iterator::Refresh()来更新他。通过这个函数,迭代器会更新到当前的数据状态,过期的资源会被清理。 48 | 49 | ## 前缀迭代 50 | 51 | 前缀迭代允许用户在迭代中使用bloom filter或者哈希索引,以此来改善性能。然而,这个功能有一定限制,而且,如果你误用他们,他还会返回错误的结果而不报任何错误。我们希望你使用这个功能的时候小心谨慎。更多关于这个功能的内容,参考 [前缀迭代]()。选项total_order_seek和prefix_same_as_start只在前缀迭代过程中有用。 52 | 53 | ## 预读取 54 | 55 | 迭代过程中,如果注意到同一个表文件有需要被读2次以上IO的数据,rocksDB会自动预读,预取数据。预读的大小从8KB开始,然后会在每次额外的顺序IO之后指数增长,最大增长到256KB。这可以帮助减少完成一个区间扫描的IO数量。这个自动预读只有在ReadOptions.readahead_size = 0(默认值)的时候开启(从5.12开始,迭代器自动预读功能可以用于带缓冲的文件IO,5.15开始可用于无缓冲文件IO) 56 | 57 | 如果你的整个应用都是不断迭代,而你又依赖OS的页缓存(例如,使用带缓冲的文件IO),你可以选择通过设置DBOptions.advise_random_on_open = false手动打开预读取。你如果使用硬盘或者远程存储的时候会更加有用,但是如果是挂载在本地的SSD设备,效果就不明显了。 58 | 59 | ReadOptions.readahead_size为RocksDB的一些非常特定的使用场景提供预读功能。他的限制就是,如果这个功能打开了,构建迭代器的开销会大很多。所以,只在你需要迭代相当大量的数据、而且又没有其他更好的办法的时候,你才应该打开它。一个典型的场景就是,存储介质是远程存储,并且延迟比较大,OS的页缓存不可用,然后又有大量数据需要扫描的时候。通过打开这个功能,每次SST文件的读都会根据这个设定进行预读取。注意,一个迭代器可能会在每一层都打开一个文件,同时也会打开L0的所有文件。你需要为你的预读准备足够的内存。而且预读的内存无法被自动追踪。 60 | 61 | 我们还在尝试改善RocksDB的预读取。 62 | 63 | -------------------------------------------------------------------------------- /doc/Direct-IO.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | 直接IO是一个系统层的功能,支持用户层直接读/写存储设备而不使用系统的页缓存。缓冲式IO通常是大多数操作系统的默认IO模式。 4 | 5 | ## 为什么我们需要它 6 | 7 | 使用缓冲式IO的时候,数据会在存储介质和内存间被拷贝两次,因为页缓存式两者的代理。在多数情况,使用页缓存可以获得更好的性能。但是对于自缓存应用,例如RocksDB,应用自身会比OS更好地了解逻辑和数据语意,这就使得应用可以通过他们对数据的了解,更好地以应用定义的数据块为单位,实现更有效的缓存替换算法。另一边,在某些情况,我们希望某些数据不使用系统缓存。这时候,直接IO会是一个更好的选择。 8 | 9 | ## 实现 10 | 11 | 打开直接IO的方式与OS以及文件系统对直接IO的支持有关。在使用这个功能之前,请检查文件系统是否支持直接IO。RocksDB已经处理了这些系统依赖的兼容性问题,但是我们想分享一下实现细节。 12 | 13 | 打开文件 14 | 对于LINUX,需要加入O_DIRECT标记。对于Mac OSX,没有O_DIRECT。作为替换,fcntl(fd, F_NOCACHE, 1),fd是文件描述符,看起来是权威方案。对于Windows,有一个叫做FILE_FLAG_NO_BUFFERING的标记,是Windows中O_DIRECT的替代品。 15 | 16 | 文件读写 17 | 直接IO要求文件读写是对齐的,这意味着,位置游标(偏移),#bytes以及buffer地址,必须与底层存储的逻辑扇区大小对齐。这样,位置游标应该,缓冲区地址必须,与逻辑扇区的大小边界对齐,读取和写入的数据量必须是逻辑扇区的大小的整数倍。RocksDB在FileReade和FileWrite里面实现了所有的对齐逻辑,一个基于File类的更高的抽象层,用来处理OS不关心对齐问题。因此,不同的OS可能有他们自己实现的File类。 18 | 19 | 20 | # API 21 | 22 | 由于options.h提供的两个新的API,使用直接IO非常简单: 23 | 24 | ```cpp 25 | // Use O_DIRECT for user reads 26 | // Default: false 27 | // Not supported in ROCKSDB_LITE mode! 28 | bool use_direct_reads = false; 29 | 30 | // Use O_DIRECT for both reads and writes in background flush and compactions 31 | // When true, we also force new_table_reader_for_compaction_inputs to true. 32 | // Default: false 33 | // Not supported in ROCKSDB_LITE mode! 34 | bool use_direct_io_for_flush_and_compaction = false; 35 | ``` 36 | 代码是自解释的。 37 | 38 | 你可能还需要其他选项来优化直接IO的性能: 39 | 40 | ```cpp 41 | // options.h 42 | // Option to enable readahead in compaction 43 | // If not set, it will be set to 2MB internally 44 | size_t compaction_readahead_size = 2 * 1024 * 1024; // recommend at least 2MB 45 | // Option to tune write buffer for direct writes 46 | size_t writable_file_max_buffer_size = 1024 * 1024; // 1MB by default 47 | // DEPRECATED! 48 | // table.h 49 | // If true, block will not be explicitly flushed to disk during building 50 | // a SstTable. Instead, buffer in WritableFileWriter will take 51 | // care of the flushing when it is full. 52 | // This option is deprecated and always be true 53 | bbto.skip_table_builder_flush = true; 54 | 55 | ``` 56 | 57 | 最近的版本如果开启了直接IO,会把这些选项自动设置好。 58 | 59 | ## 注意 60 | 61 | allow_mmap_reads不可以和use_direct_reads或者use_direct_io_for_flush_and_compaction一起用。allow_mmap_write不能和use_direct_io_for_flush_and_compaction一起用。也就是,他们不能同时为true。 62 | 63 | use_direct_io_for_flush_and_compaction和use_direct_reads只会对SST文件IO生效,WAL的IO或者MANIFEST的IO不会生效。对WAL和Manifest文件直接IO目前还不支持。 64 | 65 | 打开直接IO后,压缩写将不再写入OS页缓存,所以第一次读会从真实IO读。某些用户可能知道RocksDB有一个功能叫做压缩块缓存,用于在直接IO的时候替换页缓存。但是打开前请阅读下面的备注: 66 | 67 | - 碎片化:RocksDB的压缩块不是根据页大小对齐的。一个压缩块存放在一个malloc生成的RocksDB的压缩块缓存中。这通常意味着内存使用的碎片化。OS的页缓存会好点,因为他缓存整个物理页。如果某些连续的块总是热数据,OS的页缓存会使用更少的内存来缓存他们。 68 | - OS页缓存提供预读取。在RocksDB这个是默认关闭的,但是用户可以选择开启他们。这在范围扫描的情况下会很好用。RocksDB压缩缓存在这种情况没有任何对应的功能。 69 | - 可能存在bug。RocksDB的压缩块缓存以前从来没有被用过。我们确实看到有外部用户给它报告bug了,但是我们不会在这个模块进行更多改进。 70 | 71 | -------------------------------------------------------------------------------- /doc/Delete-Stale-Files.md: -------------------------------------------------------------------------------- 1 | 在这个wiki中,我们解释文件在他们不再被需要的时候是如何被删除的。 2 | 3 | # SST文件 4 | 5 | 当压缩结束,输入的SST文件会被LSM树里输出的文件替代。然而,他们可能还不能马上被删除。依赖于旧版本的LSM树的进行中的操作 使得这些文件不能马上被删除,需要等待这些操作完成。参考[我们如何追踪存活SST文件](https://github.com/johnzeng/rocksdb-doc-cn/blob/master/doc/How-we-keep-track-of-live-SST-files.md)了解LSM树版本是怎么工作。 6 | 7 | 可以保留旧版本的LSM树的操作包括: 8 | 9 | - 存活迭代器。迭代器创建的时候会固定LSM树的版本。该版本的所有的SST文件都不能被删除。这是因为一个迭代器从一个虚拟的快照读取数据,那个瞬间的所有SST文件都要为了保证这个快照正常工作而被保留起来。 10 | - 进行中的压缩。尽管其他的压缩没有在压缩这些SST文件,整个LSM树版本都会被固定 11 | - Get过程中的很短一个时间。在Get执行中的一个很短的时间内,LSM树版本是被固定,以保证他可以完整读取所有不可变得SST文件。 12 | 13 | 当没有操作固定一个旧的LSM树版本的SST文件时,这个文件才可以被删除。 14 | 15 | 这些可以删除的文件会通过两个机制被删除 16 | 17 | ## 引用计数 18 | 19 | RocksDB在内存中为每个SST文件保留一份引用计数。每个LSM树的版本都会有对该版本的所有SST文件有一个引用计数。基于这个版本的操作(上面提到了)会持有LSM树的版本的引用计数,或者直接通过“super versoin”间接持有。一旦一个版本的引用计数归零,他会丢弃所有SST文件的引用计数。如果一个SST文件的引用计数归零,他就可以被删除了。通常他们会马上被删除,除了一下情况: 20 | 21 | - 在关闭一个迭代器的时候,发现这个文件不再需要了。如果用户设置ReadOptions.background_purge_on_iterator_cleanup=true,我们不会马上删除,而是在高优先线程池中调度一个后台任务来删除(跟删除落盘任务的池是一个池)。 22 | - 在Get或者其他操作中,如果他对LSM树版本的解引用导致某些SST文件需要被删除。他们不会被删除,而是会被保留。下一次落盘任务会清理他们,或者如果某些SST文件会在其他线程被删除,他们会一起被删除。这样,我们保证Get不会有文件删除的IO操作。注意,如果没有落盘发生,过期文件会保留在那里,不会删除。 23 | - 如果用户调用DB::DisableFileDeletions()。所有即将删除的文件都会被保留。一旦DB::EnableFileDeletions()清理了删除限制,他会删除所有挂起的SST文件。 24 | 25 | ## 罗列所有文件,以找出过期文件 26 | 27 | 引用计数机制在大多数场景都是有用的。然而,引用计数不能持久化,所以一重启就丢失了,所以我们需要其他的机制来回收文件。重启DB的时候,我们做一次全量垃圾回收,然后根据options.background_purge_on_iterator_cleanup周期性执行清理。后面的选项只是为了保证安全。 28 | 29 | 在这个全量垃圾回收模式中,我们罗列DB目录里的所有文件,然后检查一个个文件比对检查所有的LSM树,看看是不是已经不用了。对于不需要的文件,我们删除他们。然而,并不是在DB目录,但是没有在存活版本的文件,就是没用的。对于正在进行中的压缩或者落盘创建的文件不应该被删除。为了阻止这个发生,我们使用一个好功能,他要求文件名单调递增。在落盘或者压缩开始前,我们记住当时最新的SST文件编号。如果一次全量垃圾回收在这个工作结束前开始了,所有编号大于该编号的SST文件会被保留。这个条件会在相关任务结束之后被释放。由于多个压缩和落盘工作可以并行运行,所有编号大于最早固定的SST文件编号的文件,都会被保留。可能我们会有一些假阳性,但是他们最终会被清理。 30 | 31 | # 日志文件 32 | 33 | 一个日志文件在所有数据都落盘到SST文件之后就可以被删除了。对于一个单列族的DB,决定一个日志文件是否可以删除是非常简单的,对于多列族的DB,则稍微复杂点,更复杂的情况是,如果你开启了两阶段提交(2PC)的时候 34 | 35 | ## 单列族DB 36 | 37 | 一个日志文件与一个memtable有1:1对应关系。一旦一个memtable落盘了,对应的日志文件就会被删除。会在落盘工作的最后进行。 38 | 39 | ## 多列族DB 40 | 41 | 当有多个列族,一个新的日志文件会在任何一个列族落盘的时候创建。一个日志文件只可以在所有列族的数据都落盘的时候删除。RocksDB的实现方式是每个列族追踪 本列族 还有 未落盘数据的 最早的日志文件。一个日志文件只能在他比 所有列族各自的最早的日志 的最早那个还要早的时候,才能被删除。 42 | 43 | ## 两阶段提交 44 | 45 | 在两阶段提交中,有一次写入,有两个日志需要写:一个准备和一个提交。只有当提交了的日志落盘之后,我们才能释放包含准备日志和提交日志的文件。不再有一个简单的从memtable到日志文件的映射了。例如,考虑下面的一个DB列族的序列: 46 | 47 | ``` 48 | -------------- 49 | 001.log 50 | Prepare Tx1 Write (K1, V1) 51 | Prepare Tx2 Write (K2, V2) 52 | Commit Tx1 53 | Prepare Tx3 Write (K3, V3) 54 | -------------- <= Memtable Flush <<<< Point A 55 | 002.log 56 | Commit Tx2 57 | Prepare Tx4 Write (K4, V4) 58 | -------------- <= Memtable Flush <<<< Point B 59 | 003.log 60 | Commit Tx3 61 | Prepare Tx5 Write (K5, V5) 62 | -------------- <= Memtable Flush <<<< Point C 63 | ``` 64 | 65 | 在Ponit A,尽管memtable落盘了,001.log还是不能删除,因为Tx2和Tx3还没有提交。类似的,在PointB,001.log还是不能被删除,因为Tx3没有提交。只有到了PointC,001.log才可以删除。但是002.log还是不能删除,因为Tx4还没提交。 66 | 67 | RocksDB会使用一个错综复杂的低锁数据结构来决定一个日志文件是不是可以被删除。 68 | 69 | -------------------------------------------------------------------------------- /doc/Write-Ahead-Log-File-Format.md: -------------------------------------------------------------------------------- 1 | # 概述 2 | 3 | WAL会把memtable的操作序列化之后以日志文件形式存储在持久化介质中。发生崩溃的时候,WAL文件可以用于重新构建memtable,帮助数据库恢复数据库到一个一致的状态。当一个memtable被安全地落盘到持久化介质之后,相关的WAL日志会变成过期的,然后被归档。最终归档的日志会在一定时间后被从硬盘上删除。 4 | 5 | # WAL管理器 6 | 7 | WAL文件使用一个递增的序列号生成到WAL文件夹。为了重新构建数据库的状态,这些文件会被按序列号顺序读取。WAL管理器提供把WAL文件作为一个独立单元进行读取的抽象接口。内部,他使用Writer或者Reader抽象接口打开,并读取文件。 8 | 9 | # Reader/Writer 10 | 11 | Writer提供一个抽象接口,用于在日志文件末尾增加数据。存储介质相关的内部细节信息通过WriteableFile接口处理。类似的,Reader提供一个抽象接口,用于从一个日志文件中顺序读取日志记录。内部的存储介质相关细节信息有SequentialFile接口处理。 12 | 13 | # 日志文件格式 14 | 15 | 日志文件由一系列的变长记录构成。记录通过kBlockSize聚集在一起。如果某个特定记录不能放入剩余的空间,那么剩余空间将会被空数据填充。writer写而reader读数据的时候,是按照一个kBlockSize大小的块来读的 16 | 17 | ``` 18 | +-----+-------------+--+----+----------+------+-- ... ----+ 19 | File | r0 | r1 |P | r2 | r3 | r4 | | 20 | +-----+-------------+--+----+----------+------+-- ... ----+ 21 | <--- kBlockSize ------>|<-- kBlockSize ------>| 22 | 23 | rn = 变长块记录 24 | P = 填充数据 25 | ``` 26 | 27 | # 记录格式 28 | 29 | 记录的排列格式如下所示: 30 | 31 | ``` 32 | +---------+-----------+-----------+--- ... ---+ 33 | |CRC (4B) | Size (2B) | Type (1B) | Payload | 34 | +---------+-----------+-----------+--- ... ---+ 35 | 36 | CRC = 使用CRC算出来的payload的32bit的哈希码 37 | Size = payload数据的长度 38 | Type = 记录的类型 39 | (kZeroType, kFullType, kFirstType, kLastType, kMiddleType ) 40 | 类型用于将一系列的记录分到一组,用来表示大小大于kBlockSize的块 41 | Payload = 长度为Size的payload数据流。 42 | ``` 43 | 44 | # 记录格式细节 45 | 46 | 日志的内容是一系列的32KB的块。唯一的例外是文件的末尾可能会包含一个分片的块。 47 | 48 | 每个块都由一系列记录构成: 49 | 50 | ``` 51 | block := record* trailer? 52 | record := 53 | checksum: uint32 // crc32c,覆盖 type 和 data[] 54 | length: uint16 55 | type: uint8 // FULL, FIRST, MIDDLE, LAST 的一种 56 | data: uint8[length] 57 | ``` 58 | 59 | 一个记录不会在一个块的最后6个Byte开始(毕竟放不下)。任何剩下的数据都构成tailer,tailer由全0构成,读取的时候应该被跳过。 60 | 61 | 如果当前块正好剩下7个Byte,并且一个新的非0长度记录被加入进来,那么write必须加一个FIRST记录(里面不含任何用户数据)来填充剩下的7个byte,然后在下一个块再提交用户数据。 62 | 63 | 以后可能会增加更多的类型。有些Reader会跳过那些他们不能理解的记录类习惯,其他可能会报告某些数据被跳过。 64 | 65 | ``` 66 | FULL == 1 67 | FIRST == 2 68 | MIDDLE == 3 69 | LAST == 4 70 | ``` 71 | 72 | FULL类型的记录保存完整的用户数据。 73 | 74 | FIRST,MIDDLE,LAST在不得不把用户数据切分成多个分片的时候使用(大多数是因为块边界问题)。FIRST是用户数据的第一个分片用的类型,LAST是最后一个用户数据分片用的记录类型,MIDDLE则是中间那些所有的其他数据的记录类型。 75 | 76 | 例子:考虑一个用户记录的序列: 77 | 78 | ``` 79 | A: 长度 1000 80 | B: 长度 97270 81 | C: 长度 8000 82 | ``` 83 | 84 | A会在第一个block里被存储为一个FULL记录。 85 | 86 | B会被分成三个分片:第一个分片占据第一个块剩下的空间,第二个分片占据第二个块的完整空间,第三个分片占据第三个块的开头部分。第三个块还剩下6个Byte,作为一个tailer,留空。 87 | 88 | C会在第四个块以FULL记录存储 89 | 90 | # 优势 91 | 92 | 记录型格式的优势如下: 93 | 94 | - 在重新同步的时候,我们不需要任何启发式操作——只要读到下一个块边界然后扫描即可。如果有错误中断,跳到下一个块。还有一个副作用,如果一个日志文件的一部分被嵌入到了另一个文件中,我们不会无法理解。 95 | - 在大致的边界切分很简单(也许是为了做mapreduce):找到下一个块的边界,然后跳过所有记录,直到我们命中一个FULL或者FIRST记录。我们不需要缓存大量记录。 96 | 97 | # 劣势 98 | 99 | 记录型格式的劣势如下: 100 | 101 | - 小记录没有打包方式。可能可以加入一个新的类型来解决,所以这个是当前实现导致的缺陷,不是格式本身的问题 102 | - 无法压缩。同样的,这个可以通过加一个新的记录类型来解决。 103 | 104 | 105 | -------------------------------------------------------------------------------- /doc/Memory-usage-in-RocksDB.md: -------------------------------------------------------------------------------- 1 | 这里我们尝试解释Rocksdb如何使用内存。Rocksdb中有几个组件会贡献内存使用: 2 | 3 | - 块缓存(Block cache) 4 | - 索引和bloom过滤器 5 | - Memtable 6 | - 迭代器固定的块 7 | 8 | 我们会轮流讨论他们 9 | 10 | # 块缓存 11 | 12 | 块缓存是Rocksdb缓存未压缩数据块的地方。你可以通过BlockBasedTableOptions的block_cache配置块缓存的大小: 13 | 14 | ``` 15 | rocksdb::BlockBasedTableOptions table_options; 16 | table_options.block_cache = rocksdb::NewLRUCache(1 * 1024 * 1024 * 1024LL); 17 | rocksdb::Options options; 18 | options.table_factory.reset(new rocksdb::BlockBasedTableFactory(table_options)); 19 | ``` 20 | 21 | 如果数据块在块缓存中没有被发现,Rocksdb使用带缓存的IO从文件中读取他。这意味着他也会使用页缓存 —— 这里包含了原始的压缩过的块。这样,Rocksdb的缓存是两层的:块缓存和页缓存。反直觉的一件事情是,减小块缓存不会增加IO。存起来的内存可能会被用于页缓存,所以会有更多的数据被缓存起来。然而,CPU使用量可能会增加,因为Rocksdb需要解压他从页缓存读取的数据。 22 | 23 | 为了学习块缓存使用的量有多少,你可以在一个块缓存对象上调用GetUsage(): 24 | 25 | ``` 26 | table_options.block_cache->GetUsage(); 27 | ``` 28 | 29 | 在MongoRocks,你可以通过调用以下命令得到块缓存大小: 30 | 31 | ``` 32 | > db.serverStatus()["rocksdb"]["block-cache-usage"] 33 | ``` 34 | 35 | # 索引和过滤块 36 | 37 | 索引和过滤块可能是内存使用大户,并且默认他们不会计算在你分配给块缓存的内存里。这有时候会迷惑用户:你分配了10GB给块缓存,但是Rocksdb使用了15GB的内存。这个差异通常是因为索引和bloom过滤块。 38 | 39 | 这里介绍你如何大致计算和管理索引和过滤块的大小: 40 | 41 | 对于每个数据块我们存储三个信息到索引:一个key,一个偏移,以及一个大小。因此有两个方法你可以减少索引的大小。 42 | 如果你增加块大小,块的数量会减少,所以索引的大小会线性减少。 43 | 默认情况下我们的块大小为4KB,尽管我们通常在生产环境使用16~32KB。第二个减少索引大小的方法是减少key的大小,尽管对于某些使用场景,这不现实。 44 | 计算过滤块的大小的方法很简单。如果你配置bloom过滤器为每个key取10个bit(默认,会有1%的假阳性),bloom过滤器大小为number_of_keys * 10 bit。 45 | 不过你可以使用一个技巧。如果你确认Get()在最坏的情况下都能找到一个key,你可以设置options.optimize_filters_for_hits为true。 46 | 打开了这个选项,我们会不再最后一层,包含90%的数据库内容,构建bloom过滤器。因此,内存的bloom过滤器的使用量会少10X倍。 47 | 但是,对于不能找到数据的Get请求,你每次都要花费一个额外的IO。 48 | 49 | 有两个选项用于配置索引和过滤块使用多少内存的: 50 | 51 | 如果你设置cache_index_and_filter_blocks为true,索引和过滤块会被存储在块缓存,跟其他数据块一起。 52 | 这同时意味着他们会被页换出。如果你的访问方式局部性比较强(比如,你有一些非常少用的key范围),这个设置就很合理了。 53 | 然而,在大多数情况,他都是对你的性能有害的,因为你需要索引和过滤器来访问一个特定的文件。 54 | 如果你确定你的ulimit总是大于数据库中的文件数量,我们推荐你设置max_open_files为-1,以为这无限制。 55 | 这个选项会预加载所有过滤器和索引块,并且不需要维护文件的LRU。设置max_open_files为-1会给你最好的性能。 56 | 57 | 为了了解索引和过滤块使用的内存,你可以使用RocksDB的GetProperty() API: 58 | 59 | ``` 60 | std::string out; 61 | db->GetProperty("rocksdb.estimate-table-readers-mem", &out); 62 | ``` 63 | 64 | 在MongoRock,只需要在mongo shell调用这个API: 65 | 66 | ``` 67 | > db.serverStatus()["rocksdb"]["estimate-table-readers-mem"] 68 | ``` 69 | 70 | 在[分片索引/过滤](Partitioned-Index-Filters.md)中,分片总是存储在块缓存。顶级索引可以通过cache_index_and_filter_blocks被配置为存储在堆或者块缓存。 71 | 72 | # Memtable 73 | 74 | 你可以认为memtable是内存写缓冲。每个新的键值对先写入到memtable。Memtable大小通过选项write_buffer_size控制。通常他不是内存消耗大户。然而,memtable的大小与写放大成反比 —— 你给memtable的内存越多,写放大越小。如果你增加你的memtable的大小,确保增加你的L1的大小,L1的大小通过选项max_bytes_for_level_base控制。 75 | 76 | 为了获得当前的memtable大小,你可以使用: 77 | 78 | ``` 79 | std::string out; 80 | db->GetProperty("rocksdb.cur-size-all-mem-tables", &out); 81 | ``` 82 | 83 | 在MongoRocks,等价的调用是: 84 | 85 | ``` 86 | > db.serverStatus()["rocksdb"]["cur-size-all-mem-tables"] 87 | ``` 88 | 89 | 从5.6版本开始,你可以把memtable放入块缓存中。参考[Write-Buffer-Manager](Write-Buffer-Manager.md)。 90 | 91 | # 迭代器固定的块 92 | 93 | 迭代器固定的块通常不会贡献太多的内存使用量。然而,在某些时候,当你有100k读事务同步发生,这就会给内存造成压力了。固定的块的内存使用很容易计算。每个迭代器固定L0的每个文件的一个数据块,加上每个L1+level的一个数据块。所以迭代器固定的块的总的内存使用接近 num_iterators * block_size * ((num_levels-1) + num_l0_files)。为了获得这个内存使用的统计信息,调用在块缓存对象上调用GetPinnedUsage(): 94 | 95 | ``` 96 | table_options.block_cache->GetPinnedUsage(); 97 | ``` 98 | 99 | 100 | -------------------------------------------------------------------------------- /doc/Column-Families.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 在RocksDB3.0,我们增加了Column Families的支持。 3 | 4 | RocksDB的每个键值对都与唯一一个列族(column family)结合。如果没有指定Column Family,键值对将会结合到“default” 列族。 5 | 6 | 列族提供了一种从逻辑上给数据库分片的方法。他的一些有趣的特性包括: 7 | 8 | - 支持跨列族原子写。意味着你可以原子执行Write({cf1, key1, value1}, {cf2, key2, value2})。 9 | - 跨列族的一致性视图。 10 | - 允许对不同的列族进行不同的配置 11 | - 即时添加/删除列族。两个操作都是非常快的。 12 | 13 | # API 14 | 15 | ## 向后兼容 16 | 17 | 尽管我们需要做一些很极端的修改来支持列族,我们还是支持老的API的。你不需要对你的应用做任何改变就可以迁移到RocksDB3.0。所有通过旧的API插入的键值对都会插入到“default”列族。升级之后降级也是同理。如果你从未使用超过一个列族,我们不会改变任何磁盘格式,也就是说你可以放心地回滚到RocksDB2.8。这对我们那些FaceBook里的客户来说,是非常重要的。 18 | 19 | ## 使用例子 20 | 21 | [Column_families_example.cc](https://github.com/facebook/rocksdb/blob/master/examples/column_families_example.cc) 22 | 23 | ## 参考 24 | 25 | ### Options, ColumnFamilyOptions, DBOptions 26 | 27 | 在[include/rocksdb/options.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/options.h)中定义,Options结构定义RocksDB的行为和性能。以前,每个option都被定义在单独的Options结构体里。现在,对单个列族的配置,会被定义在ColumnFamilyOptions,然后那些针对整个RocksDB实例的配置会被定义在DBOptions。Options结构体同时继承ColumnFamilyOptions和DBOptions,你仍旧可以用它来设置所有针对只有一个类族的DB实例的配置。 28 | 29 | ### ColumnFamilyHandle 30 | 31 | 列族通过ColumnFamilyHandle调用和引用。可以把它当成一个打开的文件描述符。在你删除DB指针前,你需要删除所有所有ColumnFamilyHandle。一个有趣的事实:即时一个ColumnFamilyHandle指向一个已经删除的列族,你还是可以继续使用它。数据只有在你将所有存在的ColumnFamilyHandle都删除了,才会被清除。 32 | 33 | ### DB::Open(const DBOptions& db_options, const std::string& name, const std::vector& column_families, std::vector* handles, DB** dbptr); 34 | 35 | 当使用读写模式打开一个DB的时候,你需要声明所有已经存在于DB的列族。如果不是,DB::Open会返回 Status::InvalidArgument(),你可以用一个ColumnFamilyDescriptors的vector来声明列族。ColumnFamilyDescriptors是一个只有列族名和ColumnFamilyOptions的结构体。Open会返回一个Status以及一个ColumnFamilyHandle指针的vector,你可以用他们来引用这些列族。删除DB指针前请确保你已经删除了所有ColumnFamilyHandle。 36 | 37 | 38 | ### DB::OpenForReadOnly(const DBOptions& db_options, const std::string& name, const std::vector& column_families, std::vector* handles, DB** dbptr, bool error_if_log_file_exist = false) 39 | 40 | 行为与DB::Open类似,只不过他用只读模式打开DB。其中一个比较大的差别是,如果用只读模式打开DB,你不需要声明所有列族——你可以只打开一个列族的子集。 41 | 42 | ### DB::ListColumnFamilies(const DBOptions& db_options, const std::string& name, std::vector* column_families) 43 | 44 | ListColumnFamilies是一个静态方法,会返回当前DB里面存在的列族 45 | 46 | 47 | ### DB::CreateColumnFamily(const ColumnFamilyOptions& options, const std::string& column_family_name, ColumnFamilyHandle** handle) 48 | 49 | 指定一个名字和配置,创建一个列族,然后在参数里面返回一个ColumnFamilyHandle。 50 | 51 | ### DropColumnFamily(ColumnFamilyHandle* column_family) 52 | 53 | 删除ColumnFamilyHandle指向的列族。注意,实际的数据在客户端调用delete column_family之前 并不会被删除。只要你还有column_family,你就还可以继续使用这个列族。 54 | 55 | ### DB::NewIterators(const ReadOptions& options, const std::vector& column_families, std::vector* iterators) 56 | 57 | 这是一个新的调用,允许你在DB上创建一个跨列族的一致性视图。 58 | 59 | ## 批量写 60 | 61 | 你需要构建一个WriteBatch来实现原子化的批量写操作。所有WriteBatch API现在可以额外携带一个ColumnFamilyHandle指针来声明你希望写到哪个列族。 62 | 63 | ## 所有其他API调用 64 | 65 | 所有其他API调用都有了一个新的参数`ColumnFamilyHandle*`,用于声明你想操作的列族。 66 | 67 | ## 实现 68 | 69 | 列族的主要实现思想是他们共享一个WAL日志,但是不共享memtable和table文件。通过共享WAL文件,我们实现了酷酷的原子写。通过隔离memtable和table文件,我们可以独立配置每个列族并且快速删除它们。 70 | 71 | 每当一个单独的列族刷盘,我们创建一个新的WAL文件。所有列族的所有新的写入都会去到新的WAL文件。但是,我们还不能删除旧的WAL,因为他还有一些对其他列族有用的数据。我们只能在所有的列族都把这个WAL里的数据刷盘了,才能删除这个WAL文件。这带来了一些有趣的实现细节以及一些有趣的调优需求。确保你的所有列族都会有规律地刷盘。另外,看一下Options::max_total_wal_size,通过配置他,过期的列族能自动被刷盘。 72 | 73 | -------------------------------------------------------------------------------- /doc/Write-Ahead-Log.md: -------------------------------------------------------------------------------- 1 | # 概述 2 | RocksDB中的每个更新操作都会写到两个地方:1)一个内存数据结构,名为memtable(后面会被刷盘到SST文件) 2)写到磁盘上的WAL日志。在出现崩溃的时候,WAL日志可以用于完整的恢复memtable中的数据,以保证数据库能恢复到原来的状态。在默认配置的情况下,RocksDB通过在每次写操作后对WAL调用fflush来保证一致性。 3 | 4 | # WAL的生命周期 5 | 6 | 我们用一个例子来说明一个WAL的生命周期。一个RocksDB的db实例创建了两个列族:"new_cf"和"default"。一旦db被打开,一个新的WAL会在磁盘上被创建,以保证写持久性。 7 | 8 | ```cpp 9 | DB* db; 10 | std::vector column_families; 11 | column_families.push_back(ColumnFamilyDescriptor( 12 | kDefaultColumnFamilyName, ColumnFamilyOptions())); 13 | column_families.push_back(ColumnFamilyDescriptor( 14 | "new_cf", ColumnFamilyOptions())); 15 | std::vector handles; 16 | s = DB::Open(DBOptions(), kDBPath, column_families, &handles, &db); 17 | ``` 18 | 19 | 往列族中加入一些数据: 20 | 21 | ```cpp 22 | db->Put(WriteOptions(), handles[1], Slice("key1"), Slice("value1")); 23 | db->Put(WriteOptions(), handles[0], Slice("key2"), Slice("value2")); 24 | db->Put(WriteOptions(), handles[1], Slice("key3"), Slice("value3")); 25 | db->Put(WriteOptions(), handles[0], Slice("key4"), Slice("value4")); 26 | ``` 27 | 28 | 这是,WAL需要记录所有的写操作。WAL会保持打开,并不断跟踪后续的写操作,直到他的大小到达`DBOptions::max_total_wal_size` 29 | 30 | 如果用户决定把列族"new_cf"的数据落盘,以下的事情会发生 31 | 32 | 1. new_cf的数据(key1和key3)会被落盘到一个新的SST文件 33 | 2. 一个新的WAL会被创建,现在后续的写操作都会写到新的WAL了 34 | 3. 旧的WAL不再接受新的写入,但是删除操作会被延后 35 | 36 | ```cpp 37 | db->Flush(FlushOptions(), handles[1]); 38 | // key5 与 key6 会出现在新的 WAL中 39 | db->Put(WriteOptions(), handles[1], Slice("key5"), Slice("value5")); 40 | db->Put(WriteOptions(), handles[0], Slice("key6"), Slice("value6")); 41 | ``` 42 | 43 | 这时,会有两个WAL文件,老的保存有从key1到key4的内容,新的保存key5和key6.因为老的还有线上数据,就是"defalut"列族的,他还不能被删除。只有当用户最后决定把"default"列族的数据落盘,老的WAL才能被归档,然后自动从磁盘上删除。 44 | 45 | ```cpp 46 | db->Flush(FlushOptions(), handles[0]); 47 | // 老的WAL文件会一步步地被归档,然后删除。 48 | ``` 49 | 50 | 总的来说一个WAL文件会在以下时机被创建 51 | 52 | 1. DB打开的时候 53 | 2. 一个列族落盘数据的时候 54 | 55 | 一个WAL会在他持有的所有列族的数据的最大请求序列号落盘后被删除(或者归档,如果允许归档),换句话说,所有的WAL里的数据都被固定到SST文件。归档WAL会被移到一个独立的位置,然后再从存储设备上清除。实际的删除动作可能会因为拷贝的原因被延后,参考食物日志迭代器章节。 56 | 57 | # WAl配置 58 | 59 | 60 | 下面这些配置可以在[options.h](https://github.com/facebook/rocksdb/blob/5.10.fb/include/rocksdb/options.h)中找到 61 | 62 | ## DBOptions::wal_dir 63 | 64 | DBOptions::wal_dir用于设置RocksDB存储WAL文件的目录,这允许用户把WAL和实际数据分开存储 65 | 66 | ## DBOptions::WAL_ttl_seconds, DBOptions::WAL_size_limit_MB 67 | 68 | 这两个选项影响WAL文件删除的时间。非0参数表示时间和硬盘空间的阈值,超过这个阀值,会触发删除归档的WAL文件。参考源文件以了解更多内容 69 | 70 | ## DBOptions::max_total_wal_size 71 | 72 | 如果希望限制WAL的大小,RocksDB使用DBOptions::max_total_wal_size作为列族落盘的触发器。一旦WAL超过这个大小,RocksDB会开始强制列族落盘,以保证删除最老的WAL文件。这个配置在列族以不固定频率更新的时候非常有用。如果没有大小限制,如果这个WAL中有一些非常低频更新的列族的数据没有落盘,用户可能会需要保存非常老的WAL文件。 73 | 74 | ## DBOptions::avoid_flush_during_recovery 75 | 76 | 选项名已经说明了他的用途(恢复过程中避免落盘) 77 | 78 | ## DBOptions::manual_wal_flush 79 | 80 | DBOptions::manual_wal_flush决定WAL是每次写操作之后自动flush还是纯人工flush(用户必须调用FlushWAL来触发一个WAL flush) 81 | 82 | ## DBOptions::wal_filter 83 | 84 | 通过DBOptions::wal_filter,用户可以提供一个在恢复过程中处理WAL文件时被调用的filter对象。注意:ROCKSDB_LITE模式不支持该选项。 85 | 86 | ## WriteOptions::disableWAL 87 | 88 | 如果用户依赖于其他写日志方式,或者不担心数据丢失,WriteOptions::disableWAL就非常有用了 89 | 90 | # WAL 过滤器 91 | 92 | ## 事务日志迭代器 93 | 94 | 事务日志迭代器提供一种方法,用来在RocksDB实例间复制数据。一旦一个WAL因为列族被落盘而被归档,WAL不会马上被删掉。这是为了允许事务日志迭代器可以继续读取WAL文件,再发送给从节点。 95 | 96 | ## 相关内容 97 | 98 | [WAL恢复模式](WAL-Recovery-Modes.md) 99 | [WAL日志格式](Write-Ahead-Log-File-Format.md) 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /doc/Creating-and-Ingesting-SST-files.md: -------------------------------------------------------------------------------- 1 | Rocksdb向用户提供了一系列API用于创建及导入SST文件。在你需要快速读取数据但是数据的生成是离线的时候,这非常有用。 2 | 3 | ## 创建SST文件 4 | 5 | rocksdb::SstFileWriter可以用于创建SST文件。创建了一个rocksdb::SstFileWriter 对象之后,你可以打开一个文件,插入几行数据,然后结束。 6 | 7 | 这里有一个例子,展示了如何创建SST文件/home/usr/file1.sst 8 | 9 | ```cpp 10 | Options options; 11 | SstFileWriter sst_file_writer(EnvOptions(), options); 12 | // Path to where we will write the SST file 13 | std::string file_path = "/home/usr/file1.sst"; 14 | 15 | // Open the file for writing 16 | Status s = sst_file_writer.Open(file_path); 17 | if (!s.ok()) { 18 | printf("Error while opening file %s, Error: %s\n", file_path.c_str(), 19 | s.ToString().c_str()); 20 | return 1; 21 | } 22 | 23 | // Insert rows into the SST file, note that inserted keys must be 24 | // strictly increasing (based on options.comparator) 25 | for (...) { 26 | s = sst_file_writer.Put(key, value); 27 | if (!s.ok()) { 28 | printf("Error while adding Key: %s, Error: %s\n", key.c_str(), 29 | s.ToString().c_str()); 30 | return 1; 31 | } 32 | } 33 | 34 | // Close the file 35 | s = sst_file_writer.Finish(); 36 | if (!s.ok()) { 37 | printf("Error while finishing file %s, Error: %s\n", file_path.c_str(), 38 | s.ToString().c_str()); 39 | return 1; 40 | } 41 | return 0; 42 | 43 | ``` 44 | 45 | 现在我们有了一个在/home/usr/file1.sst的SST文件了。 46 | 47 | 注意: 48 | 49 | - 传给SstFileWriter的Options会被用于指定表类型,压缩选项等,用于创建sst文件。 50 | - 传入SstFileWriter的Comparator必须与之后导入这个SST文件的DB的Comparator绝对一致。 51 | - 行必须严格按照增序插入 52 | 53 | 参考[nclude/rocksdb/sst_file_writer.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/sst_file_writer.h) 了解更多的内容 54 | 55 | ## 导入SST文件 56 | 57 | 导入SST文件非常简单,你所需要做的只是调用DB::IngestExternalFile()然后把文件地址以std::string的vector传入就行了 58 | 59 | ```cpp 60 | IngestExternalFileOptions ifo; 61 | // Ingest the 2 passed SST files into the DB 62 | Status s = db_->IngestExternalFile({"/home/usr/file1.sst", "/home/usr/file2.sst"}, ifo); 63 | if (!s.ok()) { 64 | printf("Error while adding file %s and %s, Error %s\n", 65 | file_path1.c_str(), file_path2.c_str(), s.ToString().c_str()); 66 | return 1; 67 | } 68 | ``` 69 | 70 | 参考[include/rocksdb/db.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/db.h)了解更多内容 71 | 72 | ## 导入一个文件的时候发生了什么? 73 | 74 | ### 当你调用DB::IngestExternalFile()我们会: 75 | - 把文件拷贝,或者链接到DB的目录 76 | - 阻塞DB的写入(而不是跳过),因为我们必须保证db状态的一致性,所以我们必须确保,我们能给即将导入的文件里的所有key都安全地分配正确的序列号。 77 | - 如果文件的key覆盖了memtable的键范围,把memtable刷盘 78 | - 把文件安排到LSM树的最好的层 79 | - 给文件赋值一个全局序列号 80 | - 重新启动DB的写 81 | 82 | ### 我们选择LSM树中满足以下条件的最低的一个层 83 | - 文件可以安排在这个层 84 | - 这个文件的key的范围不会覆盖上面任何一层的数据 85 | - 这个文件的key的范围不会覆盖当前层正在进行压缩 86 | 87 | ### 全局序列号 88 | 89 | 通过SstFileWriter创建的文件的元信息块里有一个特别的名为全局序号的字段,当这个字段第一次被使用,这个文件里的所有key都认为自己拥有这个序列号。当我们导入一个文件,我们给文件里的所有key都分配一个序列号。RocksDB 5.16之前,RocksDB总是用一个随机写来更新这个元数据块里的全局序列号字段。从RocksDB 5.16之后,RocksDB允许用户选择是否通过IngestExternalFileOptions::write_global_seqno更新这个字段。如果这个字段在导入的过程中没有被更新,那么RocksDB在读取文件的时候使用MANIFEST的信息以及表属性来推断全局序列号。如果底层的文件系统不支持随机写,这个路径就非常有效了。考虑到向后兼容,可以把这个选项设置为true,这样RocksDB 5.16或者更新的版本生成的SST文件就可被5.15或者更旧的Rocksdb也可以打开这个文件了。 90 | 91 | ## 下层导入 92 | 93 | 从5.5开始,IngestExternalFile加载一个外部SST文件列表的时候,支持下层导入,意思是,如果ingest_behind为true,那么重复的key会被跳过。在这种模式下,我们总是导入到最底层。文件中重复的key会被跳过,而不是覆盖已经存在的key。 94 | 95 | ### 使用场景 96 | 97 | 回读部分历史数据,而不覆盖最新的数据。这个选项只有在DB一开始运行的时候增加了allow_ingest_behind=true选项才可以使用。所有的文件都会被导入到最底层,seqno=0 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /doc/Setup-Options-and-Basic-Tuning.md: -------------------------------------------------------------------------------- 1 | # 选项 2 | 除了按照[基础操作]()里的知道使用RocksDB写代码,你可能还会想知道如何调优RocksDB使得它的性能达到预期。在这一页,我们将介绍一个初始配置,这个配置在大多数场合都应该是能满足需求的。 3 | 4 | RocksDB有非常多的配置,但是对于多数用户来说,很多选项都是可以不管的,因为他们里面的大多数,都只会影响特定的工作负荷。通常,大多数rocksDB的选项只要保持默认就好了,然而,针对以下这些选项,我们建议用户针对他们的工作负荷进行一定的试验。 5 | 6 | 首先,你需要关心资源限制相关的配置(同时参考 [基础操作]()) 7 | 8 | ## 写缓冲区大小 (Write Buffer Size) 9 | 10 | 这个可以对每个数据库和列族进行设置 11 | 12 | ### 列族写缓冲区大小 13 | 这个是一个列族可以用到的最大的缓冲区大小 14 | 15 | 它代表了用来存储写入磁盘前,没有排序过的内存数据的大小,默认值为64M。 16 | 17 | 你需要给这个值预留按照你的最坏内存使用情况 * 2 的大小。如果你的内存不足,你应该减小这个值。否则,不推荐修改这个值。例如 18 | 19 | cf_options.write_buffer_size = 64 << 20 20 | 21 | 参考下面关于列族间共享内存的配置 22 | 23 | ### 数据库写缓冲区大小 24 | 25 | 这是一个数据库里面所有跨列族写缓冲区内存的最大值。它代表了所有列族允许用于构建写盘前memtable的内存空间总大小。 26 | 27 | 这个功能默认是关闭的(设置为0)。一般不应该修改他。但是,为了举个例子,你确实需要设置他为64G的时候,可以这样做: 28 | 29 | db_options.db_write_buffer_size = 64 << 30; 30 | 31 | ## 块缓存大小(Block Cache Size) 32 | 33 | 你可以按照你希望的大小创建一个块缓存,用于缓存没有压缩的数据。 34 | 35 | 我们推荐把这个数据设置为你可用内存的三分之一。剩下的空闲内存可以用于OS的页缓存。尽量将大块的内存留给操作系统有利于避免内存不足。(参考 [RocksDB内存使用]()) 36 | 37 | 设置块缓存大小同时需要我们针对表相关的内容进行设置,例如,如果你希望使用一个128M的LRU缓存,你应该这么做: 38 | 39 | ```cpp 40 | auto cache = NewLRUCache(128 << 20); 41 | 42 | BlockBasedTableOptions table_options; 43 | table_options.block_cache = cache; 44 | 45 | auto table_factory = new BlockBasedTableFactory(table_options); 46 | cf_options.table_factory.reset(table_factory); 47 | 48 | ``` 49 | 50 | NOTE: 你应该对同一个进程的所有数据库的所有列族的table_options都使用同一个cache对象。为了达到这个目的,可以给所有列族和数据库的传递同一个tables_options或者table_factory。更多关于块存储的内容,参考 [块存储]() 51 | 52 | ## 压缩 53 | 54 | 你只能选择你的系统支持的压缩方式。使用压缩是使用CPU和IO换取存储空间 55 | 56 | cf_options.compression控制前面n-1层的压缩方式。我们推荐使用LZ4(kLZ4Compression),或者如果没有的话,使用Snappy(kSnappyCompression) 57 | 58 | cf_options.bottonmost_compression控制第n层(也就是最后一层)的压缩方式。我们推荐使用ZStand(kZSTD),或者,如果没有的话,使用Zlib(kZlibCompression) 59 | 60 | 了解更多压缩相关的内容,参考[压缩]() 61 | 62 | ## Bloom Filters 63 | 64 | 只有你确认这个符合你的查询模式的时候,才打开这个选项;如果你有点查询(Get())的需求,那么Bloom Filter可以加速这些请求,相反,如果你的查询大多数是区间扫描(例如Iterator()),那么Bloom Filter可能帮助不大 65 | 66 | Bloom Filter会使用一个key里面的部分bit位,一个好的数字是10位,会带来大概1%的假阳性结果。 67 | 68 | 如果Get操作是一个常见操作,你可以配置Bloom Filter,例如,配置为使用10个bit位: 69 | 70 | table_options.filter_policy.reset(NewBloomFilterPolicy(10, false)); 71 | 72 | 想了解更多跟Bloom Filter有关的信息,可以参考 [Bloom Filter]() 73 | 74 | ## 速度限制 75 | 76 | 有时候限制压缩和写盘的速度,是优化IO的好办法,其中一个原因是,这样可以避免异常读延迟。可以通过设置db_options.rate_limiter选项来达到目的。限速是一个复杂的话题,将在 [速度限制章节]() 详细叙述 77 | 78 | NOTE: 确保在一个进程的所有数据库中使用同一个rate_limiter对象。 79 | 80 | ## SST文件管理 81 | 82 | 如果你正在使用闪存存储,我们推荐你在挂在文件系统的时候打开discard开关,这样可以改善写放大。 83 | 84 | 如果你使用闪存存储并且打开了discard开关,那么自动整理就会开启。如果整理的空间比较大,自动整理可能会导致暂时的较大的IO延迟。SST文件管理可以控制文件删除速度,保证每次整理的空间大小是可控的。 85 | 86 | SST管理文件可以通过设置db_options.sst_file_manager来打开。关于SST文件管理的细节,可以参考这里 [sst_file_manager_impl.h](https://github.com/facebook/rocksdb/blob/5.14.fb/util/sst_file_manager_impl.h#L28) 87 | 88 | ## 其他通用选项 89 | 90 | 下面这些选项,可以帮助我们在大多数情况获得一个合理的开箱即用的性能。由于担心用户在升级新版本的rocksdb时的兼容性以及性能恶化,我们一般不修改这些选项。我们建议用户在使用新的rocksdb工程的时候使用以下选项: 91 | 92 | ```cpp 93 | cf_options.level_compaction_dynamic_level_bytes = true; 94 | options.max_background_compactions = 4; 95 | options.max_background_flushes = 2; 96 | options.bytes_per_sync = 1048576; 97 | options.compaction_pri = kMinOverlappingRatio; 98 | table_options.block_size = 16 * 1024; 99 | table_options.cache_index_and_filter_blocks = true; 100 | table_options.pin_l0_filter_and_index_blocks_in_cache = true; 101 | ``` 102 | 103 | 如果你有服务使用默认选项运行,而不是使用这些设置,也不用灰心。尽管我们认为这些选项比默认选项好,但是它们一般也不会带来明显的性能优化。 104 | 105 | ## 结论以及延展阅读 106 | 107 | 现在你可以测试你的应用,然后看看你的初始化的RocksDB的性能了。希望他们足够好。 108 | 109 | 如果按照上面的设置,你的应用已经性能足够好了,我们不推荐你进一步进行调优。由于工作压力经常变化,如果你为了提高当前工作压力下的RocksDB性能,而增加了一些无用的资源,有些不起眼的设置,也可能在未来的工作压力下导致RocksDB雪崩。 110 | 111 | 另一方面,如果性能不能满足你的要求,你可以根据 [调优指南]()进一步调优RocksDB。 112 | 113 | 114 | -------------------------------------------------------------------------------- /doc/Perf-Context-and-IO-Stats-Context.md: -------------------------------------------------------------------------------- 1 | 性能与IO上下文可以帮助用户理解每个DB操作的性能瓶颈。Options.statistics存储了从打开DB开始的所有线程的所有操作累积下来的统计信息。性能和IO统计上下文则针对每个独立的操作进行分析。 2 | 3 | 这里是性能上下文的头文件[https://github.com/facebook/rocksdb/blob/master/include/rocksdb/perf_context.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/perf_context.h) 4 | 5 | 这里是IO信息上下文的头文件: 6 | [https://github.com/facebook/rocksdb/blob/master/include/rocksdb/iostats_context.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/iostats_context.h) 7 | 8 | 他们的剖析等级受下面这个文件中的同一个方法控制: 9 | [https://github.com/facebook/rocksdb/blob/master/include/rocksdb/perf_level.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/perf_level.h) 10 | 11 | 性能和IO上下文使用同样的机制。惟一的区别是性能上下文测量RocksDB的函数,而IO状态上下文测量IO相关的调用。这些功能需要在那些等待被剖析的查询被执行的线程中打开。如果剖析等级比disable高,rocksdb会更新一个线程本地数据结构的计数器。查询之后,我们可以从该结构中读取计数器信息。 12 | 13 | # 如何使用 14 | 15 | 这里有一个使用性能和IO上下文的典型的例子: 16 | 17 | ``` 18 | #include “rocksdb/iostat_context.h” 19 | #include “rocksdb/perf_context.h” 20 | 21 | rocksdb::SetPerfLevel(rocksdb::PerfLevel::kEnableTimeExceptForMutex); 22 | 23 | rocksdb:: get_perf_context()->Reset(); 24 | rocksdb::get_iostats_context()->Reset(); 25 | 26 | ... // run your query 27 | 28 | rocksdb::SetPerfLevel(rocksdb::PerfLevel::kDisable); 29 | 30 | ... // evaluate or report variables of rocksdb::get_perf_context() and/or rocksdb::get_iostats_context() 31 | ``` 32 | 33 | 注意同样的剖析等级会被应用于性能上下文和IO上下文。 34 | 35 | 你还可以调用rocksdb::get_perf_context->ToString()和rocksdb::get_iostats_context->ToString()来得到一个易读的报告。 36 | 37 | # 剖析等级和开销 38 | 39 | 就跟平时一样,统计数量和开销之间总有一个权衡关系,我们设计了多个剖析等级供你选择: 40 | 41 | - kEnableCount 只打开计数器 42 | - kEnableTimeExceptForMutex 打开计数器统计和大多数时间开销统计,除了那些需要在一个共享互斥锁中调用一个函数的时间。 43 | - kEnableTime 进一步增加互斥锁请求和等待的时间。 44 | 45 | kEnableCount避免进一步的昂贵的系统时间获取开销,会带来更小的额外开销。我们通常使用这个等级测量所有的操作,并且在某些计数器不正常的时候报告他们。 46 | 47 | 使用kEnableTimeExceptForMutex,RocksDB的一次操作可能会调用好几次计时函数。我们通常的实践方式是在需要采样的地方打开,或者当用户需要的时候才打开。用户需要非常小心地选择采样率,因为计时函数在不同的平台的开销差异比较大。 48 | 49 | kEnableTime进一步允许了在共享互斥锁里面的计时,但是对一个操作的剖析可能会拖慢其他操作。当我们怀疑互斥锁是性能瓶颈的时候,我们使用这个等级来验证问题。 50 | 51 | 我们如何处理那些在某个等级上被关闭的计数器?如果一个计数器被关闭了,我们仅仅是不更新他们。 52 | 53 | # 统计 54 | 55 | 我们给出一些典型的例子,讲解怎么使用这些信息来解决你的问题。我们不会介绍所有的统计信息。一个完整的对所有统计信息的描述可以再头文件中找到。 56 | 57 | ## 性能上下文 58 | 59 | ### 二分搜索开销 60 | 61 | user_key_comparison_count帮助我们找出一个二分搜索里面太多的比较是不是问题的根源,特别是当一个更加昂为的比较器被使用的时候。更进一步,由于比较的次数通常因memtable的大小,level 0的SST文件的大小和其他层的大小 而有所差异,但是一个显著增加的计数器意味着有一个不合预期的LSM树结构。你可能需要检查落盘、压缩是不是能保持写速度。 62 | 63 | ### 块缓存和OS页缓存效率 64 | 65 | block_cache_hit_count告诉我们从块缓存中读取数据块的次数,block_read_count告诉我们我们不得从文件系统中读取块的次数(不管块缓存被关闭了还是缓存未命中)。我们可以通过观察这两个值计算快缓存效率。 66 | 67 | block_read_byte告诉我们有多少byte数据是从文件系统上读取的。他可以告诉我们一个慢查询是不是因为大量块需要从文件系统读取导致。索引和bloom过滤块通常是巨大的块。一个巨大的块也可以因为有一个非常大键值对。 68 | 69 | 在非常多RocksDB的设置中,我们依赖OS的页缓存来减少设备IO。事实上,在多数通用硬件设置上,我们建议用户把OS的页缓存设置到足够大来保存除了最底层以外的所有数据,这样我们可以限制一个读查询在一个IO内完成。在这种设定下,我们会发起多个文件系统调用,但是只有一个会真正读取设备。为了验证是否如此,我们可以使用计数器block_read_time来检查花费在从文件系统读取块的次数是不是如我们预期的一样。 70 | 71 | ### 墓碑 72 | 73 | 当删除一个key,RocksDB仅仅是加一个成为墓碑的标记,到memtable。在我们真正把包含有这个墓碑的key的文件压缩之前,原始的key都不会被删除。墓碑可能在原始的值被删除之后都还存在。所以如果我们有大量的连续的key被删除,一个用户可能在迭代通过这些墓碑的时候遇到慢查询。计数器internal_delete_skipped_count告诉我们有多少个墓碑被我们跳过了。internal_key_skipped_count则覆盖其他被我们跳过的key。 74 | 75 | ### Get步骤 76 | 77 | 我们可以使用"get_*"统计在一个Get查询中的步骤。最重要的两个是get_from_memtable_time和get_from_output_files_time。计数器告诉我们这个慢查询是不是因为memtable,SST文件,或者两者都有的原因导致。seek_on_memtable_time可以告诉我们花费在搜索memtable的时间。 78 | 79 | ### 写步骤 80 | 81 | "write_*"统计写操作过程中的写步骤。write_wal_time,write_memtable_time和write_delay_time告诉我们花费在写WAL,memtable的时间,或者出在减速激活状态。write_pre_and_post_process_time主要意味着花费在写队列的等待时间。如果写操作被赋值给一个提交组,但是他不是组长,write_pre_and_post_process_time会包括等待组提交组长的时间。 82 | 83 | ### 迭代操作步骤 84 | 85 | "seek_*" 和 find_next_user_entry_time把迭代操作分步。最有趣的一个是seek_child_seek_count,他告诉我们有多少子迭代,通常也就是LSM树的排序结果数量。 86 | 87 | ## IO统计上下文 88 | 89 | 我们有计数器来统计在主要文件系统调用的时间花费。如果你发现写路径有问题,写相关的计数器通常更有趣。他告诉我们,我们因为哪个文件系统调用而变慢,或者他不是因为文件系统调用导致的 90 | 91 | -------------------------------------------------------------------------------- /doc/RocksDB-Options-File.md: -------------------------------------------------------------------------------- 1 | # RocksDB选项文件 2 | 3 | 从RocksDB4.3开始,我们加了一系列功能来简化RocksDB的设置。 4 | 5 | - 每次成功调用DB::Open(),SetOptions(),以及CreateColumnFamily和DropColumnFamily被调用的时候,RocksDB的数据库都会自动将当前的配置持久化到一个文件里。 6 | - [LoadLatestOptions() / LoadOptionsFromFile()](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/utilities/options_util.h#L20-L58) :用于从一个选项文件构造RocksDB选项。 7 | - [CheckOptionsCompatibility](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/utilities/options_util.h#L64-L77) :一个用于检查两个RocksDB选项的兼容性。 8 | 9 | 通过上面这些选项文件的支持,开发者再也不用维护以前的RocksDB数据库对象的配置集。另外,如果需要修改选项,CheckOptionsCompatibility可以保证新的配置集可以在不损坏已有数据的情况下打开同一个RocksDB数据库。 10 | 11 | ## 示例 12 | 13 | 这里有一个可以运行的示例,用于展示新的功能是如何让管理RocksDB选项更简单的。一个更加完整的例子可以在[examples/options_file_example.cc](https://github.com/facebook/rocksdb/blob/master/examples/options_file_example.cc) 里找到。 14 | 15 | 假设我们打开一个RocksDB数据库,然后在运行过程中创建一个新的列族,然后关闭数据库: 16 | 17 | ```cpp 18 | s = DB::Open(rocksdb_options, path_to_db, &db); 19 | ... 20 | // 创建列族,然后rocksdb会保存这些选项。 21 | ColumnFamilyHandle* cf; 22 | s = db->CreateColumnFamily(ColumnFamilyOptions(), "new_cf", &cf); 23 | ... 24 | // 关闭 DB 25 | delete cf; 26 | delete db; 27 | ``` 28 | 29 | 从RocksDB4.3之后,每个RocksDB实例都会自动将最新的配置存储在一个配置文件,下次打开数据库的时候,我们可以利用这个配置文件来构造新的选项对象。这跟RocksDB 4.2以及更老的版本不同,我们再也不用为了打开一个数据库而记住所有列族的选项了。我们看看要如何做。 30 | 31 | 首先,我们使用LoadLatestOptions加载目标DB最新的设置: 32 | 33 | ```cpp 34 | DBOptions loaded_db_opt; 35 | std::vector loaded_cf_descs; 36 | LoadLatestOptions(path_to_db, Env::Default(), &loaded_db_opt, 37 | &loaded_cf_descs); 38 | 39 | ``` 40 | 41 | ## 不支持的选项 42 | 43 | 由于c++没有反射机制,以下需要使用用户定义的函数以及指针类型的配置项,只能被初始化为默认值。详细信息可以参考rocksdb/utilities/options_util.h: 44 | 45 | - env 46 | - memtable_factory 47 | - compaction_filter_factory 48 | - prefix_extractor 49 | - comparator 50 | - merge_operator 51 | - compaction_filter 52 | - BlockBasedTableOptions里的缓存 53 | - table_factory, 除了 BlockBasedTableFactory 54 | 55 | 对于那些不支持的用户定义函数,开发者需要手动指定他们。在这个例子,我们初始化BlockBasedTableOptions里的缓存和CompactionFilter: 56 | 57 | ```cpp 58 | for (size_t i = 0; i < loaded_cf_descs.size(); ++i) { 59 | auto* loaded_bbt_opt = reinterpret_cast( 60 | loaded_cf_descs[0].options.table_factory->GetOptions()); 61 | loaded_bbt_opt->block_cache = cache; 62 | } 63 | 64 | loaded_cf_descs[0].options.compaction_filter = new MyCompactionFilter(); 65 | 66 | ``` 67 | 68 | 现在我们执行安全性检查,确保新的选项可以安全打开目标数据库: 69 | 70 | ```cpp 71 | Status s = CheckOptionsCompatibility( 72 | kDBPath, Env::Default(), db_options, loaded_cf_descs); 73 | 74 | ``` 75 | 76 | 如果返回的值是OK,我们就可以继续使用加载起来的选项来打开目标数据库了: 77 | 78 | ```cpp 79 | s = DB::Open(loaded_db_opt, kDBPath, loaded_cf_descs, &handles, &db); 80 | ``` 81 | 82 | ## RocksDB选项文件格式 83 | 84 | RocksDB选项文件是一个 [INI文件格式](https://en.wikipedia.org/wiki/INI_file) 的text文件。每个RocksDB配置文件都一个版本号段,一个DBOptions段以及对每个列族,有一个CFOptions和TableOptions段。以下是一个示例的RocksDB选项文件。一个完整的示例可以在 [examples/rocksdb_option_file_example.ini](https://github.com/facebook/rocksdb/blob/master/examples/rocksdb_option_file_example.ini)找到 85 | 86 | ```ini 87 | [Version] 88 | rocksdb_version=4.3.0 89 | options_file_version=1.1 90 | [DBOptions] 91 | stats_dump_period_sec=600 92 | max_manifest_file_size=18446744073709551615 93 | bytes_per_sync=8388608 94 | delayed_write_rate=2097152 95 | WAL_ttl_seconds=0 96 | ... 97 | [CFOptions "default"] 98 | compaction_style=kCompactionStyleLevel 99 | compaction_filter=nullptr 100 | num_levels=6 101 | table_factory=BlockBasedTable 102 | comparator=leveldb.BytewiseComparator 103 | compression_per_level=kNoCompression:kNoCompression:kNoCompression:kSnappyCompression:kSnappyCompression:kSnappyCompression 104 | ... 105 | [TableOptions/BlockBasedTable "default"] 106 | format_version=2 107 | whole_key_filtering=true 108 | skip_table_builder_flush=false 109 | no_block_cache=false 110 | checksum=kCRC32c 111 | filter_policy=rocksdb.BuiltinBloomFilter 112 | .... 113 | ``` 114 | 115 | 116 | -------------------------------------------------------------------------------- /doc/Rocksdb-BlockBasedTable-Format.md: -------------------------------------------------------------------------------- 1 | 本页面从LevelDB的文档[表格式](https://github.com/google/leveldb/blob/master/doc/table_format.md) fork出来,然后修改了我们在开发rocksdb的时候修改的部分。 2 | 3 | RocksDB中默认的SST表格式是BlockBasedTable。 4 | 5 | # 文件格式 6 | 7 | ``` 8 | 9 | [data block 1] 10 | [data block 2] 11 | ... 12 | [data block N] 13 | [meta block 1: filter块] (参考章节: "filter" Meta Block) 14 | [meta block 2: stats块] (参考章节: "properties" Meta Block) 15 | [meta block 3: 压缩字典块] (参考章节: "compression dictionary" Meta Block) 16 | [meta block 4: 范围删除块] (参考章节: "range deletion" Meta Block) 17 | ... 18 | [meta block K: 未来拓展块] (我们以后可能会加入新的元数据块) 19 | [metaindex block] 20 | [index block] 21 | [Footer] (定长脚注,从file_size - sizeof(Footer)开始) 22 | 23 | ``` 24 | 25 | 文件包含内部指针,调用BlockHandles,包含下面信息: 26 | 27 | ``` 28 | offset: varint64 29 | size: varint64 30 | ``` 31 | 32 | 参考这个 [文件](https://developers.google.com/protocol-buffers/docs/encoding#varints)了解varint64格式 33 | 34 | (1) 键值对序列在文件中是以排序顺序排列的,并且切分成了一序列的数据块。这些块在文件头一个接一个地排列。每个数据块都根据block_builder.cc的代码进行编排(参考文件中的注释),然后选择性压缩(compress) 35 | 36 | (2) 数据块之后,我们存出一系列的元数据块。支持的元数据块类型在下面描述。更多的数据块类型可能会在以后加入。同样的,每个元数据块都根据block_builder.cc的代码进行编排(参考文件中的注释),然后选择性压缩(compress) 37 | 38 | (3) 一个metaindex块对每个元数据块都有一个对应的入口项,key为meta块的名字,值是一个BlockHandle 39 | ,指向具体的元数据块。 40 | 41 | (4) 一个索引块,对每个数据块有一个对应的入口项,key是一个string,该string >= 该数据块的最后一个key,并且小于下一个数据块的第一个key。值是数据块对应的BlockHandle。如果索引类型(IndexType)是[kTwoLevelIndexSearch](https://rocksdb.org.cn/doc/Partitioned-Index-Filters.html),这个索引块就是索引分片的第二层索引,例如,每个入口指向另一个索引块,该索引块包含每个数据块的索引。在这种情况下,格式就变成了: 42 | 43 | ``` 44 | [index block - 1st level] 45 | [index block - 1st level] 46 | ... 47 | [index block - 1st level] 48 | [index block - 2nd level] 49 | ``` 50 | 51 | (5) 在文件的最后的最后,有一个定长的脚注,包含metaindex以及索引块的BlockHandle,同事还有一个魔数: 52 | 53 | ``` 54 | metaindex_handle: char[p]; // metaindex的Block handle 55 | index_handle: char[q]; // 索引块的Block handle 56 | padding: char[40-p-q]; // 填充0以达到固定大小 57 | // (40==2*BlockHandle::kMaxEncodedLength) 58 | magic: fixed64; // 0x88e241b785f4cff7 (小端) 59 | ``` 60 | 61 | # 过滤器元数据块 62 | 63 | ## 全过滤器 64 | 65 | 在这种filter中,每个SST文件只有一个过滤块。 66 | 67 | ## 分片过滤器 68 | 69 | 全过滤器被分片到多个块。一个顶级索引块被加入到映射键表中,用于定位过滤分片。更多信息参考[这里]() 70 | 71 | ## 基于块的过滤器 72 | 73 | 基于块的过滤器,被废弃,so,我也不翻译了。。。 74 | 75 | # 属性元数据块 76 | 77 | 这个元数据块包含一系列的属性。key为属性名。值为具体的值。 78 | 79 | 统计块按下面格式排列: 80 | 81 | ``` 82 | [prop1] (每个属性都是一个键值对) 83 | [prop2] 84 | ... 85 | [propN] 86 | ``` 87 | 88 | 属性保证排序好,并且没有重复。 89 | 90 | 默认情况下,每个表提供以下属性: 91 | 92 | ``` 93 | data size // 所有数据块的总大小 94 | index size // 索引块的大小 95 | filter size // 过滤块的大小. 96 | raw key size // 未处理过的key的总大小 97 | raw value size // 未处理过的值的总大小 98 | number of entries 99 | number of data blocks 100 | ``` 101 | 102 | Rocksdb还提供用户一个“回调”来收集他们对这个表感兴趣的属性。参考UserDefinedPropertiesCollector。 103 | 104 | # 压缩字典元数据块 105 | 106 | 这个元数据块包含一个字典,在压缩库的压缩和解压每个块前导入。他的目标是解决一个基本问题,即小数据块的动态字典压缩算法:字典在块之间一次性构建,所以小的块总是小的,导致字典没啥用。 107 | 108 | 我们的解决方案是使用一个字典初始化压缩库,字典通过之前看到的数据块的数据样本建立。这个字典之后被排序到一个文件级别的元数据块,以备解压使用。这个字典的大小上限通过CompressionOptions::max_dict_bytes定义。默认为0,也就是这个块不会被生成或者排序。当前这个功能支持kZlibCompression, kLZ4Compression, kLZ4HCCompression, 和kZSTDNotFinalCompression。 109 | 110 | 更进一步,这个压缩字典只在最底层的压缩(compaction)的时候才生成,这里的数据量最大,同时也最稳定。为了避免多次遍历数据,这个字典只根据自压缩流程的第一个输出文件来取样。然后字典就被应用于压缩,并且作为输出排序好放入元数据块。如果文件比较小,一些样本间距会到超过EOF,也就是这个字典会比CompressionOptions::max_dict_bytes小一些。 111 | 112 | # 区间删除元数据块 113 | 114 | 这个元数据块包含这个文件的键范围和seqnum范围内的区间删除操作。区间删除不可以嵌入到数据块,因为这样做的话就不能使用二分搜索了。 115 | 116 | 这个快格式是标准的kv格式。一个区间删除以下面方式编码: 117 | 118 | - User key: 区间开始key 119 | - Sequence number: 区间删除操作被插入到数据库的时候的序列号 120 | - Value type: kTypeRangeDeletion 121 | - Value: 区间结束key 122 | 123 | 区间删除在插入的时候会被赋予一个seqnum,这个seqnum跟其他非区间操作类型(puts,deletes,等)用同样的机制生成。他们还会使用同样的机制在落盘和压缩(compaction)的时候在LSM树中移动。他们只有在被压缩到最后一层的时候才会被淘汰。 124 | 125 | 126 | -------------------------------------------------------------------------------- /doc/RocksDB-Bloom-Filter.md: -------------------------------------------------------------------------------- 1 | # 什么是Bloom过滤器 2 | 3 | 对于任意集合的key,一个可以用来构建一个bit数组的算法成为Bloom过滤器。给出任意key,这个bit数组可以用于决定这个key是不是*可能存在*或者*绝对不存在*于这个key集合。对于更多的细节介绍Bloom过滤器如何工作,参考这个[维基百科](http://en.wikipedia.org/wiki/Bloom_filter) 4 | 5 | 在RocksDB,当过滤策略被设置,每个新建立的SST文件会带有一个Bloom过滤器,被用于决定文件是否会包含我们在查找的key。过滤器本质上是一个bit数组。多个哈希函数会被作用在这些key上,每个决定一个bit数组中的一个是否会被set为1。读取的时候,同样的哈希函数会被应用在每个key,bit数组都会被检查,探测,然后如果有一个探测返回0,那么这个key肯定不在这个集合里。 6 | 7 | # 生命周期 8 | 9 | 在RocksDB,每个SST文件有对应的Bloom过滤器。这个过滤器在SST文件写入存储的时候被创建,并且作为SST文件的一部分存储。Bloom过滤器在每一个层都以相同的方式构建(注意,通过设置optimize_filters_for_hits,最后一层的bloom可能会被选择性跳过) 10 | 11 | Bloom过滤器可能只会通过一个key集合被创建——没有操作能结合不同的Bloom过滤器。当我们合并两个SST文件,一个新的Bloom过滤器会根据新的文件里的key进行创建。 12 | 13 | 当我们打开一个sst文件的时候,对应的bloom过滤器会同样被打开并且加载到内存。当SST文件被关闭,bloom过滤器会从内存移除。如果希望把Bloom过滤器缓存到块缓冲区,使用BlockBasedTableOptions::cache_index_and_filter_blocks=true 14 | 15 | # 基于块的bloom过滤器(旧格式) 16 | 17 | 一个Bloom过滤器只有在所有的key都能放入内存的时候被构建。换句话说,把一个bloom过滤器分片并不影响他的假阳性概率。因此,为了减轻构建SST文件的时候的内存压力,在旧的格式中,每2KB的键值对块,就建立一个独立的bloom过滤器。这个格式的细节参考[这里]()。最后,一个存储有每个bloom块的偏移的数组会被存储在SST文件中。 18 | 19 | 读取的时候,可能含有kv对的块的偏移会从SST索引中获取。根据偏移,对应的bloom过滤器会被加载。如果过滤器的结果显示key可能存在,那么他才搜索那个key实际的数据块。 20 | 21 | # 全过滤(新格式) 22 | 23 | 旧的格式的每个过滤块没有根据缓存进行对齐,搜索的时候可能会导致大量缓存未命中。更重要的是,尽管过滤器的阴性响应(也就是说key不存在)节省了数据块搜索的时间,但是索引还是会被加载到内存。新的格式,全过滤,通过构建一个针对整个SST文件的过滤器来解决这个问题。代价就是需要更多的缓存空间来存储文件中的每个key的哈希(在旧格式,只需要缓存2KB的块的key) 24 | 25 | 全过滤限制一个key的探测bit到一个相同CPU缓存流水线中。通过限制每个key的CPU缓存未命中率,保证了快速搜索。注意这本质上是把bloom空间分片并且只要我们有足够多的key,就不会影响假阳性概率。参考“数学计算”章节了解更多细节。 26 | 27 | 读取的时候,RocksDB使用与构建SST文件的时候同样的格式。用户可以设置filter_policy选项,声明新创建的SST文件的格式。helper函数NewBloomFilterPolicy可以用于帮助构建新老的基于块的过滤器(默认)以及新的全过滤器。 28 | 29 | ```cpp 30 | extern const FilterPolicy* NewBloomFilterPolicy(int bits_per_key, 31 | bool use_block_based_builder = true); 32 | } 33 | ``` 34 | 35 | # 前缀 vs 整个key 36 | 37 | 默认我们会把每个完整的key的哈希结果加入到bloom过滤器。这个可以通过设置BlockBasedTableOptions::whole_key_filtering为false来关闭。当你设置了Options.prefix_extractor,一个前缀的哈希会被加入到bloom中。由于前缀的唯一性比完整的key小,只存储前缀会生成比较小的bloom,不好的一点就是会导致更高的假阳性概率。更重要的是,在使用Seek和SeekForPrefix的时候,前缀bloom也可以用(通过在构建迭代器的时候设置check_filter),而完整key的bloom只能被用于点查询。 38 | 39 | # 统计 40 | 41 | 这里是一些统计信息,来告诉你如何获取你的全bloom过滤在生产环境的表现如何。 42 | 43 | 如果允许了前缀过滤,并且check_filter被设置,那么在每次::Seek和::SeekForPrev被调用之后会更新下面这些统计条目 44 | 45 | - rocksdb.bloom.filter.prefix.checked: seek_negatives + seek_positives 46 | - rocksdb.bloom.filter.prefix.useful: seek_negatives 47 | 48 | 在每次点查询后,会更新下面的条目。如果whole_key_filtering被设置,这是检查完整key的bloom的结果,否则这是检查前缀bloom的结果: 49 | 50 | - rocksdb.bloom.filter.useful: [true] negatives 51 | - rocksdb.bloom.filter.full.positive: positives 52 | - rocksdb.bloom.filter.full.true.positive: true positives 53 | 54 | 点查询的假阳性率可以通过(positives - true positives) / (positives - true positives + true negatives)来计算。 55 | 56 | 注意这些让人迷惑的场景: 57 | 58 | - 如果whole_key_filtering和prefix被设置,prefix在点查询的时候不会被检查 59 | - 如果只有prefix被设置,前缀bloom被校验的总次数为点查询和seeks的和。由于seek中真阳性结果的缺失,我们无法得到总的假阳性率:只能拿到点查询的。 60 | 61 | # 自定义FilterPolicy 62 | 63 | FilterPolicy(include/rocksdb/filter_policy.h)可以被继承,用于定义自己的过滤器。有两个主要的方法需要实现: 64 | 65 | ``` 66 | FilterBitsBuilder* GetFilterBitsBuilder() 67 | FilterBitsReader* GetFilterBitsReader(const Slice& contents) 68 | ``` 69 | 70 | 这样,新的过滤策略会以一个FilterBitsBuilder和FilterBitsReader的工厂来运作。FilterBitsBuilder提供key存储与过滤器生成的接口,FilterBitsReader提供校验key是否存在于过滤器中的接口。 71 | 72 | 注意:这两个新的接口只能在新的过滤器格式中工作。旧的过滤器仍旧使用旧的方法来自定义。 73 | 74 | # 分片bloom过滤器 75 | 76 | 这一全过滤的一种存储格式。他把过滤器块分片成记个小的块来缓解块缓存的加载。参考[这里]() 77 | 78 | # 数学 79 | 80 | 这里是关于bloom过滤器的假阴性率(FPR)的数学计算部分 81 | 82 | - m bit且分到s个分片,每个大小为m/s 83 | - n个key 84 | - k 个 探测/key 85 | - 对于每个key,随机选择一个分片,k bit会在分片中随机设置 86 | - 插入一个key之后,某个特定的bit仍旧为0的可能性 是 所有之前的key的哈希要么有 (s-1)/s的可能性在其他分片被设置,要么 该分片有1-1/(m/s)的概率设置其他bit 87 | - 使用二分逼近法 (1+x)^k ~= 1+ xk if |xk| << 1并且|x| < 1,我们有 88 | - prob_0 = 1-1/s + 1/s (1-sk/m) = 1 - k/m = (1-1/m)^k 89 | - 注意到prob_0(s=1)的近似值等于prob_0 90 | - 插入n个key之后,一个bit仍旧是0的概率是: 91 | - prob_0_n = (1-1/m)^kn 92 | - 这样假阳性的概率就是所有的k个bit为1: 93 | - FPR = (1 - prob_0_n) ^ k = (1- (1-1/m)^kn) ^ k 94 | 95 | 注意,FPR率不依赖于s,分片的数量。换句话说,只要sk/m << 1,分片不影响FPR。对于典型的数值s=512 且 k=6, 然后 每个key 10 bit,只要n >> 307,就能满足条件。在全过滤中,每个分片受CPU缓冲流水线允许的对齐大小和减去下次探测的cpu缓存不命中的影响。m会被设置为n * bits_per_key + epsilon来保证他是一个分片大小的整数倍,也就是cpu缓冲流水线的大小。 96 | 97 | 98 | PS.译者注:最后一段最后一句确实没看懂该怎么翻译。。。有知情人士请联系 99 | 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rocksdb-doc-cn 2 | 3 | 这个是rocksdb文档的中文翻译 4 | 5 | **严重推荐先阅读[FAQ](doc/RocksDB-FAQ.md),能解决大部分问题** 6 | 7 | 原文来自[rocksdb wiki](https://github.com/facebook/rocksdb/wiki) 8 | 9 | 中文部分会更新到[rocksdb中文网/文档](https://rocksdb.org.cn/doc.html) 10 | 11 | 本翻译没有经过任何校对,so,如果有疑惑,欢迎提issue。如果希望帮助/催更翻译一些没有的章节,同样欢迎提issue。如果你自己翻译好了,提PR什么的也不是不可以。 12 | 13 | 目前我觉得比较有意思的内容已经翻一下来了,其他内容短时间内不会更新,如果希望看某一个篇章的翻译,请提issue。 14 | 15 | # 目录 16 | 17 | - [概述](doc/OverView.md) 18 | - [FAQ](doc/RocksDB-FAQ.md) 19 | - [术语](doc/Terminology.md) 20 | - 开发者指南 21 | - [基本操作](doc/Basic-Operations.md) 22 | - [迭代器](doc/Iterator.md) 23 | - [前缀搜索](doc/Prefix-seek.md) 24 | - [向前搜索](doc/SeekForPrev.md) 25 | - [尾部迭代器](doc/Tailing-Iterator.md) 26 | - [读-修改-写操作符](doc/Merge-Operator.md) 27 | - [列族](doc/Column-Families.md) 28 | - [创建以及导入SST文件](doc/Creating-and-Ingesting-SST-files.md) 29 | - [单删除](doc/Single-Delete.md) 30 | - [低优先级写入](doc/Low-Priority-Write.md) 31 | - [生存时间(TTL)支持](doc/Time-to-Live.md) 32 | - [事务](doc/Transactions.md) 33 | - [快照](doc/Snapshot.md) 34 | - [范围删除](doc/DeleteRange.md) 35 | - [原子落盘](doc/Atomic-flush.md) 36 | - 选项 37 | - [基础选项以及调优](doc/Setup-Options-and-Basic-Tuning.md) 38 | - [选项字符串以及选项Map](doc/Option-String-and-Option-Map.md) 39 | - [配置文件](doc/RocksDB-Options-File.md) 40 | - [压缩/compression](doc/Compression.md) 41 | - [字典压缩](doc/Dictionary-Compression.md) 42 | - [IO](doc/IO.md) 43 | - [限流器](doc/Rate-Limiter.md) 44 | - [直接IO](doc/Direct-IO.md) 45 | - [后台错误处理](doc/Background-Error-Handling.md) 46 | - [MANIFEST](doc/MANIFEST.md) 47 | - [块缓存](doc/Block-Cache.md) 48 | - [Memtable](doc/MemTable.md) 49 | - [巨型页帧支持](doc/Allocating-Some-Indexes-and-Bloom-Filters-using-Huge-Page-TLB.md) 50 | - [WAL日志](doc/Write-Ahead-Log.md) 51 | - [WAL日志格式](doc/Write-Ahead-Log-File-Format.md) 52 | - [WAL恢复模式](doc/WAL-Recovery-Modes.md) 53 | - [写缓冲管理器](doc/Write-Buffer-Manager.md) 54 | - [压缩/compaction](doc/Compaction.md) 55 | - [leveled-compaction](doc/Leveled-Compaction.md) 56 | - [universal-compaction](doc/Universal-Compaction.md) 57 | - [FIFO-compaction](doc/FIFO-compaction-style.md) 58 | - [手动压缩](doc/Manual-Compaction.md) 59 | - [子压缩](doc/Sub-Compaction.md) 60 | - [选择Level压缩的文件](doc/Choose-Level-Compaction-Files.md) 61 | - [管理磁盘空间](doc/Managing-Disk-Space-Utilization.md) 62 | - SST文件格式 63 | - [基于块的表格式](doc/Rocksdb-BlockBasedTable-Format.md) 64 | - [平表](doc/PlainTable-Format.md) 65 | - [bloom过滤器](doc/RocksDB-Bloom-Filter.md) 66 | - [数据块哈希索引](doc/Data-Block-Hash-Index.md) 67 | - 日志以及监控 68 | - [日志](doc/Logger.md) 69 | - [统计](doc/Statistics.md) 70 | - [压缩统计和数据库状态](doc/Compaction-Stats-and-DB-Status.md) 71 | - [性能与IO上下文](doc/Perf-Context-and-IO-Stats-Context.md) 72 | - [事件监听器](doc/EventListener.md) 73 | - 工具/实用助手 74 | - [数据管理和访问工具](doc/Administration-and-Data-Access-Tool.md) 75 | - [checkpoint](doc/Checkpoints.md) 76 | - [如何备份RocksDB](doc/How-to-backup-RocksDB.md) 77 | - 实现细节 78 | - [删除过期文件](doc/Delete-Stale-Files.md) 79 | - [分片索引-过滤器](doc/Partitioned-Index-Filters.md) 80 | - [写预备事务](doc/WritePrepared-Transactions.md) 81 | - [写未预备事务](doc/WriteUnprepared-Transactions.md) 82 | - [我们是如何维护存活SST文件的](doc/How-we-keep-track-of-live-SST-files.md) 83 | - [优化SST文件索引以获得更好的搜索性能](doc/Indexing-SST-Files-for-Better-Lookup-Performance.md) 84 | - [合并运算实现](doc/Merge-Operator-Implementation.md) 85 | - [RocksDB修复器](doc/RocksDB-Repairer.md) 86 | - [两步提交实现](doc/Two-Phase-Commit-Implementation.md) 87 | - [迭代器的实现](doc/Iterator-Implementation.md) 88 | - [模拟缓存](doc/Simulation-Cache.md) 89 | - [持久化读缓存](doc/Persistent-Read-Cache.md) 90 | - RocksJava 91 | - [RocksJava基础](doc/RocksJava-Basics.md) 92 | - [RocksJava性能测试](doc/RocksJava-Performance-on-Flash-Storage.md) 93 | - 性能 94 | - [RocksDB内存使用](doc/Memory-usage-in-RocksDB.md) 95 | - [调优指南](doc/RocksDB-Tuning-Guide.md) 96 | - [写失速](doc/Write-Stalls.md) 97 | - [使用RocksDB实现队列服务](doc/Implement-Queue-Service-Using-RocksDB.md) 98 | 99 | # TODO 100 | 101 | - 部分链接由于没有翻译,所以暂时没有编辑上去 102 | - 校对。。。 103 | 104 | -------------------------------------------------------------------------------- /doc/Leveled-Compaction.md: -------------------------------------------------------------------------------- 1 | # 文件结构 2 | 3 | 磁盘上的文件被分成多层进行组织。我们叫他们Level-1, Level-2,等等,或者简单的L1,L2,等等。一个特殊的层,Level-0(L0),会包含刚从内存memtable落盘的数据。每个层(除了Level0)都是一个独立的排序结果 4 | 5 | ![level_structure](https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/level_structure.png) 6 | 7 | 在每一层(除了Level-0),数据被切分成多个SST文件。 8 | 9 | ![level_files](https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/level_files.png) 10 | 11 | 每一层都是一个排序结果,因为每个SST文件中的key都是排好序的(参考[基于块的表格式]())。如果需要定位一个key,我们先二分查找所有文件的起始和结束key,定位哪个文件有这个key,然后二分查找具体的文件,来定位key的位置。总的来说,就是在该层的所有key里面进行二分查找。 12 | 13 | 所有非0层都有目标大小。压缩的目标是限制这些层的数据大小。大小目标通常是指数增加。 14 | 15 | ![level_targets](https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/level_targets.png) 16 | 17 | # 压缩(compaction) 18 | 19 | 当L0的文件数量到达level0_file_num_compaction_trigger,压缩(compaction)就会被触发,L0的文件会被合并进L1。通常我们需要把所有L0的文件都选上,因为他们通常会有交集: 20 | 21 | ![pre_l0_compaction](https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/pre_l0_compaction.png) 22 | 23 | 压缩过后,可能会使得L1的大小超过目标大小: 24 | 25 | ![https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/post_l0_compaction.png](https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/post_l0_compaction.png) 26 | 27 | 这个时候,我们会选择至少一个文件,然后把它跟L2有交集的部分进行合并。生成的文件会放在L2: 28 | 29 | ![https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/pre_l1_compaction.png](https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/pre_l1_compaction.png) 30 | 31 | 如果结果仍旧超出下一层的目标大小,我们重复之前的操作 —— 选一个文件然后把它合并到下一层: 32 | 33 | ![https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/post_l1_compaction.png](https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/post_l1_compaction.png) 34 | 35 | 然后 36 | 37 | ![https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/pre_l2_compaction.png](https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/pre_l2_compaction.png) 38 | 39 | 然后 40 | 41 | ![https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/post_l2_compaction.png](https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/post_l2_compaction.png) 42 | 43 | 如果有必要,多个压缩会并发进行: 44 | 45 | ![https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/multi_thread_compaction.png](https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/multi_thread_compaction.png) 46 | 47 | 最大同时进行的压缩数由max_background_compactions控制。 48 | 49 | 然而,L0到L1的压缩不能并行。在某些情况,他可能变成压缩速度的瓶颈。在这种情况下,用户可以设置max_subcompactions为大于1。在这种情况下,我们尝试进行分片然后使用多线程来执行。 50 | 51 | ![https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/subcompaction.png](https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/subcompaction.png) 52 | 53 | # 压缩文件选择 54 | 55 | 当多个层触发压缩条件,RocksDB需要选择哪个层先进行压缩。对每个层通过下面方式生成一个分数: 56 | 57 | - 对于非0层,分数是当前层的总大小除以目标大小。如果已经有文件被选择进行压缩到下一层,这些文件的大小不会算入总大小,因为他们马上就要消失了 58 | - 对于level0,分数是文件的总数量,除以level0_file_num_compaction_trigger,或者总大小除以max_bytes_for_level_base,这个数字可能更大一些。(如果文件的大小小于level0_file_num_compaction_trigger,level 0 不会触发压缩,不管这个分数有多大) 59 | 60 | 我们比较每一层的分数,分数高的压缩优先级更高。 61 | 62 | 具体那个一个文件会被压缩会在[选择层压缩文件](Choose-Level-Compaction-Files.md)中解释。 63 | 64 | # 层的目标大小 65 | 66 | ## level_compaction_dynamic_level_bytes为false 67 | 68 | 如果level_compaction_dynamic_level_bytes为false,那么层目标这样决定: L1的目标是max_bytes_for_level_base,然后 `Target_Size(Ln+1) = Target_Size(Ln) * max_bytes_for_level_multiplier * max_bytes_for_level_multiplier_additional[n]`。max_bytes_for_level_multiplier_additional默认全为1。 69 | 70 | 举个例子,如果max_bytes_for_level_base = 16384,max_bytes_for_level_multiplier = 10 ,max_bytes_for_level_multiplier_additional没有被设置,那么L1,L2,L3与L4层的大小分别是16384, 163840, 1638400, 以及 16384000。 71 | 72 | ## level_compaction_dynamic_level_bytes为true 73 | 74 | 最后一层的目标大小(层数-1)总是层的实际大小。剩下的`Target_Size(Ln-1) = Target_Size(Ln) / max_bytes_for_level_multiplier`。如果哪一层的大小小于 `max_bytes_for_level_base / max_bytes_for_level_multiplier`,我们不会填充他。这些层会保持为空,所有L0的压缩会跳过这些层,直接到第一个符合大小的层。 75 | 76 | 举个例子,如果max_bytes_for_level_base为1GB,num_levels=6,最后一层的实际大小为276GB,那么L1-L6层的目标大小经分别为0,0,0.276GB,2.76GB,27.6GB以及276GB。 77 | 78 | 这是为了保证一个稳定的LSM树结构,如果level_compaction_dynamic_level_bytes为false,就无法保证。举个例子,在前面的例子中: 79 | 80 | ![https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/dynamic_level.png](https://github.com/facebook/rocksdb/raw/gh-pages-old/pictures/dynamic_level.png) 81 | 82 | 我们可以保证90%的数据存储在最后一层,9%的数据在倒数第二层。这样会有许多好处。 83 | 84 | # TTL 85 | 86 | 如果没有人更新一个文件中key,那么这个文件将不会进入压缩流程,而这个文件就会存活很久。例如,在某些特定场景,key会被“软删除” —— 把数值设置为空而不是直接使用Delete删除。这个“已经删除”的key范围可能不会有任何新的写入了,这样,这个数据就会在LSM里面待很长时间,浪费空间。 87 | 88 | 一个动态ttl列族选项被用于解决这个问题。文件(或者说,数据)老于TTL的数据在没有其他后台任务的时候会被定时压缩。这会让数据进入常规的压缩流程,摆脱无用的老数据。对于所有不是最底层的,比ttl要新的数据,以及所有在最底层老于ttl的数据,这还有一个(好的)副作用。注意这会导致更多的写因为RocksDB会调度更多的压缩。 89 | -------------------------------------------------------------------------------- /doc/Prefix-seek.md: -------------------------------------------------------------------------------- 1 | # 前缀搜索接口 2 | 3 | 如果你的DB或者列族的options.prefix_extractor选项有被声明,那么rocksdb就会在一个“前缀搜索”模式,具体会在下面解释。使用的例子如下: 4 | 5 | ```cpp 6 | Options options; 7 | 8 | // <---- Enable some features supporting prefix extraction 9 | options.prefix_extractor.reset(NewFixedPrefixTransform(3)); 10 | 11 | DB* db; 12 | Status s = DB::Open(options, "/tmp/rocksdb", &db); 13 | 14 | ...... 15 | 16 | auto iter = db->NewIterator(ReadOptions()); 17 | iter->Seek("foobar"); // Seek inside prefix "foo" 18 | iter->Next(); // Find next key-value pair inside prefix "foo" 19 | 20 | ``` 21 | options.prefix_extractor是一个SliceTransform类型的共享指针。通过调用SliceTransform.Transform(),我们可以我们从一个Slice里面提取出一个子串,用来代表该Slice,通常是前缀部分。在这个wiki页面,我们使用“前缀”来指代 options.prefix_extractor.Transform() 对一个 key 处理后的输出。你可以通过调用NewFixedPrefixTransform(prefix_len),获得一个使用固定长度的前缀转换器,或者你可以根据需要实现自己的转换器,然后传给options.prefix_extractor。 22 | 23 | 当options.prefix_extractor.不是nullptr,迭代器不能保证所有key都是按顺序迭代的,只能保证相同前缀的key是按顺序迭代的。当调用Iterator.Seek(lookup_key)时,RocksDB会从lookup_key里面提取前缀。与全排序模式不同,如果数据库里面有一个或者多个key满足这个前缀,RocksDB会把迭代器放在前缀相同,或者更大的key上。如果没有前缀等于或者大于lookup_key的前缀,或者在调用几次Next之后,根据ReadOptions.prefix_same_as_start是否为true,我们处理完了相同前缀的所有key,我们可能会反回Valid()=false,或者是任何一个大于前面的key的key。从4.11之后,我们支持前缀模式下使用Prev,但是仅限于迭代器仍旧在该前缀的所有键值范围内的时候。Prev的输出在迭代器已经超出范围的时候不保证正确。 24 | 25 | 当前缀模式被打开的时候,RocksDB会为了快速定位相同前缀的key或者排除不存在的前缀,自由组织数据,或者构建搜索数据。这里有些为前缀模式进行优化的支持:数据块和memtable的prefix bloom,基于哈希的memtable,还有平表格式。一个示范设定: 26 | 27 | ```cpp 28 | Options options; 29 | 30 | // Enable prefix bloom for mem tables 31 | options.prefix_extractor.reset(NewFixedPrefixTransform(3)); 32 | options.memtable_prefix_bloom_bits = 100000000; 33 | options.memtable_prefix_bloom_probes = 6; 34 | 35 | // Enable prefix bloom for SST files 36 | BlockBasedTableOptions table_options; 37 | table_options.filter_policy.reset(NewBloomFilterPolicy(10, true)); 38 | options.table_factory.reset(NewBlockBasedTableFactory(table_options)); 39 | 40 | DB* db; 41 | Status s = DB::Open(options, "/tmp/rocksdb", &db); 42 | 43 | ...... 44 | 45 | auto iter = db->NewIterator(ReadOptions()); 46 | iter->Seek("foobar"); // Seek inside prefix "foo" 47 | 48 | ``` 49 | 从3.5版本开始,我们支持通过一个配置,使得rocksdb即使在前缀模式,也能使用全顺序。调用NewIterator的时候,打开ReadOption.total_order_seek=true就可以打开这个功能。 50 | 51 | ```cpp 52 | ReadOptions read_options; 53 | read_options.total_order_seek = true; 54 | auto iter = db->NewIterator(read_options); 55 | Slice key = "foobar"; 56 | iter->Seek(key); // Seek "foobar" in total order 57 | ``` 58 | 这个模式下,性能可能会变差。请注意,并不是所有的前缀搜索实现都支持这个功能。例如,平表的实现就不支持,所以如果你尝试这么使用,你会看到一个错误码返回。基于哈希的mentable会在使用这个功能的时候做一次昂贵的在线排序。prefix bloom和使用哈希索引的基于块的表是支持这个模式的。 59 | 60 | ## 限制 61 | 62 | SeekToLast对前缀索引不友好。SeekToFirst只对部分配置有支持。如果你的迭代器需要使用这两个操作,你应该使用全排序模式。 63 | 64 | 一个常见的使用前缀索引的bug是使用逆序迭代前缀模式。这个还没有支持。如果你需要经常使用逆序迭代,你可以重新对数据进行排序,然后把前缀迭代器的顺序反过来。你可以通过自己实现一个比较器,或者使用其他方法编码你的key 65 | 66 | ## API变化(2.8->3.0) 67 | 68 | 这一节,我们会说明从2.8版本到3.0版本的API变化 69 | 70 | ### 修改前 71 | 72 | #### 全排序搜索 73 | 74 | 这就是你认为的传统的索引行为。搜索会在一个全排序的key空间进行,把迭代器定位到第一个大于或者等于你搜索的key的位置。 75 | 76 | ```cpp 77 | auto iter = db->NewIterator(ReadOptions()); 78 | Slice key = "foo_bar"; 79 | iter->Seek(key); 80 | 81 | ``` 82 | 并不是所有表组织方式都支持全排序搜索。例如,新引入的平表格式,除非使用全排序模式打开(Options.prefix_extractor == nullptr),否则就只支持基于前缀的seek。 83 | 84 | #### 使用ReadOptions.prefix 85 | 86 | 这是最不灵活的搜索方式。创建迭代器的时候需要支持前缀。 87 | 88 | ```cpp 89 | Slice prefix = "foo"; 90 | ReadOptions ro; 91 | ro.prefix = &prefix; 92 | auto iter = db->NewIterator(ro); 93 | Slice key = "foo_bar" 94 | iter->Seek(key); 95 | ``` 96 | 97 | Options.prefix_extractor需要提前设置。Seek调用会被ReadOptions提供的前缀限制,这意味着,如果你希望搜索一个新的前缀,你需要重新创建迭代器。这种方式的好处是,不相关的文件可以在创建迭代器的时候被过滤掉。所以如果你希望搜索同一个前缀的不同key,他的表现会比较好。然而,我们认为这个使用方式非常少见。 98 | 99 | #### 使用ReadOptions.prefix_seek 100 | 101 | 这个模式比 ReadOption.prefix 更加灵活。创建迭代器的时候没有预过滤。这样,通过一个迭代器就可以用于搜索不同的key和前缀了。 102 | 103 | ```cpp 104 | ReadOptions ro; 105 | ro.prefix_seek = true; 106 | auto iter = db->NewIterator(ro); 107 | Slice key = "foo_bar"; 108 | iter->Seek(key); 109 | ``` 110 | 111 | 与ReadOptions.prefix一样,Options.prefix_extractor是前置条件。 112 | 113 | ### 修改的内容 114 | 115 | 很显然,三种搜索模式让人感到混乱: 116 | 117 | - 一个模式要求另一个选项被设置(例如Options.prefix_extractor) 118 | - 对于我们的用户而言,后面两种模式哪一个在不同的场景下比较合适,不明显。 119 | 120 | 这个修改希望解决这个问题,并且把事情变得简单明了:如果Options.prefix_extractor被设置,seek默认使用前缀模式,反之亦然。动机很简单:如果Options.prefix_extractor存在,很显然,数据可以被切片,然后前缀搜索就自然匹配这种场景。使用方式就变的统一了: 121 | 122 | ```cpp 123 | auto iter = db->NewIterator(ReadOptions()); 124 | Slice key = "foo_bar"; 125 | iter->Seek(key); 126 | 127 | ``` 128 | 129 | ### 迁移到新的用法 130 | 131 | 迁移到新的用法很简单。去除Options.prefix或者Options.prefix_seek的赋值,因为他们已经被弃用了。现在,直接搜索你的key或者前缀就好了。因为Next会穿过上限,走到不同的前缀去,你可能需要检查结束状态: 132 | 133 | ```cpp 134 | auto iter = DB::NewIterator(ReadOptions()); 135 | for (iter.Seek(prefix); iter.Valid() && iter.key().starts_with(prefix); iter.Next()) { 136 | // do something 137 | } 138 | ``` 139 | 140 | -------------------------------------------------------------------------------- /doc/Iterator-Implementation.md: -------------------------------------------------------------------------------- 1 | # RocksDB迭代器 2 | 3 | RocksDB迭代器允许用户以一个排序好的顺序向后或者向前遍历db。它还拥有查找DB中的一个特定key的功能,为此,迭代器需要以一个排序好的流来访问DB。RocksDB迭代器实现类名为DBIter,在这个wiki页,我们会讨论DBIter是如何工作的,以及他的构成。在下面的图片,你可以看到DBIter的设计和构成。 4 | 5 | 6 | 7 | ![迭代器架构](../img/iterator-struct.png) 8 | 9 | 10 | # DBIter 11 | 12 | 实现: [db/db_iter.cc](https://github.com/facebook/rocksdb/blob/master/db/db_iter.cc) 13 | 接口: [Iterator](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/iterator.h) 14 | 15 | DBIter是InternalIterator(在这里是MergingIterator)的一个封装。DBIter的工作是解析InternalIterator返回的InternalKeys,然后把它们解开成用户key。 16 | 17 | 例子: 18 | 19 | 底层InternalIterator导出: 20 | 21 | ``` 22 | InternalKey(user_key="Key1", seqno=10, Type=Put) | Value = "KEY1_VAL2" 23 | InternalKey(user_key="Key1", seqno=9, Type=Put) | Value = "KEY1_VAL1" 24 | InternalKey(user_key="Key2", seqno=16, Type=Put) | Value = "KEY2_VAL2" 25 | InternalKey(user_key="Key2", seqno=15, Type=Delete) | Value = "KEY2_VAL1" 26 | InternalKey(user_key="Key3", seqno=7, Type=Delete) | Value = "KEY3_VAL1" 27 | InternalKey(user_key="Key4", seqno=5, Type=Put) | Value = "KEY4_VAL1" 28 | ``` 29 | 30 | 但是DBIter导出给用户的是 31 | 32 | ``` 33 | Key="Key1" | Value = "KEY1_VAL2" 34 | Key="Key2" | Value = "KEY2_VAL2" 35 | Key="Key4" | Value = "KEY4_VAL1" 36 | ``` 37 | 38 | # MergingIterator 39 | 40 | 实现:[table/merging_iterator.cc](https://github.com/facebook/rocksdb/blob/master/table/merging_iterator.cc) 41 | 42 | 接口:[InternalIterator](https://github.com/facebook/rocksdb/blob/master/table/internal_iterator.h) 43 | 44 | MergingIterator由非常多的子迭代器组成,MergingIterator基本就是一个迭代器的堆。在MergingIterator,我们把所有子迭代器放在一个堆里,以构成一个排序流。 45 | 46 | 例子: 47 | 48 | 底层子迭代器暴露 49 | 50 | ``` 51 | = Child Iterator 1 = 52 | InternalKey(user_key="Key1", seqno=10, Type=Put) | Value = "KEY1_VAL2" 53 | 54 | = Child Iterator 2 = 55 | InternalKey(user_key="Key1", seqno=9, Type=Put) | Value = "KEY1_VAL1" 56 | InternalKey(user_key="Key2", seqno=15, Type=Delete) | Value = "KEY2_VAL1" 57 | InternalKey(user_key="Key4", seqno=5, Type=Put) | Value = "KEY4_VAL1" 58 | 59 | = Child Iterator 3 = 60 | InternalKey(user_key="Key2", seqno=16, Type=Put) | Value = "KEY2_VAL2" 61 | InternalKey(user_key="Key3", seqno=7, Type=Delete) | Value = "KEY3_VAL1" 62 | ``` 63 | 64 | MergingIterator会把所有子迭代器保存在一个堆里,然后把它们按照一个排序流导出: 65 | 66 | ``` 67 | InternalKey(user_key="Key1", seqno=10, Type=Put) | Value = "KEY1_VAL2" 68 | InternalKey(user_key="Key1", seqno=9, Type=Put) | Value = "KEY1_VAL1" 69 | InternalKey(user_key="Key2", seqno=16, Type=Put) | Value = "KEY2_VAL2" 70 | InternalKey(user_key="Key2", seqno=15, Type=Delete) | Value = "KEY2_VAL1" 71 | InternalKey(user_key="Key3", seqno=7, Type=Delete) | Value = "KEY3_VAL1" 72 | InternalKey(user_key="Key4", seqno=5, Type=Put) | Value = "KEY4_VAL1" 73 | ``` 74 | 75 | # MemtableIterator 76 | 77 | 实现: [db/memtable.cc](https://github.com/facebook/rocksdb/blob/master/db/memtable.cc) 78 | 79 | 接口:[IternalIterator](https://github.com/facebook/rocksdb/blob/master/table/internal_iterator.h) 80 | 81 | 这是一个MemtableRep::Iterator的封装,每一个memtable分别实现自己的迭代器以导出memtable自己的kv排序流数据。 82 | 83 | # BlockIter 84 | 85 | 实现:[table/block.h](https://github.com/facebook/rocksdb/blob/master/table/block.h) 86 | 87 | 接口:[InternalIterator](https://github.com/facebook/rocksdb/blob/master/table/internal_iterator.h) 88 | 89 | 这个迭代器被用于读取SST文件的块,不管这些块是索引块还是数据块。由于SST文件块都是排序好的,并且不可变得,我们把这个块的数据加载到内存然后为排序好的数据创建一个BlockIter 90 | 91 | # TwoLevelIterator 92 | 93 | 实现:[table/two_level_iterator.cc](https://github.com/facebook/rocksdb/blob/master/table/two_level_iterator.cc) 94 | 95 | 接口:[InternalIterator](https://github.com/facebook/rocksdb/blob/master/table/internal_iterator.h) 96 | 97 | 一个TwoLevelIterator由两个迭代器构成: 98 | 99 | - 第一层迭代器(first_level_iter_) 100 | - 第二层迭代器(second_level_iter_) 101 | 102 | first_level_iter_被用于找出需要使用的second_level_iter_,second_level_iter_指向实际读取的数据。 103 | 104 | 例子: 105 | 106 | RocksDB使用TwoLevelIterator来读取SST文件,first_level_iter_指向SST文件的索引块的BlockIter,而second_level_iter_则是一个数据块的BlockIter。 107 | 108 | 来看一个简单的SST文件例子,我们有4个数据块和一个索引块: 109 | 110 | ``` 111 | [Data block, offset: 0x0000] 112 | KEY1 | VALUE1 113 | KEY2 | VALUE2 114 | KEY3 | VALUE3 115 | 116 | [Data Block, offset: 0x0100] 117 | KEY4 | VALUE4 118 | KEY7 | VALUE7 119 | 120 | [Data Block, offset: 0x0250] 121 | KEY8 | VALUE8 122 | KEY9 | VALUE9 123 | 124 | [Data Block, offset: 0x0350] 125 | KEY11 | VALUE11 126 | KEY15 | VALUE15 127 | 128 | [Index Block, offset: 0x0500] 129 | KEY3 | 0x0000 130 | KEY7 | 0x0100 131 | KEY9 | 0x0250 132 | KEY15 | 0x0500 133 | ``` 134 | 135 | 为了读这个文件,我们创建一个TwoLevelIterator: 136 | 137 | - first_level_iter_ => BlockIter指向索引块 138 | - second_level_iter_ => BlockIter指向数据块,会通过first_level_iter_来决定 139 | 140 | 比如,当我们要求TwoLevelIterator查找KEY8,他会先使用first_level_iter_(索引块的BlockIter)来找出那个块可能会包含这个key。这会找到对应的second_level_iter_(偏移0x0250的数据块的BlockIter)。我们会使用second_level_iter_来找到我们在数据块的key和value。 141 | 142 | 143 | -------------------------------------------------------------------------------- /doc/Compaction-Stats-and-DB-Status.md: -------------------------------------------------------------------------------- 1 | # 压缩统计和数据库状态 2 | 3 | 哪里开启他?你可以从以下路径找到压缩统计: 4 | 5 | 1、RocksDB每*stats_dump_period_sec* 秒导出统计信息到LOG文件中,默认600秒,也就意味着统计信息会每10分钟写入LOG文件。 6 | 7 | 2、你可以在应用中通过db->GetProperty("rocksdb.stats")得到相同的(统计)数据。 8 | 9 | 两种方法的输出都像这样: 10 | 11 | ```c++ 12 | Compaction Stats 13 | Level Files Size(MB) Score Read(GB) Rn(GB) Rnp1(GB) Write(GB) Wnew(GB) Moved(GB) W-Amp Rd(MB/s) Wr(MB/s) Comp(sec) Comp(cnt) Avg(sec) Stall(sec) Stall(cnt) Avg(ms) KeyIn KeyDrop 14 | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 15 | L0 2/0 15 0.5 0.0 0.0 0.0 32.8 32.8 0.0 0.0 0.0 23.0 1457 4346 0.335 0.00 0 0.00 0 0 16 | L1 22/0 125 1.0 163.7 32.8 130.9 165.5 34.6 0.0 5.1 25.6 25.9 6549 1086 6.031 0.00 0 0.00 1287667342 0 17 | L2 227/0 1276 1.0 262.7 34.4 228.4 262.7 34.3 0.1 7.6 26.0 26.0 10344 4137 2.500 0.00 0 0.00 1023585700 0 18 | L3 1634/0 12794 1.0 259.7 31.7 228.1 254.1 26.1 1.5 8.0 20.8 20.4 12787 3758 3.403 0.00 0 0.00 1128138363 0 19 | L4 1819/0 15132 0.1 3.9 2.0 2.0 3.6 1.6 13.1 1.8 20.1 18.4 201 206 0.974 0.00 0 0.00 91486994 0 20 | Sum 3704/0 29342 0.0 690.1 100.8 589.3 718.7 129.4 14.8 21.9 22.5 23.5 31338 13533 2.316 0.00 0 0.00 3530878399 0 21 | Int 0/0 0 0.0 2.1 0.3 1.8 2.2 0.4 0.0 24.3 24.0 24.9 91 42 2.164 0.00 0 0.00 11718977 0 22 | Flush(GB): accumulative 32.786, interval 0.091 23 | Stalls(secs): 0.000 level0_slowdown, 0.000 level0_numfiles, 0.000 memtable_compaction, 0.000 leveln_slowdown_soft, 0.000 leveln_slowdown_hard 24 | Stalls(count): 0 level0_slowdown, 0 level0_numfiles, 0 memtable_compaction, 0 leveln_slowdown_soft, 0 leveln_slowdown_hard 25 | 26 | DB Stats 27 | Uptime(secs): 128748.3 total, 300.1 interval 28 | Cumulative writes: 1288457363 writes, 14173030838 keys, 357293118 batches, 3.6 writes per batch, 3055.92 GB user ingest, stall micros: 7067721262 29 | Cumulative WAL: 1251702527 writes, 357293117 syncs, 3.50 writes per sync, 3055.92 GB written 30 | Interval writes: 3621943 writes, 39841373 keys, 1013611 batches, 3.6 writes per batch, 8797.4 MB user ingest, stall micros: 112418835 31 | Interval WAL: 3511027 writes, 1013611 syncs, 3.46 writes per sync, 8.59 MB written 32 | ``` 33 | 34 | 35 | 36 | ## 压缩统计 37 | 38 | 压缩统计在压缩N层和N+1层且输出到N+1层时进行。 39 | 40 | 下面是快速参考: 41 | 42 | - Level: 表示层级压缩的层级。 对于universal compaction,所有文件都在L0层。 **Sum** 是所有层的数据. **Int** 像 **Sum** 一样是聚合数据,但数据限制在最近一个报告周期。 43 | - Files: 有两个值 (a/b). 第一个值是在这个层级上的文件书. 第二个是正在这个层级上做压缩的文件数。 44 | - Score: 除了L0外的分数为 (当前层级的大小) / (层级的最大大小). 只要大于1,就意味着该层级需要呗压缩。对L0来说分数是当前的文件数和压缩触发值的比。 45 | - Read(GB): 层级N和N+1的压缩中读取数据的字节总数。包括从N层读的数据和N+1层读的数据。 46 | - Rn(GB): 层级N和N+1的压缩中读取N层的字节数。 47 | - Rnp1(GB): 层级N和N+1的压缩中读取N+1层的字节数 48 | - Write(GB): 层级N和N+1的压缩中写入数据的字节总数。 49 | - Wnew(GB): 写入N+1层的新数据的总字节数, 通过 (写入 N+1层的总字节数) - (压缩时从N+1层读取的字节总数)计算。 50 | - Moved(GB): 压缩时移入N+1层的字节总数. 这种情况下除了更新manifest这个文件从X层到Y层外,没有任何IO。 51 | - W-Amp: (写入N+1层的总字节数) / (从N层读取的总字节数). 这是N层到N+1层的写放大。 52 | - Rd(MB/s): 压缩N到N+1层时的读取速度。 通过 (Read(GB) * 1024) / compaction周期计算得出。 53 | - Wr(MB/s): 压缩N到N+1层时的写入速度。 见 Rd(MB/s)。 54 | - Rn(cnt): 压缩N到N+1层时从N层读取的文件总数。 55 | - Rnp1(cnt): 压缩N到N+1层时从N+1层读取的文件总数。 56 | - Wnp1(cnt): 压缩N到N+1层时写入N+1层的文件总数。 57 | - Wnew(cnt): (Wnp1(cnt) - Rnp1(cnt)) -- 压缩N到N+1层时增长的文件数 58 | - Comp(sec): 压缩N到N+1层花费的时间数。 59 | - Comp(cnt): 压缩N到N+1层的压缩任务数。 60 | - Avg(sec): 压缩N到N+1层每个压缩任务的平均耗时。 61 | - Stall(sec): 因为N+1层未压缩而导致的写暂停时间(压缩分数高于1)。 62 | - Stall(cnt): 因为N+1层未压缩而导致的写暂停数。 63 | - Avg(ms): 因为N+1层未压缩而导致的一次写入被暂停的平均时间。 64 | - KeyIn: 压缩期间写入的Key数。 65 | - KeyDrop: 压缩期间被丢弃的Key数。 66 | 67 | 68 | 69 | ### 通用统计 70 | 71 | 在每层的压缩统计之后,我们也输出一些通用的统计。通用统计报告累计数据和定期数据。累计数据报告从RocksDB启动后的数据总值。定期数据则报告从上一次统计结束后开始的数据。 72 | 73 | - Uptime(secs): total -- 从实例启动后运行的总时间, interval -- 从上一次统计数据导出后经过的时间。 74 | 75 | - Cumulative/Interval writes: total -- 总Put调用数; keys -- Put调用中包含的总key数; batches --写入的batch数 (并发下可能一个batch内有多个put请求); per batch -- 单个batch的平均大小; ingest -- 写入 DB的总大小 (不包括compactions); stall micros - 因后端compaction导致的写入暂停微秒数。 76 | 77 | - Cumulative/Interval WAL: writes -- 写入WAL的写请求数; syncs - fsync 或 fdatasync被调用次数; writes per sync - 每次syncs包含的写入请求数; GB written - 写入WAL的GB大小 78 | 79 | - Stalls: 每一种暂停类型的总数和时间 80 | 81 | level0_slowdown -- 因为 `level0_slowdown_writes_trigger`触发暂停。 82 | 83 | level0_numfiles -- 因为 `level0_stop_writes_trigger` 触发暂停。 84 | 85 | memtable_compaction -- 因为memtables数量满,flush进程无法继续触发暂停。 86 | 87 | leveln_slowdown -- 因为 `soft_rate_limit` and `hard_rate_limit`触发暂停。 -------------------------------------------------------------------------------- /doc/Option-String-and-Option-Map.md: -------------------------------------------------------------------------------- 1 | # String选项以及Map选项 2 | 3 | 用户通过Options类向RocksDB传递选项。除了通过使用Options类设置选项,还可以使用另外两(写作两,读作三)个方法设置: 4 | 5 | - 从一个选项文件生成一个选项类 6 | - 通过一个选项字符串获得 7 | - 通过一个字符串map获得 8 | 9 | ## 选项字符串 10 | 11 | 可以通过调用helper函数GetColumnFamilyOptionsFromString或者GetDBOptionsFromString,将一个带有配置信息的字符串传入,来构造选项。另外还有一个特别的函数:GetBlockBasedTableOptionsFromString和GetPlainTableOptionsFromString来获得表设置选项。 12 | 13 | 一个选项字符串如下: 14 | 15 | table_factory=PlainTable;prefix_extractor=rocksdb.CappedPrefix.13;comparator=leveldb.BytewiseComparator;compression_per_level=kBZip2Compression:kBZip2Compression:kBZip2Compression:kNoCompression:kZlibCompression:kBZip2Compression:kSnappyCompression;max_bytes_for_level_base=986;bloom_locality=8016;target_file_size_base=4294976376;memtable_huge_page_size=2557;max_successive_merges=5497;max_sequential_skip_in_iterations=4294971408;arena_block_size=1893;target_file_size_multiplier=35;min_write_buffer_number_to_merge=9;max_write_buffer_number=84;write_buffer_size=1653;max_compaction_bytes=64;max_bytes_for_level_multiplier=60;memtable_factory=SkipListFactory;compression=kNoCompression;bottommost_compression=kDisableCompressionOption;min_partial_merge_operands=7576;level0_stop_writes_trigger=33;num_levels=99;level0_slowdown_writes_trigger=22;level0_file_num_compaction_trigger=14;compaction_filter=urxcqstuwnCompactionFilter;soft_rate_limit=530.615385;soft_pending_compaction_bytes_limit=0;max_write_buffer_number_to_maintain=84;verify_checksums_in_compaction=false;merge_operator=aabcxehazrMergeOperator;memtable_prefix_bloom_size_ratio=0.4642;memtable_insert_with_hint_prefix_extractor=rocksdb.CappedPrefix.13;paranoid_file_checks=true;force_consistency_checks=true;inplace_update_num_locks=7429;optimize_filters_for_hits=false;level_compaction_dynamic_level_bytes=false;inplace_update_support=false;compaction_style=kCompactionStyleFIFO;purge_redundant_kvs_while_flush=true;hard_pending_compaction_bytes_limit=0;disable_auto_compactions=false;report_bg_io_stats=true;compaction_filter_factory=mpudlojcujCompactionFilterFactory; 16 | 17 | 每个选项都是通过 <选项名>:<选项值>,然后使用分号;进行分割。支持的选项,可以查看下面的内容。 18 | 19 | ## 选项map 20 | 21 | 类似的,用户可以通过一个string map来构造选项类,通过调用GetColumnFamilyOptionsFromMap,GetDBOptionsFromMap,GetBlockBasedTableOptionsFromMap或者GetPlainTableOptionsFromMap即可。将string到string的map传入,从字符选项名映射到字符选项值。一个字符map的选项如下: 22 | 23 | ```cpp 24 | std::unordered_map cf_options_map = { 25 | {"write_buffer_size", "1"}, 26 | {"max_write_buffer_number", "2"}, 27 | {"min_write_buffer_number_to_merge", "3"}, 28 | {"max_write_buffer_number_to_maintain", "99"}, 29 | {"compression", "kSnappyCompression"}, 30 | {"compression_per_level", 31 | "kNoCompression:" 32 | "kSnappyCompression:" 33 | "kZlibCompression:" 34 | "kBZip2Compression:" 35 | "kLZ4Compression:" 36 | "kLZ4HCCompression:" 37 | "kXpressCompression:" 38 | "kZSTD:" 39 | "kZSTDNotFinalCompression"}, 40 | {"bottommost_compression", "kLZ4Compression"}, 41 | {"compression_opts", "4:5:6:7"}, 42 | {"num_levels", "8"}, 43 | {"level0_file_num_compaction_trigger", "8"}, 44 | {"level0_slowdown_writes_trigger", "9"}, 45 | {"level0_stop_writes_trigger", "10"}, 46 | {"target_file_size_base", "12"}, 47 | {"target_file_size_multiplier", "13"}, 48 | {"max_bytes_for_level_base", "14"}, 49 | {"level_compaction_dynamic_level_bytes", "true"}, 50 | {"max_bytes_for_level_multiplier", "15.0"}, 51 | {"max_bytes_for_level_multiplier_additional", "16:17:18"}, 52 | {"max_compaction_bytes", "21"}, 53 | {"soft_rate_limit", "1.1"}, 54 | {"hard_rate_limit", "2.1"}, 55 | {"hard_pending_compaction_bytes_limit", "211"}, 56 | {"arena_block_size", "22"}, 57 | {"disable_auto_compactions", "true"}, 58 | {"compaction_style", "kCompactionStyleLevel"}, 59 | {"verify_checksums_in_compaction", "false"}, 60 | {"compaction_options_fifo", "23"}, 61 | {"max_sequential_skip_in_iterations", "24"}, 62 | {"inplace_update_support", "true"}, 63 | {"report_bg_io_stats", "true"}, 64 | {"compaction_measure_io_stats", "false"}, 65 | {"inplace_update_num_locks", "25"}, 66 | {"memtable_prefix_bloom_size_ratio", "0.26"}, 67 | {"memtable_huge_page_size", "28"}, 68 | {"bloom_locality", "29"}, 69 | {"max_successive_merges", "30"}, 70 | {"min_partial_merge_operands", "31"}, 71 | {"prefix_extractor", "fixed:31"}, 72 | {"optimize_filters_for_hits", "true"}, 73 | }; 74 | ``` 75 | 76 | 具体支持的选项,可以参考下面的章节。 77 | 78 | ## 如何找到字符串选项和map选项支持的选项 79 | 80 | 不管是字符串还是map配置,选项名映射到目标类(如DBOptions, ColumnFamilyOptions, BlockBasedTableOptions,或者PlainTableOptions)的变量名。对于DBOptions和ColumnFamilyOptions,你可以在 [options.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/options.h) 找到具体的版本,然后找到他们支持的选项列表和对应的类。另外两个选项,在 [table.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/table.h) 文件里面。 81 | 82 | 主意,尽管类里面的多数选项都是支持的,例外还是有的。你需要通过[util/options_helper.h](https://github.com/facebook/rocksdb/blob/master/util/options_helper.h) 文件里的db_options_type_info,cf_options_type_info和block_based_table_type_info找到对应版本的所有支持的选项。 83 | 84 | 如果某个选项是一个回调函数,例如 comparators,compaction filter,以及merge options,你需要把回调类的指针传入,通常还需要进行一定的类型转换。 85 | 86 | 也有例外,某些回调类支持通过字符选项或者map选项传入,如: 87 | 88 | - Prefix extractor(选项名prefix_extractor),他的值可以设置为rocksdb.FixedPrefix.<前缀长度> 或者 rocksdb.CappedPrefix.<前缀长度>。 89 | - Filter policy(选项名filter_poilcy),他的值可以设置为bloomfilter:: 90 | - Table factory (选项名 table_factory)。他的值是BlockBasedTable 或者 PlainTable。除此之外,两个特殊的选项字符串名可以用于设置这个选项,也就是block_based_table_factory或者plain_table_factory。这两个选项的值可以是BlockBasedTableOptions或者PlainTableOptions。 91 | - Memtable Factory(选项名memtable_factory)。它可以是skip_list,prefix_hash,hash_linkedlist,vector或者cuckoo。 92 | 93 | -------------------------------------------------------------------------------- /doc/Partitioned-Index-Filters.md: -------------------------------------------------------------------------------- 1 | 随着DB/内存比例变大,过滤器/索引块的内存空间变得越来越重要。尽管cache_index_and_filter_blocks允许在块缓存中只存储这些内容的一个子集,但是他们本身巨大的尺寸会通过以下方式降低性能: 2 | 3 | - 占用本来是用于缓存数据的块缓存空间。 4 | - 在未命中的时候,加载数据到内存,增加磁盘存储的压力。 5 | 6 | 这里我们讲解释问题的细节,以及如何给索引和过滤器分片,以减少消耗。 7 | 8 | ## 索引/过滤器块有多大? 9 | 10 | RocksDB默认对每个SST文件有一个索引/过滤器块。根据配置的不同,索引/过滤器块的大小也会有所不同,但是,对于一个256MB的SST文件,通常他的索引/过滤器块的大小就是0.5/5MB,比常见的4~32KB的数据块要大很多。如果索引/过滤器块能完美的加载进内存,那是完全可以的,这样他们在SST的生命周期中只要被读取一次就可以,而不是需要跟数据块竞争块缓存空间,导致他们需要被重复从磁盘读取。 11 | 12 | ## 巨大的索引/过滤器块会有什么大问题吗? 13 | 14 | 如果索引/过滤器块存储在块缓存,他们会跟数据块(以及他们自己)竞争这部分稀有资源。一个5MB的过滤器占用的空间可以被1000多个数据块(4KB大小)使用。这会导致数据块出现更多的缓存未命中。巨大的索引/过滤器同时还会把对方踢出块缓存,导致他们自己的缓存命中率下降。这会导致只有一小部分的索引/过滤器块会在他的缓存生命周期内被实际使用。 15 | 16 | 在一个索引/过滤器的缓存未命中发生后,他需要从磁盘中重新加载,他巨大的尺寸不能帮助减轻IO开销。就算是一个简单的点查询也需要从LSM的每一层读取好几个数据块,这也可能导致载入大量的索引/过滤器块。如果这种情况经常发生,磁盘甚至可能会花费比 服务数据块 更多的时间来服务索引/过滤器。 17 | 18 | ## 什么是分片索引/过滤器? 19 | 20 | 使用分片,一个SST文件的索引/过滤器会被分片成多个更小的块,然后加入一个新的上层索引给他们。当读取一个索引/过滤器的时候,只有最上层的索引被加载到内存。分片索引/过滤器之后会使用顶层索引来按需加载该查询需要使用的索引/过滤器到块缓存。顶层索引有更小的内存空间,可以被存储在堆上,或者是块缓存,具体配置依赖于cache_index_and_filter_blocks。 21 | 22 | ### 优点: 23 | 24 | - 更高的缓存命中率:不再使用巨型的索引/过滤器块污染缓存,分片允许索引/过滤器以一个更小的单位加载,因此加大了缓存空间利用率。 25 | - 更小的IO单位:当一个分片的索引/过滤器块缓存未命中发生的时候,只有一个分片需要从磁盘被载入内存,相比较读取整个索引/过滤器,他大大减小了磁盘压力。 26 | - 不需要牺牲索引/过滤器:不使用分片直接降低索引/过滤器内存使用的的策略通常是牺牲他们的准确性,比如说,使用更大的数据块或者更少的额bloom位来生成一个更小的索引和过滤器。 27 | 28 | ### 缺点: 29 | 30 | - 存储顶级索引的额外磁盘空间: 挺小的,大概是0.1~1%的索引/过滤器大小。 31 | - 更多的磁盘IO:如果顶级索引不在缓存,他会需要一个额外的IO。为了避免这种情况,他们可以存放在堆里,或者是高优先级缓存(TODO的工作)。 32 | - 失去空间局部性:如果有一个工作压力,频繁的请求,然后从同一个SST文件做随机读取,会导致每次读取都要额外读取一次独立的索引/过滤器,这比直接把整个索引读取起来要低效一些。不过我们在我们的压力测试中没有看到这种情况,他通常发生在LSM树的L0/L1层,这里分片可以关闭(TODO) 33 | 34 | ## 成功案例 35 | 36 | ### HDD, 100TB DB 37 | 38 | 在这个例子里,我们有一个86G的DB存放在HDD上,用这个DB,使用直接IO,来模拟一个100TB数据的节点的小内存(跳过OS的文件缓存),使用块大小60MB。分片把性能从 5 op/s 提升了大概11倍吞吐到55ops/s。 39 | 40 | ``` 41 | ./db_bench --benchmarks="readwhilewriting[X3],stats" --use_direct_reads=1 -compaction_readahead_size 1048576 --use_existing_db --num=2000000000 --duration 600 --cache_size=62914560 -cache_index_and_filter_blocks=false -statistics -histogram -bloom_bits=10 -target_file_size_base=268435456 -block_size=32768 -threads 32 -partition_filters -partition_indexes -index_per_partition 100 -pin_l0_filter_and_index_blocks_in_cache -benchmark_write_rate_limit 204800 -max_bytes_for_level_base 134217728 -cache_high_pri_pool_ratio 0.9 42 | ``` 43 | 44 | 45 | ### SSD,Linkbench 46 | 47 | 在这个例子里我们有一个SSD上的300GB的数据库,同样通过直接IO(跳过OS文件缓存)扮演本地另一个DB的小内存,块缓存大小为6G和2G。不使用分片的时候,把块缓存大小从6G调到2G,linkbench的吞吐从38k tps掉到 23kb。使用分片,吞吐下降为从38k掉到30k。 48 | 49 | ## 如何使用? 50 | 51 | - index_type = IndexType::kTwoLevelIndexSearch 52 | - 打开分片索引 53 | - NewBloomFilterPolicy(BITS, false) 54 | - 使用全过滤器 55 | - partition_filters = true 56 | - 为了打开分片过滤器 57 | - metadata_block_size = 4096 58 | - 索引分片的块大小 59 | - cache_index_and_filter_blocks = false [如果你的版本 <= 5.14] 60 | - 分片总是存在块缓存中。这是为了控制顶级索引的位置(可以更好的填入内存):固定在堆还是缓存到块缓存中。存储到块缓存比较少用。 61 | - cache_index_and_filter_blocks = true and pin_top_level_index_and_filter = true [如果你的版本 >= 5.15] 62 | - 把所有东西都丢进块缓存,同时固定顶级索引,这个索引不会很大的。 63 | - cache_index_and_filter_blocks_with_high_priority = true 64 | - 推荐配置 65 | - pin_l0_filter_and_index_blocks_in_cache = true 66 | - 推荐设置,因为这个属性可以拓展到索引/过滤器分片。 67 | - 只有压缩方式是基于level的压缩方式的时候才使用他 68 | - **注意**:如果固定这些块到块缓存,如果strict_capacity_limit没有设置(默认),可能会使容量过大。 69 | - 块缓存大小:如果你习惯于把过滤器/索引存储在堆里,不要忘了把你从堆那边节省出来的缓存归还给块缓存。 70 | 71 | ## 当前限制 72 | 73 | - 分片过滤器如果不使用分片索引,就不能打开。 74 | - 我们有一样数量的过滤器和索引分片。换句话说,不管索引块怎么切,过滤器块也那么切。如果这样有问题,我们以后可能会考虑改掉他。 75 | - 过滤器块大小是根据索引块切分的时候决定的。我们会尽快拓展metadata_block_size来限制过滤器和索引块的最大大小,比如,一个过滤器块会在 索引块被切割,或者,他的大小可能超过metadata_block_size的时候,被切割 76 | 77 | # 底层实现 78 | 79 | 这里展示开发者关心的实现细节。 80 | 81 | ## BlockBasedTable格式 82 | 83 | 可以在[这里]()了解BlockBasedTable格式。使用分片的时候,差异会是索引块[index block],会被存储成: 84 | 85 | ``` 86 | [index block - partition 1] 87 | [index block - partition 2] 88 | ... 89 | [index block - partition N] 90 | [index block - top-level index] 91 | ``` 92 | 93 | 然后SST的脚注指向顶级索引块(他自身就是一个索引分片块的索引)。每个独立的索引块分片遵从与kBinarySearch相同的格式。顶级索引格式同事遵从kBinarySearch,因此可以使用普通的数据块读取器来读取。 94 | 95 | 类似的结果被用于过滤器块分片。每个独立的过滤器块的格式遵从kFullFilter的格式。顶级索引格式遵从kBinarySearch的格式,类似索引块的顶级索引。 96 | 97 | 注意,对于带分片的SST文件,检查SST的工具,如sst_dump,会报告索引/过滤器上的顶级索引的大小,而不是索引和过滤器块的总大小。 98 | 99 | 100 | ## 构造器 101 | 102 | 分片索引和过滤器可以通过PartitionedIndexBuilder和PartitionedFilterBlockBuilder分别构造。 103 | 104 | PartitionedIndexBuilder维护sub_index_builder_,一个指向ShortenedIndexBuilder的指针,用来构建当前的索引分片。当通过flush_policy_指定的时候,构造器会把指针跟索引块中最后一个key一起保存,然后创建一个新的活跃ShortenedIndexBuilder。当你对这个构造器调用::Finish,他会对最早的子索引构造器调用::Finish然后返回分片块的结果。下次调用PartitionedIndexBuilder::Finish同样会包含之前返回的SST文件分片的偏移,会被用作顶级索引的值。最后一次PartitionedIndexBuilder::Finish 的调用会完成顶级索引,这次会返回该顶级索引。把顶级索引存储到SST文件之后,他的偏移会被用作索引块的偏移。 105 | 106 | PartitionedFilterBlockBuilder继承FullFilterBlockBuilder,它带有一个FilterBitsBuilder可以用于构建bloom过滤器。他也有一个指针指向PartitionedIndexBuilder,并且使用该指针调用ShouldCutFilterBlock,来决定什么时候应该吧一个过滤器块切片(在一个索引块被切片后)。为了切割一个过滤器块,他执行完FilterBitsBuilder并且把 返回的块 和 PartitionedIndexBuilder::GetPartitionKey()提供的分片key 存储在一起,然后重置FilterBitsBuilder,一边下一个分片使用。在最后一次 PartitionedFilterBlockBuilder::Finish 被调用的时候,其中一个分片会返回,并且前面分片的偏移会被用于构建顶级索引。调用::Finish会返回顶级索引块。 107 | 108 | 之所以基于PartitionedIndexBuilder实现PartitionedFilterBlockBuilder,是为了优化索引/过滤器分片在SST文件的插入。不过这个优化效果不是很明显,我们以后可能会放弃这个依赖。 109 | 110 | ## 读取器 111 | 112 | 分片索引通过PartitionIndexReader来读取,他会操作顶级索引块。当NewIterator被调用,一个TwoLevelIterator会作用于顶级索引块。这个简单的实现的原理是这样的,因为每个索引分片都是kBinarySearch格式,他们跟数据块是一样的,因此他们可以简单地插入一层迭代器来实现迭代。如果pin_l0_filter_and_index_blocks_in_cache被设置了,底层的迭代器会被固定到PartitionIndexReader,这样,只要PartitionIndexReader还存活,他们对应的索引分片都会被固定到块缓存。BlockEntryIteratorState使用一系列的固定的分片偏移来避免两次解除一个索引分片的固定。 113 | 114 | PartitionedFilterBlockReader使用顶级索引找到过滤器分片的偏移。然后调用BlockBasedTable对象的GetFilter,通过FilterBlockReader对象读取块缓存上的过滤器分片(如果没有,则从磁盘读入到这里来) ,然后释放FilterBlockReader对象。为了让table_options.pin_l0_filter_and_index_blocks_in_cache支持分片索引,PartitionedFilterBlockReader并不会释放这些块的缓存句柄(或者说,保证他们会被固定到块缓存)。他从另一个方向维护了filter_cache_,一个固定的FilterBlockReader映射表,被用于在PartitionedFilterBlockReader析构的时候释放缓存项。 115 | 116 | -------------------------------------------------------------------------------- /doc/Block-Cache.md: -------------------------------------------------------------------------------- 1 | 块缓存是Rocksdb在内存中缓存数据以用于读取的地方。用户可以带上一个期望的空间大小,传一个Cache对象给Rocksdb实例。一个缓存对象可以在同一个进程的多个RocksDB实例之间共享,这允许用户控制总的缓存大小。块缓存存储未压缩过的块。用户也可以选择设置另一个块缓存,用来存储压缩后的块。读取的时候会先拉去未压缩的数据块的缓存,然后才拉取压缩数据块的缓存。在打开直接IO的时候压缩块缓存可以替代OS的页缓存。 2 | 3 | RocksDB里面有两种实现方式,分别叫做LRUCache和ClockCache。两个类型的缓存都通过分片来减轻锁冲突。容量会被平均的分配到每个分片,分片之间不共享空间。默认情况下,每个缓存会被分片到64个分片,每个分片至少有512kB空间。 4 | 5 | # 使用 6 | 7 | 开箱即用的情况下,RocksDB会使用LRU块缓存实现,空间为8MB。如果希望使用自定义的块缓存,调用N额外LRUCache()或者NewClockCache()来创建一个缓存对象,然后把它设置到基于块的表选项。用户也可以使用自己实现的缓存,只需要实现Cache接口即可 8 | 9 | ```cpp 10 | std::shared_ptr cache = NewLRUCache(capacity); 11 | BlockBasedTableOptions table_options; 12 | table_options.block_cache = cache; 13 | Options options; 14 | options.table_factory.reset(new BlockBasedTableFactory(table_options)); 15 | ``` 16 | 17 | 如果希望设置压缩块缓存 18 | ```cpp 19 | table_options.block_cache_compressed = another_cache; 20 | ``` 21 | 22 | 如果block_cache是空指针,RocksDB会创建默认的块缓存。如果希望彻底关闭块缓存,你需要: 23 | 24 | ```cpp 25 | table_options.no_block_cache = true; 26 | ``` 27 | 28 | # LRU缓存 29 | 30 | 开箱即用的情况下,RocksDB会使用LRU块缓存实现,空间为8MB。每个缓存分片都维护自己的LRU列表以及自己的查找哈希表。通过每个分片持有一个互斥锁来实现并发。不管是查找还是插入,都需要申请该分片的互斥锁。用户可以通过调用NewLRUCache创建一个LRU缓存。函数提供了一些非常有用的选项来设置缓存: 31 | 32 | - capacity: 缓存的总大小 33 | - num_shar_bits:该缓存需要提取key的多少个bit来生成分片id。缓存会被分片成2^num_shard_bits个分片。 34 | - strict_capacity_limit:在非常少有的情况下,缓存块大小会比他的容量大。比如说,正在进行的读取操作或者整个DB的迭代器遍历操作把块钉在了块缓存里,然后被固定的块的大小超过了容量。如果后面有读取操作,希望将数据插入到块缓存中,如果strict_capacity_limit=false(这是默认的),缓存会没法按照容量设置进行处理,只能同意插入。如果主机内存不够,这可能导致未预期的OOM错误导致DB崩溃。把这个选项设置为true会拒绝进一步的缓存插入,并且导致读或者迭代失败。这个选项是按照分片粒度来工作的,也就是说有可能一个分片满的时候拒绝插入,而其他分片还有未使用的空间。 35 | - high_pri_pool_ratio:保留给高优先级块的容量的百分比。参考[缓存索引与过滤块]()章节了解更多内容 36 | 37 | # Clock缓存 38 | 39 | ClockCache实现了[CLOCK算法](https://en.wikipedia.org/wiki/Page_replacement_algorithm#Clock)。每个clock缓存分片都维护一个缓存项的环形列表。一个clock指针遍历这个环形列表来找一个没有固定的项进行驱逐,同时,如果在上一个扫描中他被使用过了,那么给予这个项两次机会来留在缓存里。tbb::concurrent_hash_map 被用来查找数据。 40 | 41 | 与LRU缓存比较,clock缓存有更好的锁粒度。在LRU缓存下面,每个分片的互斥锁在读取的时候都需要上锁,因为他需要更新他的LRU列表。在一个clock缓存上查找数据不需要申请该分片的互斥锁,只需要搜索并行的哈希表就行了,所以有更好锁粒度。只有在插入的时候需要每个分片的锁。用clock缓存,在一定环境下,我们能看到读性能的增长。(参考cache/clock_cache.cc的注释以了解这个性能测试的设置) 42 | 43 | ``` 44 | Threads Cache Cache ClockCache LRUCache 45 | Size Index/Filter Throughput(MB/s) Hit Throughput(MB/s) Hit 46 | 32 2GB yes 466.7 85.9% 433.7 86.5% 47 | 32 2GB no 529.9 72.7% 532.7 73.9% 48 | 32 64GB yes 649.9 99.9% 507.9 99.9% 49 | 32 64GB no 740.4 99.9% 662.8 99.9% 50 | 16 2GB yes 278.4 85.9% 283.4 86.5% 51 | 16 2GB no 318.6 72.7% 335.8 73.9% 52 | 16 64GB yes 391.9 99.9% 353.3 99.9% 53 | 16 64GB no 433.8 99.8% 419.4 99.8% 54 | ``` 55 | 56 | 如果需要构造一个clock缓存,调用NewClockCache,如果需要使用clock缓存,RocksDB需要与[intel TBB库](https://www.threadingbuildingblocks.org/)链接。clock缓存在构建的时候也有几个选项供用户选择: 57 | 58 | - capacity: 与LRUCache相同 59 | - num_shard_bits: 与LRUCache相同 60 | - strict_capacity_limit: 与LRUCache相同 61 | 62 | # 缓存索引以及过滤块 63 | 64 | 默认情况下,索引和过滤块都在块缓存外面存储,并且,除了max_open_files,用户无法控制使用多少内存来缓存这些块。用户可以选择在块缓存中缓存索引和过滤块,这样可以更好的控制RocksDB的缓存。在块缓存中缓存索引和过滤块: 65 | 66 | ```cpp 67 | BlockBasedTableOptions table_options; 68 | table_options.cache_index_and_filter_blocks = true; 69 | 70 | ``` 71 | 72 | 通过把索引和过滤块放入块缓存,这些块需要和数据块竞争来留存在缓存中。尽管索引和过滤块的访问频率比数据块高,也有一些情况会出现相反的情况。这不是预期行为,因为索引和过滤块一般会比数据块更大,并且他们通常留存在内存的价值更大。有两个选项用来调优减轻这些问题: 73 | 74 | - cache_index_and_filter_blocks_with_high_priority : 把块缓存中的索引和过滤块缓存设置为高优先级。目前这个选项只对LRUCcache有用,并且需要调用NewLRUCache的时候配合high_pri_pool_ratio使用。如果这个功能打开,LRU缓存中的LRU列表会分成两个部分,一个用于高优先级块,一个用于低优先级块。数据块会被插入到低优先级的池的头部。索引和过滤器块会被插入到高优先级池的头部。如果高优先级池的总大小超过了capacity * high_pri_pool_ratio,高优先级池的块会溢出到低优先级池,然后他开始跟低优先级池的数据发生竞争。淘汰会从低优先级池开始。 75 | - pin_l0_filter_and_index_blocks_in_cache:把level0的文件的索引和过滤块钉在块缓存中,避免他们被淘汰。level0的索引和过滤块通常会非常频繁的被访问。而且通常他们也比较小,所以通常来说他们不会浪费太多空间 76 | 77 | # 模拟缓存 78 | 79 | SimCache是一个工具,用来在缓存容量和数量变化的时候预测缓存命中率。他通过封装DB正在使用的真正的Cache对象,然后根据给定的容量和分片大小,运行一个后台的LRU缓存模拟,以测量缓存命中率和影子缓存的丢失率。这个工具在用户希望打开一个,比如说,有4GB的缓存大小的DB,但是希望知道如果缓存空间扩展到,比如说,64GB,的时候,缓存命中率会是多少?创建一个模拟缓存: 80 | 81 | ```cpp 82 | // This cache is the actual cache use by the DB. 83 | std::shared_ptr cache = NewLRUCache(capacity); 84 | // This is the simulated cache. 85 | std::shared_ptr sim_cache = NewSimCache(cache, sim_capacity, sim_num_shard_bits); 86 | BlockBasedTableOptions table_options; 87 | table_options.block_cache = sim_cache; 88 | ``` 89 | 90 | 模拟缓存需要的额外数据量小于sim_capacity的2% 91 | 92 | # 统计 93 | 94 | 如果Options.statistics不是null,那么会有一系列的块缓存计数器可以被访问 95 | 96 | ``` 97 | // 总块缓存不明中数 98 | // REQUIRES: BLOCK_CACHE_MISS == BLOCK_CACHE_INDEX_MISS + 99 | // BLOCK_CACHE_FILTER_MISS + 100 | // BLOCK_CACHE_DATA_MISS; 101 | BLOCK_CACHE_MISS = 0, 102 | // 总块缓存命中 103 | // REQUIRES: BLOCK_CACHE_HIT == BLOCK_CACHE_INDEX_HIT + 104 | // BLOCK_CACHE_FILTER_HIT + 105 | // BLOCK_CACHE_DATA_HIT; 106 | BLOCK_CACHE_HIT, 107 | // # of blocks added to block cache. 108 | BLOCK_CACHE_ADD, 109 | // # of failures when adding blocks to block cache. 110 | BLOCK_CACHE_ADD_FAILURES, 111 | // # of times cache miss when accessing index block from block cache. 112 | BLOCK_CACHE_INDEX_MISS, 113 | // # of times cache hit when accessing index block from block cache. 114 | BLOCK_CACHE_INDEX_HIT, 115 | // # of times cache miss when accessing filter block from block cache. 116 | BLOCK_CACHE_FILTER_MISS, 117 | // # of times cache hit when accessing filter block from block cache. 118 | BLOCK_CACHE_FILTER_HIT, 119 | // # of times cache miss when accessing data block from block cache. 120 | BLOCK_CACHE_DATA_MISS, 121 | // # of times cache hit when accessing data block from block cache. 122 | BLOCK_CACHE_DATA_HIT, 123 | // # of bytes read from cache. 124 | BLOCK_CACHE_BYTES_READ, 125 | // # of bytes written into cache. 126 | BLOCK_CACHE_BYTES_WRITE, 127 | ``` 128 | 129 | 参考[rocksdb内存使用#block-cache](https://rocksdb.org.cn/doc/Memory-usage-in-RocksDB.html#block-cache) 130 | 131 | -------------------------------------------------------------------------------- /doc/MANIFEST.md: -------------------------------------------------------------------------------- 1 | # 概述 2 | Rocksdb对文件系统以及存储介质保持不可预知的态度。文件系统操作不是原子的,并且在系统错误的时候容易出现不一致。即使打开了日志系统,文件系统还是不能在一个不合法的重启中保持一致。POSIX文件系统不支持原子化的批量操作。因此,无法依赖RocksDB的数据存储文件中的元数据文件来构建RocksDB重启前的最后的状态。 3 | 4 | RocksDB有一个内建的机制来处理这些POSIX文件系统的限制,这个机制就是保存一个名为MANIFEST的ROCKSDB状态变化的事务日志文件。MANIFEST文件用于在重启的时候,恢复rocksdb到最后一个一致的一致性状态。 5 | 6 | # 术语 7 | 8 | - MANIFEST 指通过一个事务日志,来追踪Rocksdb状态迁移的系统 9 | - Manifest日志 指一个独立的日志文件,它包含RocksDB的状态快照/版本 10 | - CURRENT 指最后的Manifest日志 11 | 12 | # 如何工作? 13 | 14 | MANIFEST是一个RocksDB状态变更的事务日志。MANIFEST由manifest日志文件以及最后的manifest文件指针组成。Manifest日志是滚动日志文件,命名方式为MANIFEST-(seq number)。seq number总是递增。CURRENT是一个特殊的文件,用于声明最新的manifest日志文件。 15 | 16 | 在系统(重新)启动的时候,最新的manifest日志文件会包含一个一致的ROCKSDB的状态。任何对RocksDB状态修改的子序列都会被记录到manifest日志文件中。当一个manifest日志超过特定的大小,一个新的manifest日志文件会更新,且保证刷盘到文件系统。成功更新CURRENT文件之后,就的manifest文件就会被删掉。 17 | 18 | ``` 19 | MANIFEST={CURRENT, MANIFEST-*} 20 | CURRENT = 指向当前manifest日志的文件指针 21 | MANIFEST- = 包含RocksDB状态的快照以及后续的修改 22 | ``` 23 | 24 | # 版本(version)编辑 25 | 26 | 一个任何时刻的RocksDB的特定状态都指向一个版本(version)(换句话说,快照)。任何针对这个版本的修改,都被认为是一个版本编辑。一个版本(或者说一个rocksDB的状态快照)由一系列的版本编辑合并构成。本质上来说,一个manifest日志文件是一个版本编辑的序列。 27 | 28 | ``` 29 | 版本编辑 = 任何RocksDB状态变更 30 | 版本 = {版本编辑*} 31 | manifest日志文件 = {版本,版本编辑*} = {版本编辑*} 32 | ``` 33 | 34 | # 版本编辑布局 35 | 36 | Manifest日志是一个版本编辑记录的序列。版本编辑记录类型是通过编辑标示号码区分的。 37 | 38 | 我们使用下列数据类型来进行编码/解码。 39 | 40 | ## 数据类型 41 | 42 | 简单数据类型 43 | 44 | - VarX - 由intX编码的变长字符 45 | - FixedX - 由intX编码的定长字符 46 | 47 | 复杂数据类型 48 | 49 | string - 带长度前缀的字符串数据: 50 | ``` 51 | +-----------+--------------------+ 52 | | size (n) | content of string | 53 | +-----------+--------------------+ 54 | |<- Var32 ->|<-- n -->| 55 | 56 | ``` 57 | 58 | ## 版本编辑记录格式 59 | 60 | 版本编辑记录使用下面的格式。解码器通过记录标示号码区分不同类别的记录 61 | 62 | ``` 63 | +-------------+------ ......... ----------+ 64 | | Record ID | Variable size record data | 65 | +-------------+------ .......... ---------+ 66 | <-- Var32 --->|<-- varies by type --> 67 | ``` 68 | 69 | ## 版本编辑记录类型以及布局 70 | 71 | 针对RocksDB不同的状态变更,有非常多样的编辑记录。 72 | 73 | 比较器编辑记录: 74 | 75 | 记录比较器的名字 76 | 77 | ``` 78 | +-------------+----------------+ 79 | | kComparator | data | 80 | +-------------+----------------+ 81 | <-- Var32 --->|<-- String -->| 82 | ``` 83 | 84 | 日志数量编辑记录: 85 | 最新的WAL日志文件数量 86 | 87 | ``` 88 | +-------------+----------------+ 89 | | kLogNumber | log number | 90 | +-------------+----------------+ 91 | <-- Var32 --->|<-- Var64 -->| 92 | ``` 93 | 94 | 上一个文件号编辑记录: 95 | 上一个manifest文件号 96 | 97 | ``` 98 | +------------------+----------------+ 99 | | kPrevFileNumber | log number | 100 | +------------------+----------------+ 101 | <-- Var32 --->|<-- Var64 -->| 102 | ``` 103 | 104 | 下一个文件号编辑记录: 105 | 下一个manifest文件号: 106 | 107 | ``` 108 | +------------------+----------------+ 109 | | kNextFileNumber | log number | 110 | +------------------+----------------+ 111 | <-- Var32 --->|<-- Var64 -->| 112 | ``` 113 | 114 | 最新的seq number编辑记录: 115 | 116 | rocksdb最新的seq number 117 | 118 | ``` 119 | +------------------+----------------+ 120 | | kLastSequence | log number | 121 | +------------------+----------------+ 122 | <-- Var32 --->|<-- Var64 -->| 123 | ``` 124 | 125 | 最大的列族编辑记录: 126 | 127 | 调整允许使用的最大列族数 128 | 129 | ``` 130 | +---------------------+----------------+ 131 | | kMaxColumnFamily | log number | 132 | +---------------------+----------------+ 133 | <-- Var32 --->|<-- Var32 -->| 134 | ``` 135 | 136 | 删除文件编辑记录: 137 | 把一个文件标记为从数据库中删除 138 | 139 | ``` 140 | +-----------------+-------------+--------------+ 141 | | kDeletedFile | level | file number | 142 | +-----------------+-------------+--------------+ 143 | <-- Var32 --->|<-- Var32 -->|<-- Var64 -->| 144 | ``` 145 | 146 | 新文件编辑记录: 147 | 把一个文件标记为新加入数据库,并且提供必要的元数据给ROCKSDB 148 | 149 | - 文件编辑记录与压缩信息 150 | 151 | ``` 152 | +--------------+-------------+--------------+------------+----------------+--------------+----------------+----------------+ 153 | | kNewFile4 | level | file number | file size | smallest_key | largest_key | smallest_seqno | largest_seq_no | 154 | +--------------+-------------+--------------+------------+----------------+--------------+----------------+----------------+ 155 | |<-- var32 -->|<-- var32 -->|<-- var64 -->|<- var64 ->|<-- String -->|<-- String -->|<-- var64 -->|<-- var64 -->| 156 | 157 | +-----------+---------------+-------+------------------+-------+--------------+ 158 | |kPathID ---| Path size(n) | path | kNeedCompaction | 1 | value (0/1) | 159 | +-----------+---------------+-------+------------------+-------+--------------+ 160 | <- var32 ->|<-- var32 -->|<- n ->|<-- var32 -->|<- 1 ->|<-- 1 -->| 161 | ``` 162 | 163 | - 文件编辑记录向后兼容 164 | 165 | ``` 166 | +--------------+-------------+--------------+------------+----------------+--------------+----------------+----------------+ 167 | | kNewFile2 | level | file number | file size | smallest_key | largest_key | smallest_seqno | largest_seq_no | 168 | +--------------+-------------+--------------+------------+----------------+--------------+----------------+----------------+ 169 | <-- var32 -->|<-- var32 -->|<-- var64 -->|<- var64 ->|<-- String -->|<-- String -->|<-- var64 -->|<-- var64 -->| 170 | ``` 171 | 172 | - 文件编辑记录与路径信息 173 | 174 | ``` 175 | +--------------+-------------+--------------+-------------+-------------+----------------+--------------+ 176 | | kNewFile3 | level | file number | Path ID | file size | smallest_key | largest_key | 177 | +--------------+-------------+--------------+-------------+-------------+----------------+--------------+ 178 | |<-- var32 -->|<-- var32 -->|<-- var64 -->|<-- var32 -->|<-- var64 -->|<-- String -->|<-- String -->| 179 | +----------------+----------------+ 180 | | smallest_seqno | largest_seq_no | 181 | +----------------+----------------+ 182 | <-- var64 -->|<-- var64 -->| 183 | ``` 184 | 185 | 列族信息编辑记录: 186 | 187 | 标记列族功能的状态(打开/关闭) 188 | 189 | ``` 190 | +------------------+----------------+ 191 | | kColumnFamily | 0/1 | 192 | +------------------+----------------+ 193 | <-- Var32 --->|<-- Var32 -->| 194 | ``` 195 | 196 | 列族增加编辑记录: 197 | 198 | 增加一个列族 199 | 200 | ``` 201 | +---------------------+----------------+ 202 | | kColumnFamilyAdd | cf name | 203 | +---------------------+----------------+ 204 | <-- Var32 --->|<-- String -->| 205 | ``` 206 | 207 | 列族删除编辑记录: 208 | 删除所有列族 209 | 210 | ``` 211 | +---------------------+ 212 | | kColumnFamilyDrop | 213 | +---------------------+ 214 | <-- Var32 --->| 215 | ``` 216 | 217 | 218 | -------------------------------------------------------------------------------- /doc/RocksJava-Basics.md: -------------------------------------------------------------------------------- 1 | RocksJava是为了给RocksDB构建一个高性能,但是易用的java驱动的工程。 2 | 3 | RocksJava由3层构成: 4 | 5 | - org.rocksdb包里面的Java类,构成RocksJava API。Java用户只会直接接触到这一层。 6 | - C++的JNI代码,提供Java API和Rock是DB之间的链接。 7 | - C++层的RocksDB本身,并且编译成了一个native库,被JNI层使用。 8 | 9 | 我们尽力是RocksJava的API和RocksDB的c++ API同步,但是他经常会落后。我们高度鼓励社区贡献代码。。。如果你需要某个特定的API在c++有但是Java没有,提PR吧 10 | 11 | 在这一页,你会学习RocksDB Java API的基础。 12 | 13 | # 开始 14 | 15 | 你可以使用我们发布的预编译好的Maven包,或者自己从代码构建RocksJava。 16 | 17 | ## Maven 18 | 19 | 我们在Maven Central发布了RocksJava,这样你就可以依赖jar包而不是自己构建了 [https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.rocksdb%22](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.rocksdb%22) 20 | 21 | 我们同时发布了通用jar包(rocksdbjni-X.X.X.jar),包含了所有支持的平台的native库和java class文件,也发布了一些小平台的jar包(例如rocksdbjni-X.X.X-linux65.jar) 22 | 23 | 在一个支持Maven风格依赖的构建系统中,最简单的使用RocksJava的方法就是增加一个RocksJava的依赖,例如,如果你是用Maven: 24 | 25 | ``` 26 | 27 | org.rocksdb 28 | rocksdbjni 29 | 5.5.1 30 | 31 | ``` 32 | 33 | **给Windows用户的备注:**如果你正在MS的Windows上使用Maven Central编译的包,他们使用Microsoft Visual Studio 2015编译的,如果你没有安装“Microsoft Visual C++ 2015 Redistributable”,那么你需要从[https://www.microsoft.com/en-us/download/details.aspx?id=48145](https://www.microsoft.com/en-us/download/details.aspx?id=48145)安装他们,或者你需要自己从源码编译(rocksdb) 34 | 35 | ## 从源代码编译 36 | 37 | 要编译RocksJava,你首先需要设置你的JAVA_HOME环境变量,指向你安装的java SDK目录(必须Java 1.7+)。你必须有预编译好的RocksDB的native库,参考[INSTALL.md](https://github.com/facebook/rocksdb/blob/master/INSTALL.md)。一旦JAVA_HOME正确设置,并且你安装了需要的库,只要通过以下命令就可以构建rocksdbjava: 38 | 39 | ``` 40 | $ make -j8 rocksdbjava 41 | ``` 42 | 43 | 这会生成rocksdbjni.jar和librocksdbjni.so(或者如果你是macOS,librocksdbjni.jnilib),位置在rocksdb根目录的java/target目录。特别的,rocksdbjni.jar包含Java类,定义了rocksdb的Java API,而librocksdbjni.so包含C++ rocksdb库和rocksdbjni.jar中定义的java类的native实现。 44 | 45 | 如果希望运行单元测试: 46 | 47 | ``` 48 | $ make jtest 49 | ``` 50 | 51 | 清理: 52 | 53 | ``` 54 | $ make jclean 55 | ``` 56 | 57 | # 样例 58 | 59 | 我们在[这里](https://github.com/facebook/rocksdb/tree/master/java/samples/src/main/java) 提供了一些使用样例,你可以直接参考代码 60 | 61 | # 内存管理 62 | 63 | RocksJava中的许多Java对象后面是C++对象,Java的对象拥有对应的控制权。由于C++没有跟Java一样的自动垃圾回收的概念,我们必须在使用完毕之后,显式释放C++对象使用的内存。 64 | 65 | 任何RocksJava中的管理了C++对象的Java对象会继承自org.rocksdb.AbstractNativeReference,当你用完这个对象后,这个父类被用来帮助管理和清理他手上的所有C++对象。有两个机制: 66 | 67 | ## AbstractNativeReference#close() 68 | 69 | 当用户使用完RocksJava的一个对象后,这个方法应该被用户显式调用。如果C++对象被分配而没有被释放,那么他们会在第一次调用这个方法的时候被释放。 70 | 71 | 为了简化使用,这个方法重载了java.lang.AutoCloseable#close(),这就允许他使用ARM (Automatic Resource Management自动资源管理)风格的构造方法,例如java SE 7的 [try-with-resources](try-with-resources)声明 72 | 73 | ## AbstractNativeReference#finalize() 74 | 75 | 当一个对象的所有存储引用都失效,并且对象就要进行垃圾回收的时候,这个方法被Java的Finalizer线程调用。他最后会委托给AbstractNativeReference#close()。不过,用户不应该依赖他,而应该认为这个是一个最后的防线。 76 | 77 | 他保证了Java对象手头的C++对象最终会被回收。但是他不能帮助RocksJava管理所有内存,因为native C++对象的内存在C++的堆上分配,然后返回给Java对象,这些对Java的GC机制是不可见的,所以JVM无法正确计算GC的内存压力。 使用完一个对象之后,**用户总是应该显式调用AbstractNativeReference#close()**。 78 | 79 | # 打开数据库 80 | 81 | 一个rocksdb数据库有一个名字,对应于文件系统上的一个文件夹。该数据库所有的数据都会存储在这个文件夹中。下面的例子展示如何打开一个数据库,如果有需要,自动创建: 82 | 83 | ```java 84 | import org.rocksdb.RocksDB; 85 | import org.rocksdb.Options; 86 | ... 87 | // a static method that loads the RocksDB C++ library. 88 | RocksDB.loadLibrary(); 89 | 90 | // the Options class contains a set of configurable DB options 91 | // that determines the behaviour of the database. 92 | try (final Options options = new Options().setCreateIfMissing(true)) { 93 | 94 | // a factory method that returns a RocksDB instance 95 | try (final RocksDB db = RocksDB.open(options, "path/to/db")) { 96 | 97 | // do something 98 | } 99 | } catch (RocksDBException e) { 100 | // do some error handling 101 | ... 102 | } 103 | ... 104 | ``` 105 | 106 | *TIP: 你可能注意到上面的RocksDBException类。这个异常类继承了java.lang.Exception,他包含了C++中的Status类,用于描述任何Rocksdb的错误* 107 | 108 | # 读写 109 | 110 | 数据库提供put,remove,和get方法用于修改、查询数据库。例如,下面的代码吧存储在key1的值移动到key2中 111 | 112 | ```java 113 | byte[] key1; 114 | byte[] key2; 115 | // some initialization for key1 and key2 116 | 117 | try { 118 | final byte[] value = db.get(key1); 119 | if (value != null) { // value == null if key1 does not exist in db. 120 | db.put(key2, value); 121 | } 122 | db.remove(key1); 123 | } catch (RocksDBException e) { 124 | // error handling 125 | } 126 | 127 | ``` 128 | 129 | *TIP:调用RocksDB.put(WriteOptions opt, byte[] key, byte[] value) 和 RocksDB.get(ReadOptions opt, byte[] key),你可以通过WriteOptions和ReadOptions控制put和get的行为* 130 | 131 | *TIP:使用int RocksDB.get(byte[] key, byte[] value)或者int RocksDB.get(ReadOptions opt, byte[] key, byte[] value),来避免在RocksDB.get()中创建一个byte数组,这两个函数的输出会填充到预分配好的输出缓冲区value中,返回的int表示value的实际长度。如果返回的长度大于value.length,意味着输出缓冲区的大小不够* 132 | 133 | # 打开一个带有列族的数据库 134 | 135 | 一个rocksdb数据库可以有多个列族。列族允许你把类似的键值对放在一起,与其他列族独立进行操作。 136 | 137 | 如果你以前使用过Rocksdb但是没有显式使用过列族,你可能惊奇地发现,你的所有操作都发生在一个列族,这个列族名为“default” 138 | 139 | 在RocksJava中使用列族的时候,一个非常重要的注意点就是,在关闭数据库的时候,需要遵从一个非常特别的顺序来析构,保证资源的正确释放。这个顺序可以通过下列代码来说明: 140 | 141 | ```java 142 | ... 143 | // a static method that loads the RocksDB C++ library. 144 | RocksDB.loadLibrary(); 145 | 146 | try (final ColumnFamilyOptions cfOpts = new ColumnFamilyOptions().optimizeUniversalStyleCompaction()) { 147 | 148 | // list of column family descriptors, first entry must always be default column family 149 | final List cfDescriptors = Arrays.asList( 150 | new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, cfOpts), 151 | new ColumnFamilyDescriptor("my-first-columnfamily".getBytes(), cfOpts) 152 | ); 153 | 154 | // a list which will hold the handles for the column families once the db is opened 155 | final List columnFamilyHandleList = 156 | new ArrayList<>(); 157 | 158 | try (final DBOptions options = new DBOptions() 159 | .setCreateIfMissing(true) 160 | .setCreateMissingColumnFamilies(true); 161 | final RocksDB db = RocksDB.open(options, 162 | "path/to/do", cfDescriptors, 163 | columnFamilyHandleList)) { 164 | 165 | try { 166 | 167 | // do something 168 | 169 | } finally { 170 | 171 | // NOTE frees the column family handles before freeing the db 172 | for (final ColumnFamilyHandle columnFamilyHandle : 173 | columnFamilyHandleList) { 174 | columnFamilyHandle.close(); 175 | } 176 | } // frees the db and the db options 177 | } 178 | } // frees the column family options 179 | ... 180 | ``` 181 | 182 | -------------------------------------------------------------------------------- /doc/Compaction.md: -------------------------------------------------------------------------------- 1 | # 关于压缩(Compaction)与压缩(Compression)的区别 2 | 3 | **这段内容在原文是没有的,翻译君实在没有什么好办法,不得不加上这段话** 4 | 5 | RocksDB涉及两个压缩概念,英文原文是Compaction和Compression。两个术语用中文翻译,都是“压缩”,实际上大家交流的时候也都是使用“压缩”。这个wiki很良心地写了两个压缩的内容,用英语能区分,但是用中文。。。嗯。。。。这里简单介绍下两个压缩的区别。 6 | 7 | compaction,在RocksDB,或者说LSM存储中,指的是把数据从Ln层,存储到Ln+1层这个过程,例如把重复的旧的数据删除之类的。 8 | 9 | compression,在这里指的是数据压缩,把1MB的数据压缩成500KB这样。数据还是那些数据,只是从明文,变成了压缩之后的数据。 10 | 11 | 他们的关系大概是: 12 | 13 | 通过Compaction把数据压缩到不同的层,每层使用不同的Compression算法压缩数据,减少存储空间。 14 | 15 | 下面开始是正文。 16 | 17 | -- 18 | 19 | 压缩算法限制LSM树的形状。他们决定了那些排序结果可以被合并以及那些排序结果需要被一个读取操作访问。你可以参考[多线程压缩]()了解关于RocksDB压缩的更多细节。 20 | 21 | # 压缩算法概述 22 | 23 | 源:[https://smalldatum.blogspot.com/2018/08/name-that-compaction-algorithm.html](https://smalldatum.blogspot.com/2018/08/name-that-compaction-algorithm.html) 24 | 25 | 这里我们展示一系列压缩算法:经典Leveled,Tiered,Tiered+Leveled,Leveled-N,FIFO。除了这些,RocksDB实现了Tiered+Leveled和termed Level,Tiered termed Universal,FIFO。 26 | 27 | ## 经典Leveled 28 | 29 | 经典Leveled压缩算法,第一次在O'Neil et al的LSM-tree论文中出现,将读操作的空间放大以及写放大最小化。 30 | 31 | LSM树是一系列的层。每一层都是一个排序结果,可以被按照范围切分成许多分片放到独立的文件中。每一层都比上一层大非常多倍。相邻层的大小倍数叫做扇出,当所有层之间的扇出都相同的时候,写放大会被最小化。把数据压缩进第N层(Ln)会把地N-1层(Ln-1)的数据合并到Ln。压缩到Ln会把之前和并进Ln的数据重写。最坏情况下,每层的写放大等于扇出数,但实际操作中,通常他会比扇出数小,Hyeontaek Lim et al的论文有相关解释。 32 | 33 | 原始LSM论文中的压缩算法使用 all-to-all的——所有Ln-1的数据会跟所有Ln层的数据合并。LevelDB和RocksDB的则是some-to-some的——Ln-1的部分数据和并进Ln层的部分数据(有覆盖的部分) 34 | 35 | 尽管leveled的写放大通常比tiered要大,但是在某些场景,leveled是有优势的。首先是按key顺序插入,一个RocksDB的优化大大减少这种场景的写放大。另一个是有倾向性的写操作,导致只有一小块的key会被更新。把RocksDB的压缩优先级设置为正确的数值,压缩过程应该在层数最小的,拥有足够空间存储写操作的层停止——他不会一致写到最大层。当leveled压缩是some-to-some模式,那么压缩只会对LSM树的一个写操作覆盖了的分片进行处理,这样可以让写放大比all-to-all模式小很多。 36 | 37 | ## Leveled-N 38 | 39 | Leveled-N跟Leveled压缩算法很像,但是会有更小的写放大,更多的读放大。它允许每层拥有大于一个排序结果。压缩合并所有Ln-1的排序结果到Ln的一个排序结果中,也就是Leveled。然后"-N“会被驾到名称中用于暗示每层可能会有n个排序结果。Dostoevsky的论文定义了一个压缩算法,名称是Fluid LSM,他最大的层有一个排序结果,但是非最大层有多于一个排序结果。leveled压缩会在最大层完成 40 | 41 | ## Tiered 42 | 43 | Tiered压缩通过增加读放大和空间放大,来最小化写放大。 44 | 45 | LSM树仍旧可以看成是Niv Dayan和Stratos Idreos论文中讲到的一系列的层。每一层有N个排序结果。每个Ln层的排序结果都比上一层的排序结果大n倍。压缩合并同一层的所有排序结果来构造一个下一层的新的排序结果。这里的N与leveled压缩的扇出类似。合并到下一层的时候,压缩不会读/重写已经排序好的Ln层的结果。每层的写放大是1,远小于Leveled的扇出。 46 | 47 | 一个比较接近Tiered的实现是合并相似大小的排序结果,不必关心层的概念(这个概念会引入一个特定大小的排序结果的目标数字)。大多数(实现)包含一些主压缩的概念,也就是包含最大的排序结果,然后还有一些条件用来触发主,和非主压缩。通常的情况是会导致大量的文件以及自己。 48 | 49 | tiered压缩也有一些挑战: 50 | 51 | - 当压缩包含一个最大层的排序结果的时候,会有一个短暂的空间放大。 52 | - 对于那些比较大的排序结果,排序结果的块索引和bloom过滤器会比较大。把他们切小块点通常是个好主意。 53 | - 大的排序结果进一步压缩会花费非常多的时间。多线程可能有帮助 54 | - 压缩过程是all-to-all的。如果写入有倾向性,并且大多数key都不更新,那么大量的排序结果都可能因为all-to-all的压缩过程倍重写。在传统的tiered算法中,没办法只重写一个大排序结果的一个子集。 55 | 56 | 对于tiered压缩,层的概念通常是一个用于构成LSM树形状的概念,以及用于估算写放大。对于RocksDB而言,他们还是一个开发细节。L0之上的层在LSM树中可以用来存储更大的排序结果。这样做的好处是可以吧排序结果切分成更小的SST文件。这减少了最大的bloom过滤器以及块索引块的大小——这对于块索引更友好——并且在分片索引/过滤倍支持之前是一个非常重要的主意。如果引入子压缩,就可以使对大块排序结果进行多线程压缩变为可能。注意RocksDB使用“universal”而不是tiered这个名字。 57 | 58 | Tiered压缩算法在RocksDB的代码里倍命名为“Universal压缩”。 59 | 60 | ## Tiered+Leveled 61 | 62 | Tiered+Leveled会有比leveled更小的写放大,以及比teired更小的空间放大。 63 | 64 | Tiered+Leveled实现方式是一种混合实现,在小的层使用tiered,在大的层使用leveld。具体哪一层切换tiered和leveled可以非常灵活。现在我假定如果Ln是leveled那么所有之后的层(Ln+1,Ln+2)都是leveled。 65 | 66 | VLDB2018的SlimDB是一个tiered+leveled的例子,机关它允许Lk层使用tiered,Ln使用leveled,而k>n。Fluid LSM倍描述为tiered+leveled的实现,但是我认为它是Leveled-N。 67 | 68 | RocksDB中的Leveled压缩也是Tiered+Leveled。遵照max_write_buffer_number的设置,可能会有N个排序结果在memtable这一层——只有一个是活跃可写的,剩下的都是只读,等待落盘的。一个memtable落盘过程类似于tiered压缩——memtable的输出在L0构建一个新的排序结果并且不需要读/重写L0上已经存在的排序结果。根据level0_file_num_compaction_trigger的配置,L0可以有多个排序结果。所以L0是Teired的。memtable层没有压缩,所以也就没有该层是tiered还是leveled的说法。RocksDB中L0的子压缩过程会更加有趣,但这是另一篇文章的内容了。 69 | 70 | ## FIFO 71 | 72 | FIFO 风格的压缩在淘汰的时候把最老的文件丢弃,可以被用于缓存数据。 73 | 74 | ## 选项 75 | 76 | 这里我们给出选项的概述以及他们如何影响压缩: 77 | 78 | - Options::compaction_style —— RocksDB目前支持两种压缩算法——Universal风格和Level风格。这个选项在这两个之间切换。可以是kCompactionStyleUniversal或者kCompactionStyleLevel。如果是kCompactionStyleUniversal,你可以用Options::compaction_options_universal配置universal风格参数。 79 | - Options::disable_auto_compactions——关闭自动压缩。你仍然可以选择手动压缩。 80 | - Options::compaction_filter——允许应用在后台压缩的时候修改/删除一个键值对。如果希望针对不同的压缩过程,使用不同的过滤器,客户需要提供一个compaction_filter_factory。用户只能声明一种压缩过滤器或者工厂。 81 | - Options::compaction_filter_factory——一个用于提供允许应用在后台压缩的时候修改/删除一个键值对的过滤器的工厂。 82 | 83 | 其他会影响压缩性能以及触发条件的选项是: 84 | - Options::access_hint_on_compaction_start——压缩启动的时候,声明文件访问模式。对于该压缩的所有文件都会应用这个选项。默认:NORMAL 85 | - Options::level0_file_num_compaction_trigger——触发level0压缩发生的文件数量。一个负数表示level-0压缩不会因为文件数量而被触发。 86 | - Options::target_file_size_base与Options::target_file_size_multiplier——压缩的目标文件大小。target_file_size_base是level-1的每个文件的大小。Level-L目标文件的大小可以通过`target_file_size_base * (target_file_size_multiplier ^ (L-1)) `来计算。比如,如果target_file_size_base为2MB,target_file_size_multiplier为10,那么level-1的每个文件大小就为2MB,level2每个文件的大小就是20MB,level3的文件就是200MB。默认的target_file_size_base为64MB,target_file_size_multiplier为1。 87 | - Options::max_compaction_bytes——所有压缩后的文件的最大大小。如果需要压缩的文件总大小大于这个值,我们在压缩的时候会避免展开更低级别的文件。 88 | - Options::max_background_compactions——后台并发执行的最大线程数,会提交给默认优先级为LOW的线程池。 89 | - Options::compaction_readahead_size——如果非零,我们在压缩的时候会做更大的读。如果你在机械硬盘上运行RocksDB,你应该把这个值设置为至少2MB。如果你不使用直接IO,我们会强制设置这个为2MB。 90 | 91 | 压缩还可以人工触发,参考 [人工触发压缩](Manual-Compaction.md) 92 | 93 | 参考rocksdb/options.h了解更多的选项信息。 94 | 95 | ## Leveled风格压缩 96 | 97 | 参考[Leveled-Compaction](Leveled-Compaction.md) 98 | 99 | ## Universal风格压缩 100 | 101 | 关于Universal风格的压缩的描述,参考[Universal-Compaction-Style](Universal-Compaction.md) 102 | 103 | 如果你正在使用Universal风格的压缩,有一个CompactionOptionsUniversal对象,会持有该风格的所有特殊压缩配置。额外的定义在rocksdb/universal_compaction.h,你可以在Options::compaction_options_universal中设置他。我们在这里简单介绍Options::compaction_options_universal: 104 | 105 | - CompactionOptionsUniversal::size_ratio —— 比较文件大小的时候的灵活性比例。如果候选文件的大小比下一个文件小1%,那么把下一个文件也包括进压缩候选集。默认:1 106 | - CompactionOptionsUniversal::min_merge_width —— 一次压缩中最小的文件数量。默认:2 107 | - CompactionOptionsUniversal::max_merge_width —— 一次压缩中最大的文件数量。默认:UINT_MAX 108 | - CompactionOptionsUniversal::max_size_amplification_percent —— 空间放大被定义为一个byte的数据存储在硬盘上需要多少额外的存储空间。例如,一个空间放大为2%意味着一个持有100byte用户数据的数据库,需要占用102byte的物理存储。通过这个定义,一个完全压缩的数据库的空间放大为0%。RocksDB用下面公式计算空间放大率:假设所有文件,除了最早的文件以外,都计算进空间放大中。默认200,这意味着一个100byte的数据库可能需要占用300byte存储。 109 | - CompactionOptionsUniversal::compression_size_percent —— 如果这个选项被设置为-1(默认值),所有的输出文件都会根据指定的压缩(compress)类型进行压缩(compress)。如果这个选项不是负数,我们会尝试确保压缩(compress)大小刚好比这个值大。正常情况下,至少这个比例的数据会被压缩。当我们把压缩(compact)到一个新的文件,他是不是需要被压缩(compress)的标准是这样的:假设根据生成时间排序的文件列表如下:[A1....An B1....Bm C1....Ct],A1是最新的,而Ct时最老的,我们将把B1...Bm压缩[compact],我们计算所有的文件大小为总的大小total_size,我们把C1...Ct的总大小计算为total_C,如果total_C / total_size < compression_size_percent,压缩(compact)输出的文件会被压缩(compress)。(这个行为看起来很诡异,但是代码确实是这么写的。。。) 110 | - CompactionOptionsUniversal::stop_style —— 停止选取下一个文件的算法条件。可以是kCompactionStopStyleSimilarSize(选择相似大小的文件)或者kCompactionStopStyleTotalSize(选取的文件的总大小>下一个文件)。默认为kCompactionStopStyleTotalSize 111 | 112 | ## FIFO压缩风格 113 | 114 | 参考 [FIFO压缩风格](FIFO-compaction-style.md) 115 | 116 | ## 线程池 117 | 118 | 压缩过程在线程池中进行,参考[线程池]() 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /doc/How-to-backup-RocksDB.md: -------------------------------------------------------------------------------- 1 | # Backup API 2 | 3 | 对于C++ APi,参考`include/rocksdb/utilities/backupable_db.h`。主要的抽象是备份引擎,会暴露一些简单的接口用于创建备份,获取备份信息,以及从备份中恢复。有两个不同的备份引擎实现:(1)`BackupEngine`用于创建新的备份,以及(2)`BackupEngineReadOnly`用于从备份恢复数据。他们都可以用于获取备份相关的信息。 4 | 5 | # 创建并校验一份备份 6 | 7 | 在RocksDB,我们实现了一个简单的方法来备份db,以及校验其正确性。这里是一个简单的例子: 8 | 9 | ```cpp 10 | #include "rocksdb/db.h" 11 | #include "rocksdb/utilities/backupable_db.h" 12 | 13 | #include 14 | 15 | using namespace rocksdb; 16 | 17 | int main() { 18 | Options options; 19 | options.create_if_missing = true; 20 | DB* db; 21 | Status s = DB::Open(options, "/tmp/rocksdb", &db); 22 | assert(s.ok()); 23 | db->Put(...); // do your thing 24 | 25 | BackupEngine* backup_engine; 26 | s = BackupEngine::Open(Env::Default(), BackupableDBOptions("/tmp/rocksdb_backup"), &backup_engine); 27 | assert(s.ok()); 28 | s = backup_engine->CreateNewBackup(db); 29 | assert(s.ok()); 30 | db->Put(...); // make some more changes 31 | s = backup_engine->CreateNewBackup(db); 32 | assert(s.ok()); 33 | 34 | std::vector backup_info; 35 | backup_engine->GetBackupInfo(&backup_info); 36 | 37 | // you can get IDs from backup_info if there are more than two 38 | s = backup_engine->VerifyBackup(1 /* ID */); 39 | assert(s.ok()); 40 | s = backup_engine->VerifyBackup(2 /* ID */); 41 | assert(s.ok()); 42 | delete db; 43 | delete backup_engine; 44 | } 45 | ``` 46 | 47 | 这个简单的例子会创建一对备份到`/tmp/rocksdb_backup`。注意你可以在同一个引擎里创建并校验多个备份。 48 | 49 | 备份正常来说是增量的(参考`BackupableDBOptions::share_table_files`)。你可以使用`BackupEngine::CreateNewBackup()`创建一个新的备份,并且只有最新的数据会被拷贝到备份目录(更多细节,参考[底层实现](#底层实现)) 50 | 51 | 一旦你有一些保存好的备份,你可以调用`BackupEngine::GetBackupInfo()`来获取一个备份列表以及时间戳对应的备份信息大小信息(注意,所有备份的和大于实际的备份目录的大小,因为有些数据会被多个备份共享)。备份可以通过自增ID做唯一标志。 52 | 53 | 当`BackupEngine::VerifyBackups()`被调用,他会检查备份目录的文件大小和db目录下原始文件的大小。然而,我们不检查校验和,因为这需要读取所有数据。注意`BackupEngine::VerifyBackups()`唯一的合理用途是 同一个引擎在因为在备份过程中,状态被捕获,被用于创建(多个)备份后,在一个备份引擎上调用他。 54 | 55 | # 从备份恢复 56 | 57 | 恢复备份也很简单: 58 | 59 | ```cpp 60 | #include "rocksdb/db.h" 61 | #include "rocksdb/utilities/backupable_db.h" 62 | 63 | using namespace rocksdb; 64 | 65 | int main() { 66 | BackupEngineReadOnly* backup_engine; 67 | Status s = BackupEngineReadOnly::Open(Env::Default(), BackupableDBOptions("/tmp/rocksdb_backup"), &backup_engine); 68 | assert(s.ok()); 69 | backup_engine->RestoreDBFromBackup(1, "/tmp/rocksdb", "/tmp/rocksdb"); 70 | delete backup_engine; 71 | } 72 | ``` 73 | 74 | 这段代码会读取/tmp/rocksdb里的第一个备份。`BackupEngineReadOnly::RestoreDBFromBackup()`的第一个参数是备份ID,第二个参数是目标DB目录,第三个是日志文件的目标地址(在某些DB,他们可能跟DB目录不同,但是大多数时候他们是同一个目录,参考Options::wal_dir了解更多信息)。`BackupEngineReadOnly::RestoreDBFromLatestBackup()`会从最新的备份里恢复DB,也就是说,ID最高的那个备份。 75 | 76 | 每个恢复的文件的校验和都要被计算,然后与备份的文件进行比较。如果校验和不一致,恢复过程会终止,并且会返回`Status::Corruption` 77 | 78 | 你必须重新打开任何线上的数据库,才能看到恢复的数据。 79 | 80 | # 备份目录结构 81 | 82 | ``` 83 | /tmp/rocksdb_backup/ 84 | ├── LATEST_BACKUP 85 | ├── meta 86 | │   └── 1 87 | ├── private 88 | │   └── 1 89 | │   ├── CURRENT 90 | │   ├── MANIFEST-000008 91 | | └── OPTIONS-000009 92 | └── shared_checksum 93 | └── 000007_1498774076_590.sst 94 | ``` 95 | 96 | `LATEST_BACKUP`是一个包含最高备份ID的文件。在我们上面的例子里,它里面有一个"1"。他被用于获取最新的备份编号,但是现在不再需要他了,因为有一个更简单的通过META文件获取的方式。这个文件会在Rocksdb5.0被移除。 97 | 98 | `meta` 目录包含一个“meta文件”,描述每一个备份,他的名字为备份ID。例如,一个meta文件包含一个包含该备份的所有文件的列表。格式在其实现文件中被描述(`utilities/backupable/backupable_db.cc`)。 99 | 100 | `private`目录总是包含非SST文件(options, current, manifest, 以及WAL)。如果`Options::share_table_files`被置空,他还会包含SST文件。 101 | 102 | `shared`目录(未展示)在设置了`Options::share_table_files`而没有设置`Options::share_files_with_checksum`的时候包含SST文件。在这个目录,文件直接用他们在原DB里的名称命名。所以他只应该被用于备份一个单一RocksDB实例;否则,文件名会冲突。 103 | 104 | `shared_checksum`目录在置了`Options::share_table_files`和`Options::share_files_with_checksum`的时候包含SST文件。在这个目录,文件用他们在原DB里的名称,大小和校验和命名。这些属性能帮助来自多个RocksDB实例的文件获得唯一标志。 105 | 106 | # 备份性能 107 | 108 | 请注意,备份引擎的`Open()`的时间开销 跟已经存在的备份数量成正比,因为我们需要初始化已经存在的每个备份的文件信息。所以如果你面向一个远程文件系统(如HDFS),而你可能有很多备份,那么初始化备份引擎可能需要花点时间,因为有网络交互的时间。我们推荐你保持备份引擎存活,并且不要每次都重新创建。 109 | 110 | 另一个加快备份引擎初始化的方法是删除不用的备份。为了删除不用的备份,只需要调用`PurgeOldBackups(N)`,N是你希望保留的备份数量。除了最新的N份备份,所有备份都会被清理。你可以通通过调用`DeleteBackup(id)`来删除任意备份。 111 | 112 | 同时注意性能也受从本地db读取然后拷贝到备份这个过程的影响。由于你可能使用不同的环境来读取和拷贝,并行瓶颈可能在任意一边出现。例如,如果本地db是HDD,使用更多的线程来备份(参考[高级使用](#高级使用))不会有效果,因为这个场景的瓶颈是磁盘度性能,可能已经饱和了。同事,一个很小的HDFS集群可能无法获得好的并发性。如果本地db在SSD而备份目标在一个大容量HDFS,就比较好了。在我们的压测下,使用16线程会减小备份时间到单线程的1/3。 113 | 114 | # 底层实现 115 | 116 | 当你调用`BackupEngine::CreateNewBackup()`,他执行一下动作: 117 | 118 | 1. 关闭文件删除 119 | 2. 获取存活文件(包括表文件,current,选线和manifest文件) 120 | 3. 拷贝存货文件到备份目录。由于表文件是不可修改,并且文件名唯一,如果备份目录下已经有这个文件了,我们就不拷贝他了。例如,如果有一个文件叫`00050.sst`在备份目录里,然后`GetLiveFiles()`返回了`00050.sst`,我们不会拷贝这个文件到备份目录。然而,不管文件是否被拷贝,所有文件的校验和都需要被计算。如果一个文件已经存在,算出来的校验和会跟之前算出来的校验和做比较,以确保没有意料外的事情发生。如果校验和不一致,备份会终止,系统恢复到`BackupEngine::CreateNewBackup()`调用前的状态。值得注意的是,一个备份终止可能意味着当前db中的文件出错,也可能是备份的文件出错。另外,manifest和current文件总是被拷贝到private目录,因为他们不是不可变得。 121 | 4. 如果`flush_before_backup`为false,我们还需要把WAL拷贝到备份目录。我们调用`GetSortedWalFiles()`然后拷贝所有存活文件到备份目录。 122 | 5. 重新打开文件删除。 123 | 124 | # 高级使用 125 | 126 | 我们可以恢复用户定义的元数据备份。把你的元数据传给`BackupEngine::CreateNewBackupWithMetadata()`然后后续通过`BackupEngine::GetBackupInfo()`读取。例如,这个可以使用不同于我们的自增id的自定义id,以用于区分备份。 127 | 128 | 我们现在也备份和恢复option文件了。回复后,你可以从db目录使用`rocksdb::LoadLatestOptions()`或者`rocksdb:: LoadOptionsFromFile()`加载option。限制是,并不是options里的所有内容都可以转换成一个文件中的text。加载并恢复后,你仍旧需要一些步骤来手工设置一些没有设置的信息。好消息是,你比以前需要做的事情少了许多。 129 | 130 | 你需要初始化一些env,然后给backup_target初始化`BackupableDBOptions::backup_env`。把你的备份根目录写入`BackupableDBOptions::backup_dir`在该目录下,文件会按照上述说明组织。 131 | 132 | `BackupableDBOptions::share_table_files`控制备份是否增量完成。如果为真,SST文件会去到"shared"目录。如果不同的SST文件使用同一个名称,就会造成冲突(比如说,如果多个数据库使用同一个备份目标文件夹)。 133 | 134 | `BackupableDBOptions::share_files_with_checksum`控制共享文件如何被区分。如果为真,共享SST文件会通过校验和,大小,以及序列号进行区分。这可以防止上述多个数据库使用同一个目录带来的冲突。 135 | 136 | `BackupableDBOptions::max_background_operations`控制备份和恢复的时候用于拷贝文件的线程数。对于分布式文件系统,如HDFS,增加拷贝并发数可以获得很好的收益。 137 | 138 | `BackupableDBOptions::info_log`是一个Logger对象,非空时,用于打印LOG日志。参考[Logger](Logger.md) 139 | 140 | 如果`BackupableDBOptions::sync`为真,每次文件写,我们都会使用`fsync(2)`来同步文件数据和元数据到磁盘上,保证重启或者机器崩溃后的备份持久化。设置为false会加快一点速度,但是一些(更新的)备份可能不能持久化。尽管在大多数情况下,一切都相安无事。 141 | 142 | 如果你设置`BackupableDBOptions::destroy_old_data`为true,创建新的`BackupEngin`会删除目标备份目录下的所有旧的备份。 143 | 144 | `BackupEngine::CreateNewBackup()`方法需要一个参数`flush_before_backup`,默认为false。如果`flush_before_backup`为true,`BackupEngine`会先发起一个memtable落盘,之后才开始拷贝DB文件到备份目录。这样会不再拷贝WAL文件到备份目录(因为落盘后会删除他们)。如果`flush_before_backup`为false,备份不会发起落盘。这时,备份会包括memtable对应的WAL文件。不管`flush_before_backup`为何,备份总是与当前状态一致。 145 | 146 | # 进一步阅读 147 | 148 | 对于具体实现,参考`utilities/backupable/backupable_db.cc` 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /doc/Terminology.md: -------------------------------------------------------------------------------- 1 | # 术语 2 | 3 | **迭代器(Iterator)**: 迭代器被用户用于按顺序查询一个区间内的键值。参考 4 | [https://rocksdb.org.cn/doc/Basic-Operations.html#iteration](https://rocksdb.org.cn/doc/Basic-Operations.html#iteration) 5 | 6 | **点查询**:在RocksDB,点查询意味着通过 Get()读取一个键的值。 7 | 8 | **区间查询**:区间查询意味着通过迭代器读取一个区间内的键值。 9 | 10 | **SST文件(数据文件/SST表)**:SST是Sorted Sequence Table(排序队列表)。他们是排好序的数据文件。在这些文件里,所有键都按照排序好的顺序组织,一个键或者一个迭代位置可以通过二分查找进行定位。 11 | 12 | **索引(Index)**: SST文件里的数据块索引。他会被保存成SST文件里的一个索引块。默认的索引个是是二分搜索索引。 13 | 14 | **分区索引(Partitioned Index)**:被分割成许多更小的块的二分搜索索引。参考[https://rocksdb.org.cn/doc/Partitioned-Index-Filters.html](https://rocksdb.org.cn/doc/Partitioned-Index-Filters.html) 15 | 16 | **LSM树**:参考定义:[https://en.wikipedia.org/wiki/Log-structured_merge-tree](https://en.wikipedia.org/wiki/Log-structured_merge-tree),RocksDB是一个基于LSM树的存储引擎 17 | 18 | **前置写日志(WAL)或者日志(LOG)**:一个在RocksDB重启的时候,用于恢复没有刷入SST文件的数据的文件。参考 [https://rocksdb.org.cn/doc/Write-Ahead-Log-File-Format.html](https://rocksdb.org.cn/doc/Write-Ahead-Log-File-Format.html) 19 | 20 | **memtable/写缓冲(write buffer)**:在内存中存储最新更新的数据的数据结构。通常它会按顺序组织,并且会包含一个二分查找索引。参考[https://rocksdb.org.cn/doc/Basic-Operations.html#memtable-and-table-factories](https://rocksdb.org.cn/doc/Basic-Operations.html#memtable-and-table-factories) 21 | 22 | **memtable切换**:在这个过程,**当前活动的memtable**(现在正在写入的那个)被关闭,然后转换成 **不可修改memtable**。同时,我们会关闭当前的WAL文件,然后打开一个新的。 23 | 24 | **不可修改memtable**:一个已经关闭的,正在等待被落盘的memtable。 25 | 26 | **序列号(SeqNum/SeqNo)**:数据库的每个写入请求都会分配到一个自增长的ID数字。这个数字会跟键值对一起追加到WAL文件,memtable,以及SST文件。序列号用于实现snapshot读,压缩过程的垃圾回收,MVCC事务和其他一些目的。 27 | 28 | **恢复**:在一次数据库崩溃或者关闭之后,重新启动的过程。 29 | 30 | **落盘,刷新(flush)**:将memtable的数据写入SST文件的后台任务。 31 | 32 | **压缩**:将一些SST文件合并成另外一些SST文件的后台任务。LevelDB的压缩还包括落盘。在RocksDB,我们进一步区分两个操作。查看[https://rocksdb.org.cn/doc/RocksDB-Basics.html#multi-threaded-compactions](https://rocksdb.org.cn/doc/RocksDB-Basics.html#multi-threaded-compactions) 33 | 34 | **分层压缩或者基于分层的压缩方式**:RocksDB的默认压缩方式。 35 | 36 | **全局压缩方式**:一种备选的压缩算法。参考 [https://rocksdb.org.cn/doc/Universal-Compaction.html](https://rocksdb.org.cn/doc/Universal-Compaction.html) 37 | 38 | **比较器(comparator)**:一种插件类,用于定义键的顺序。参考[https://github.com/facebook/rocksdb/blob/master/include/rocksdb/comparator.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/comparator.h) 39 | 40 | **列族(column family)**:列族是一个DB里的独立键值空间。尽管他的名字有一定的误导性,他跟其他存储系统里的“列族”没有任何关系。RocksDB甚至没有“列”的概念。参考[https://rocksdb.org.cn/doc/Column-Families.html](https://rocksdb.org.cn/doc/Column-Families.html) 41 | 42 | **快照(snapshot)**:一个快照是在一个运行中的数据库上的,在一个特定时间点上,逻辑一致的,视图。 43 | 44 | **检查点(checkpoint)**:一个检查点是一个数据库在文件系统的另一个文件夹的物理镜像映射。参考 [https://rocksdb.org.cn/doc/Checkpoints.html](https://rocksdb.org.cn/doc/Checkpoints.html) 45 | 46 | **备份(backup)**:RocksDB有一套备份工具用于帮助用户将数据库的当前状态备份到另一个地方,例如HDFS。参考[https://rocksdb.org.cn/doc/How-to-backup-RocksDB%3F.html](https://rocksdb.org.cn/doc/How-to-backup-RocksDB%3F.html) 47 | 48 | **版本(Version)**:这个是RocksDB内部概念。一个版本包含某个时间点的所有存活SST文件。一旦一个落盘或者压缩完成,由于存活SST文件发生了变化,一个新的“版本”会被创建。一个旧的“版本”还会被仍在进行的读请求或者压缩工作使用。旧的版本最终会被回收。 49 | 50 | **超级版本(super version)**:RocksDB的内部概念。一个超级版本包含一个特定时间的 的 一个SST文件列表(一个“版本”)以及一个存活memtable的列表。不管是压缩还是落盘,抑或是一个memtable切换,都会生成一个新的“超级版本”。一个旧的“超级版本”会被继续用于正在进行的读请求。旧的超级版本最终会在不再需要的时候被回收掉。 51 | 52 | **块缓存(block cache)**:用于在内存缓存来自SST文件的热数据的数据结构。参考 [https://rocksdb.org.cn/doc/Block-Cache.html](https://rocksdb.org.cn/doc/Block-Cache.html) 53 | 54 | **统计数据(statistics)**:一个在内存的,用于存储运行中数据库的累积统计信息的数据结构。[https://rocksdb.org.cn/doc/Statistics.html](https://rocksdb.org.cn/doc/Statistics.html) 55 | 56 | **性能上下文(perf context)**:用于衡量本地线程情况的内存数据结构。通常被用于衡量单请求性能。参考 [https://rocksdb.org.cn/doc/Perf-Context-and-IO-Stats-Context.html](https://rocksdb.org.cn/doc/Perf-Context-and-IO-Stats-Context.html) 57 | 58 | **DB属性(DB properties)**:一些可以通过DB::GetProperty()获得的运行中状态。 59 | 60 | **表属性(table properites)**:每个SST文件的元数据。包括一些RocksDB生成的系统属性,以及一些通过用户定义回调函数生成的,用户定义的表属性。参考[https://github.com/facebook/rocksdb/blob/master/include/rocksdb/table_properties.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/table_properties.h) 61 | 62 | **写失速(write stall)**:如果有大量落盘以及压缩工作被积压,RocksDB可能会主动减慢写速度,确保落盘和压缩可以按时完成。参考[https://rocksdb.org.cn/doc/Write-Stalls.html](https://rocksdb.org.cn/doc/Write-Stalls.html) 63 | 64 | **bloom filter**:参考[https://rocksdb.org.cn/doc/RocksDB-Bloom-Filter.html](https://rocksdb.org.cn/doc/RocksDB-Bloom-Filter.html) 65 | 66 | **前缀bloom filter**:一种特殊的bloom filter,只能在迭代器里被使用。通过前缀提取器,如果某个SST文件或者memtable里面没有指定的前缀,那么可以避免这部分文件的读取。参考 [https://rocksdb.org.cn/doc/Prefix-Seek-API-Changes.html](https://rocksdb.org.cn/doc/Prefix-Seek-API-Changes.html) 67 | 68 | **前缀提取器(prefix extractor)**:一个用于提取一个键的前缀部分的回调类。通常被用于前缀bloom filter。参考 [https://github.com/facebook/rocksdb/blob/master/include/rocksdb/slice_transform.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/slice_transform.h) 69 | 70 | **基于块的bloom filter或者全bloom filter**: 两种不同的SST文件里的bloom filter存储方式。参考[https://rocksdb.org.cn/doc/RocksDB-Bloom-Filter.html#new-bloom-filter-format](https://rocksdb.org.cn/doc/RocksDB-Bloom-Filter.html#new-bloom-filter-format) 71 | 72 | **分片过滤器(partitioned Filters)**:分片过滤器是将一个全bloom filter分片进更小的块里面,参考[https://rocksdb.org.cn/doc/Partitioned-Index-Filters.html](https://rocksdb.org.cn/doc/Partitioned-Index-Filters.html) 73 | 74 | **压缩过滤器(compaction filter)**:一种用户插件,可以用于在压缩过程中,修改,或者丢弃一些键。参考[https://github.com/facebook/rocksdb/blob/master/include/rocksdb/compaction_filter.h](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/compaction_filter.h) 75 | 76 | **合并操作符(merge operator)**:RocksDB支持一种特殊的操作符Merge(),可以用于对现存数据进行差异纪录,合并操作。合并操作符是一个用户定义类,可以合并合并 操作。参考[https://rocksdb.org.cn/doc/Merge-Operator-Implementation.html](https://rocksdb.org.cn/doc/Merge-Operator-Implementation.html) 77 | 78 | **基于块的表(block-based table)**:默认的SST文件格式。参考[https://rocksdb.org.cn/doc/Rocksdb-BlockBasedTable-Format.html](https://rocksdb.org.cn/doc/Rocksdb-BlockBasedTable-Format.html) 79 | 80 | **块(block)**:SST文件的数据块。在SST文件里,一个块以压缩形式存储。 81 | 82 | **平表(plain table)**:另一种SST文件格式,针对ramfs优化。参考[https://rocksdb.org.cn/doc/PlainTable-Format.html](https://rocksdb.org.cn/doc/PlainTable-Format.html) 83 | 84 | **前向/反向迭代器**:一种特殊的迭代器选项,针对特定的使用场景进行优化。参考[https://rocksdb.org.cn/doc/Tailing-Iterator.html](https://rocksdb.org.cn/doc/Tailing-Iterator.html)。 85 | 86 | **单点删除**:一种特殊的删除操作,只在用户从未更新过某个存在的键的时候被使用。参考:[https://rocksdb.org.cn/doc/Single-Delete.html](https://rocksdb.org.cn/doc/Single-Delete.html) 87 | 88 | **限速器(rate limiter)**:用于限制落盘和压缩的时候写文件系统的速度。参考[https://rocksdb.org.cn/doc/Rate-Limiter.html](https://rocksdb.org.cn/doc/Rate-Limiter.html) 89 | 90 | **悲观事务**:用锁来保证多个并行事务的独立性。默认的写策略是WriteCommited。 91 | 92 | **两阶段提交(2PC,two phase commit)**:悲观事务可以通过两个阶段进行提交:先准备,然后正式提交。参考[https://rocksdb.org.cn/doc/Two-Phase-Commit-Implementation.html](https://rocksdb.org.cn/doc/Two-Phase-Commit-Implementation.html) 93 | 94 | **提交写(WriteCommited)**:悲观事务的默认写策略,会把写入请求缓存在内存,然后在事务提交的时候才写入DB。 95 | 96 | **预备写(WritePrepared)**:一种悲观事务的写策略,会把写请求缓存在内存,如果是二阶段提交,就在准备阶段写入DB,否则,在提交的时候写入DB。参考[https://rocksdb.org.cn/doc/WritePrepared-Transactions.html](https://rocksdb.org.cn/doc/WritePrepared-Transactions.html) 97 | 98 | **未预备写(WriteUnprepared)**:一种悲观事务的写策略,由于这个是事务发送过来的请求,所以直接写入DB,以此避免写数据的时候需要使用过大的内存。 99 | 100 | 101 | -------------------------------------------------------------------------------- /doc/Administration-and-Data-Access-Tool.md: -------------------------------------------------------------------------------- 1 | # Ldb 工具 2 | 3 | ldb命令行工具提供多种数据访问和数据库管理命令。下面列出了一些样例。如果需要更多帮助信息,请直接不带参数运行ldb工具,或者运行tools/ldb_test.py内的单元测试。 4 | 5 | 数据访问样例: 6 | 7 | ```shell 8 | $./ldb --db=/tmp/test_db --create_if_missing put a1 b1 9 | OK 10 | 11 | $ ./ldb --db=/tmp/test_db get a1 12 | b1 13 | 14 | $ ./ldb --db=/tmp/test_db get a2 15 | Failed: NotFound: 16 | 17 | $ ./ldb --db=/tmp/test_db scan 18 | a1 : b1 19 | 20 | $ ./ldb --db=/tmp/test_db scan --hex 21 | 0x6131 : 0x6231 22 | 23 | $ ./ldb --db=/tmp/test_db put --key_hex 0x6132 b2 24 | OK 25 | 26 | $ ./ldb --db=/tmp/test_db scan 27 | a1 : b1 28 | a2 : b2 29 | 30 | $ ./ldb --db=/tmp/test_db get --value_hex a2 31 | 0x6232 32 | 33 | $ ./ldb --db=/tmp/test_db get --hex 0x6131 34 | 0x6231 35 | 36 | $ ./ldb --db=/tmp/test_db batchput a3 b3 a4 b4 37 | OK 38 | 39 | $ ./ldb --db=/tmp/test_db scan 40 | a1 : b1 41 | a2 : b2 42 | a3 : b3 43 | a4 : b4 44 | 45 | $ ./ldb --db=/tmp/test_db batchput "multiple words key" "multiple words value" 46 | OK 47 | 48 | $ ./ldb --db=/tmp/test_db scan 49 | Created bg thread 0x7f4a1dbff700 50 | a1 : b1 51 | a2 : b2 52 | a3 : b3 53 | a4 : b4 54 | multiple words key : multiple words value 55 | 56 | ``` 57 | 58 | 以十六进制导出已经存在的rocksdb库: 59 | 60 | ```shell 61 | $ ./ldb --db=/tmp/test_db dump --hex > /tmp/dbdump 62 | ``` 63 | 64 | 加载十六进制格式的数据进新的rocksdb库 65 | 66 | ```shell 67 | $ cat /tmp/dbdump | ./ldb --db=/tmp/test_db_new load --hex --compression_type=bzip2 --block_size=65536 --create_if_missing --disable_wal 68 | ``` 69 | 70 | 压缩一个已经存在的rocksdb库 71 | 72 | ```shell 73 | $ ./ldb --db=/tmp/test_db_new compact --compression_type=bzip2 --block_size=65536 74 | ``` 75 | 76 | 你可以通过--column_family=来指定你要查询的column family。 77 | 78 | --try_load_options 将会尝试加载配置文件来打开数据库。如果你总是要用这个配置去打开数据库,这是一个好方法。如果你使用默认配置打开数据库,它可能破坏LSM-Tree结构并导致无法自动恢复。 79 | 80 | # SST dump tool 81 | 82 | sst_dump工具能用来获取一个指定的SST文件视图。对一个SST文件sst_dump有多种操作功能。 83 | 84 | ```shell 85 | $ ./sst_dump 86 | file or directory must be specified. 87 | 88 | sst_dump --file= [--command=check|scan|raw] 89 | --file= 90 | Path to SST file or directory containing SST files 91 | 92 | --command=check|scan|raw|verify 93 | check: Iterate over entries in files but dont print anything except if an error is encounterd (default command) 94 | scan: Iterate over entries in files and print them to screen 95 | raw: Dump all the table contents to _dump.txt 96 | verify: Iterate all the blocks in files verifying checksum to detect possible coruption but dont print anything except if a corruption is encountered 97 | recompress: reports the SST file size if recompressed with different 98 | compression types 99 | 100 | --output_hex 101 | Can be combined with scan command to print the keys and values in Hex 102 | 103 | --from= 104 | Key to start reading from when executing check|scan 105 | 106 | --to= 107 | Key to stop reading at when executing check|scan 108 | 109 | --prefix= 110 | Returns all keys with this prefix when executing check|scan 111 | Cannot be used in conjunction with --from 112 | 113 | --read_num= 114 | Maximum number of entries to read when executing check|scan 115 | 116 | --verify_checksum 117 | Verify file checksum when executing check|scan 118 | 119 | --input_key_hex 120 | Can be combined with --from and --to to indicate that these values are encoded in Hex 121 | 122 | --show_properties 123 | Print table properties after iterating over the file when executing 124 | check|scan|raw 125 | 126 | --set_block_size= 127 | Can be combined with --command=recompress to set the block size that will 128 | be used when trying different compression algorithms 129 | 130 | --compression_types= 132 | Can be combined with --command=recompress to run recompression for this 133 | list of compression types 134 | 135 | --parse_internal_key=<0xKEY> 136 | Convenience option to parse an internal key on the command line. Dumps the 137 | internal key in hex format {'key' @ SN: type} 138 | ``` 139 | 140 | ### 导出SST文件块 141 | 142 | ```shell 143 | ./sst_dump --file=/path/to/sst/000829.sst --command=raw 144 | ``` 145 | 146 | 这个命令会产生一个名为 /path/to/sst/000829_dump.txt. 的txt文件。这个文件会包含所有的十六进制编码的索引块和数据块,同时包含表配置,footer细节和原数据索引细节。 147 | 148 | ### 打印SST文件条目 149 | 150 | ```shell 151 | ./sst_dump --file=/path/to/sst/000829.sst --command=scan --read_num=5 152 | ``` 153 | 154 | 这个命令会打印SST文件内头5个key。输出可能像这样: 155 | 156 | ```shell 157 | 'Key1' @ 5: 1 => Value1 158 | 'Key2' @ 2: 1 => Value2 159 | 'Key3' @ 4: 1 => Value3 160 | 'Key4' @ 3: 1 => Value4 161 | 'Key5' @ 1: 1 => Value5 162 | ``` 163 | 164 | 输出能被这样解释 165 | 166 | ``` 167 | '' @ : => 168 | ``` 169 | 170 | 请注意如果你的key是非Ascii编码的,它很难打印在屏幕上,这种情况下使用--output_hex是个好方法 171 | 172 | ``` 173 | ./sst_dump --file=/path/to/sst/000829.sst --command=scan --read_num=5 --output_hex 174 | ``` 175 | 176 | 你也可以使用--from和--to指定开始和结束位置 177 | 178 | ```shell 179 | ./sst_dump --file=/path/to/sst/000829.sst --command=scan --from="key2" --to="key4" 180 | ``` 181 | 182 | 使用--input_key_hex选项传入十六进制--from和--to 183 | 184 | ```shell 185 | ./sst_dump --file=/path/to/sst/000829.sst --command=scan --from="0x6B657932" --to="0x6B657934" --input_key_hex 186 | ``` 187 | 188 | ### 检查SST文件 189 | 190 | ```shell 191 | ./sst_dump --file=/path/to/sst/000829.sst --command=check --verify_checksum 192 | ``` 193 | 194 | 这个命令会迭代SST文件内的所有条目,但是只会在出错时打印信息。它也会校验文件。 195 | 196 | ##### 打印 SST file 配置 197 | 198 | ``` 199 | ./sst_dump --file=/path/to/sst/000829.sst --show_properties 200 | ``` 201 | 202 | 这个命令会读取SST文件配置并打印,输出像这样 203 | 204 | ``` 205 | from [] to [] 206 | Process /path/to/sst/000829.sst 207 | Sst file format: block-based 208 | Table Properties: 209 | ------------------------------ 210 | # data blocks: 26541 211 | # entries: 2283572 212 | raw key size: 264639191 213 | raw average key size: 115.888262 214 | raw value size: 26378342 215 | raw average value size: 11.551351 216 | data block size: 67110160 217 | index block size: 3620969 218 | filter block size: 0 219 | (estimated) table size: 70731129 220 | filter policy name: N/A 221 | # deleted keys: 571272 222 | ``` 223 | 224 | 尝试不同的压缩算法 225 | 226 | sst_dump能够被用来测试文件在不同压缩算法下的大小 227 | 228 | ``` 229 | ./sst_dump --file=/path/to/sst/000829.sst --show_compression_sizes 230 | ``` 231 | 232 | 通过使用 --show_compression_sizes sst_dump 能够在内存中用不同的算法重建SST文件,并输出其大小,输出像这样 233 | 234 | ```shell 235 | from [] to [] 236 | Process /path/to/sst/000829.sst 237 | Sst file format: block-based 238 | Block Size: 16384 239 | Compression: kNoCompression Size: 103974700 240 | Compression: kSnappyCompression Size: 103906223 241 | Compression: kZlibCompression Size: 80602892 242 | Compression: kBZip2Compression Size: 76250777 243 | Compression: kLZ4Compression Size: 103905572 244 | Compression: kLZ4HCCompression Size: 97234828 245 | Compression: kZSTDNotFinalCompression Size: 79821573 246 | ``` 247 | 248 | 这些文件被创建在内容中,并且他们的块大小为16KB,块大小能通过--set_block_size改变。 249 | -------------------------------------------------------------------------------- /doc/WriteUnprepared-Transactions.md: -------------------------------------------------------------------------------- 1 | 这份文档展示 为了在批量写请求还在写入的时候,把写memtable这个操作从准备阶段移动到未准备阶段 进行的最初设计。 2 | 3 | ``` 4 | WriteUnprepared很快就能成为可以在生产使用的功能了 5 | ``` 6 | 7 | # 目标 8 | 9 | 以下是这个项目的目标: 10 | 11 | 1. 减少内存消耗,这使得处理非常大的事务变为可能。 12 | 2. 避免由于一次性写入巨大的事务造成的写失速。 13 | 14 | # 梗概 15 | 16 | 目前,事务会被缓存在内存,知道2PC的准备阶段。当事务非常巨大的时候,缓存的数据也会很大,并且一次性写入如此巨大的事务会对并行的事务造成负面影响。更进一步,缓存一个非常巨大的事务的所有数据会导致机器内存耗尽。对内存消耗最大的贡献者是 i)缓存的键值对,比如说,批量写 ii)每个key的锁。在这个设计,我们把工程拆分为三步,以减少内存。i)值 ii)键 iii)他们对应的锁 17 | 18 | 为了排除在巨大事务中用于缓存值使用的内存,未准备的数据会被逐步写入未预备批处理,并且在写批处理还在构建的时候就被写入数据库。然而,为了简化回滚算法,key还是被缓存起来了。当一个事务提交,每个未预备的批处理都会 更新 提交缓存中的一个项。这里主要的挑战是处理回滚,以及读取自己的写入。 19 | 20 | 事务需要可以读取他们自己的未提交事务。之前这是通过在搜索DB前,先查找缓存的数据来实现的。我们通过保持记录已经写入硬盘的未预备批量写的序列号,然后增加ReadCallback,让其在独到的数据的序列号能匹配的时候返回true,来解决这个问题。这只对巨大的事务生效,这些事务不需要让批量写请求缓存到内存。 21 | 22 | 目前,WritePrepared的回滚算法只能在没有存活快照的恢复流程之后工作。在WriteUnPrepared中,即使现在有快照,事务也可以回滚。我们这样设计回滚算法 i) 追加被事务修改key的值,在事务修改前的值,这样肯定可以取消写入 ii)提交回滚的事务。把一个回滚事务看做提交事务 极大简化了实现,因为现有的提交时处理存活快照的机制能与之无缝结合。WAL仍旧包括一个回滚标记,以保证在DB崩溃后,恢复过程能重新进行回滚。为了找到事务修改前的值,修改了的key的值必须被缓存在Transaction对象中。在没有缓存的批量写的情况下,在工程的第一阶段,我们仍需要缓存被修改的key的集合。在第二步,如果事务很大,我们从WAL中获取key集合。 23 | 24 | RocksDB使用TransactionLockMgr追踪一个事务被锁定的key。对于巨大事务,被锁定的key的列表可能无法填入内存。自动锁定会上升至一个范围锁,用于近似一个集合的点锁。当RocksDB检测到一个事务正在锁定一个非常大的数量的在一个特定范围的key的时候,他会自动升级到区间锁。我们会在这个工程的第三阶段讨论这个。更多细节参考下面的“key锁” 25 | 26 | # 阶段 27 | 28 | 计划是我们分三个阶段执行这个项目: 29 | 30 | - 实现未预备批量写,但是key仍旧缓存在内存中,用于回滚和key上锁 31 | - 回滚的时候,使用WAL来获取key集合,而不是从内存缓冲区获得。 32 | - 对巨大的事务,实现区间锁机制。 33 | 34 | # 实现 35 | 36 | ## WritePrepared概览 37 | 38 | 在已经有的WritePrepared策略,数据结构包括: 39 | 40 | - PrepareHeap:一个正在处理中的准备序列号的堆 41 | - CommitCache:一个从准备序列号到提交序列号的映射 42 | - max_evicted_seq_:从CommitCache淘汰的最大淘汰序列号 43 | - OldCommitMap:从CommitCache淘汰的项,如果他们在某些线上快照还能看到的话,进入这里。 44 | - DelayedPrepared:从PrepareHeap出来的小于max_evicted_seq_的准备序列号。 45 | 46 | ## Put 47 | 48 | 写入数据库需要使用批处理以避免因为写队列带来的额外消耗。为了避免与“批量写”冲突,他们会被成为“批量未预备(unprepared batches)”。通过批处理,我们还会节省未预备序列号的号码,这个号码我们需要生成,并且在我们的数据结构中追踪。一旦一个批处理到达一个可配置的阈值,他会被标记,以确定是不是有一个预备操作在WritePreparedTxn中,只不过一个新的WAL类型,名为BeginUnprepareXID,会被用于BeginPersistedPrepareXID(在WritePreparedTxn策略中被使用)的反面。所有的在同一个未预备批处理中的key会获得同样的序列号(除非有一个冲突的key,会把批处理拆分为多个子批处理) 49 | 50 | 提出的WAL格式: [BeginUnprepareXID] .... [EndPrepare(XID)] 51 | 52 | 这意味着,在最后准备之前,我们需要知道事务的XID(XID是一个事务生命周期中唯一的编码)。(这在Myrocks里面是这样的,因为我们在事务开始的时候生成XID) 53 | 54 | Transaction对象会需要追踪已经写入到db的未预备事务的列表。为此,Transaction对象会包含一系列unprep_seq数字,当一个未预备批处理被写入,unprep_seq会被加入到这个集合。 55 | 56 | 在未预备批量写中,unprep_seq数字同样会被加入到未预备堆(类似WritePreparedTxn的预备堆) 57 | 58 | ## prepare 59 | 60 | 在prepare的时候,我们会把当前批量写中剩余的项写出到WAL,但是使用BeginPersistedPrepareXID来表示这个事务已经准备好。这样,在崩溃的时候,我们可以给应用返回已经准备好的事务,这样应用可以进行正确的操作。未预备事务会在恢复的时候隐式地回滚。 61 | 62 | 提出的WAL格式: [BeginUnprepareXID]...[EndPrepare(XID)] ... [BeginUnprepareXID]...[EndPrepare(XID)] ... [BeginPersistedPrepareXID]...[EndPrepare(XID)] ... ...[Commit(XID)] 63 | 64 | 在这种情况下,最后的准备获得BeginPersistedPrepareXID,而不是BeginUnprepareXID,以表示事务真的准备好了。 65 | 66 | 注意,尽管DB(sst文件)在WritePreparedTxn和WriteUnpreparedTxn是向前向后兼容的,WriteUnpreparedTxn的WAL对WritePreparedTxn不是想前兼容的:WritePreparedTxn肯定会在回复一个通过WriteUnpreparedTxn生成的WAL的时候失败,因为有新的标记类型。然而,WriteUnpreparedTxn仍旧向后兼容WritePreparedTxn,并且可以读取WritePreparedTxn的WAL,因为他们看起来是一样的。 67 | 68 | ## Commit 69 | 70 | 提交的时候,提交映射和未预备堆需要被更新。对于WriteUnprepared,一个提交会潜在地拥有多个预备序列号。所有(unprep_seq, commit_seq)对都需要被加入到提交映射,并且所有unprep_seq都必须从unprepare_heap删除。 71 | 72 | 如果提交在没有准备的时候被执行,并且事务没有提前写入未预备批处理,那么当前的未预备批量写会直接写到类似WritePreparedTxn的CommitWithoutPrepareInternal的情况中。如果事务已经写入为预备批处理,那么我们认为预备阶段也被加入。 73 | 74 | ## Rollback 75 | 76 | 在WritePreparedTxn中,回滚实现被限制在只在恢复之后进行回滚。他大概是这样实现的: 77 | 78 | 1. 对于已经写入的以准备数据,prep_seq = seq 79 | 2. 对于每个修改的key,通过prep_seq-1读取原始的数值 80 | 3. 回写原有的值,但是使用一个新的序列号,rollback_seq 81 | 4. rollback_seq被加入到提交映射里 82 | 5. prep_seq从PrepareHeap中移除 83 | 84 | 这个实现在 存在线上快照能看到prep_seq的时候,是不能工作的。因为如果max_evicted_seq增加到prep_seq之上了,我们会有 `prep_seq < max_evicted_seq < snaphot_seq < rollback_seq`。这时候,正在序列号snapshot_seq上读取的快照会假设在prep_seq的数据已经被提交了,因为`prep_seq < max_evicted_seq`且在old_commit_map里面没有记录 85 | 86 | 这个缺点在WritePreparedTxn中可以容忍,因为Mysql只会在恢复的时候回滚准备好的事务,此时不会有存活快照,因此不会有这个不一致问题。然而,在WriteUnpreparedTxn,这个场景不止发生在恢复阶段,同事会发生在用户发起的未预备事务的回滚上。 87 | 88 | 我们通过写入一个回滚标记解决这个问题,在放弃的事务后追加回滚数据,然后在提交映射里提交事务。因为事务后面追加了回滚数据,尽管被提交了,但是他不会修改数据库的状态,因此他被有效地回滚了。如果max_evicted_seq增加到超过prep_seq了,由于被加入到CommitCache,已有路径,比如,增加淘汰项到old_commit_map,会处理存活的,满足`prep_seq < snapshot_seq < commit_seq`的快照。如果他在回滚过程中崩溃,在恢复的时候,他读取回滚标记,然后完成回滚操作。 89 | 90 | 如果DB在回滚中间泵快,恢复者会看到一些部分写入到WAL的回滚数据。因此恢复过程会最终重试完成回滚,这种部分数据会简单地 使用之前的数值,覆盖为新的回滚批处理。 91 | 92 | 回滚批处理会被 一次性,或者 如果事务很大,分成多个子事务 写入。我们未来会探讨其他的可能实现。 93 | 94 | 其他关于WriteUnpreparedTxn的回滚问题就是如何知道应该回滚哪些key了。之前,由于整个准备好的批处理缓存在了内存,因此可以值迭代写批处理来找到修改了的,需要回滚的key的集合。在这个工程,第一个迭代,我们仍旧保留把key集合写入内存的做法。在下一个迭代,如果key集合的大小增长到一个阈值,我们会从内存中清理这个key的集合,然后如果事务被丢弃了,就从WAL中读取key。每个事务已经在追踪一个列表的未预备序列号,这可以被用于查找WAL中正确的位置。 95 | 96 | ## Get 97 | 98 | 读取的路径跟WritePreparedTxn基本一致。惟一的区别在于,对于事务可以读自己的写入。目前,GetFromBatchAndDB会处理这个问题,具体做法是,在从DB拉取数据前,在ReadCallback被调用已决定哪些可读前,先检查写批处理。在没有写批处理的时候,我们需要其他机制来处理这个。 99 | 100 | 记得每个事务都维护一个unprep_seq的列表。在进入主要的可视性检查前,如WritePreparedTxn中描述的,检查一个key是否有一个存在unprep_seq的序列号,如果是,则这个key可见。这个逻辑在ReadCallback中调用,目前不能支持一个序列号的集合,但是这个可以被拓展,这样unprep_seq的集合就能往下传了。 101 | 102 | 目前,从DB读数据的时候,Get和Seek会直接查找 快照声明的 序列号,这样同一个事务写入的未提交的数据会潜在地,在检查可视性的逻辑前,被跳过。为了解决这个问题,如果当前事务足够大,使得它的写批处理缓冲被删除然后以未预备批处理写入DB,这个优化会被删除。 103 | 104 | ## Recovery 105 | 106 | 恢复过程会跟WritePreparedTxn一样工作,除了一些修改用于决定事务状态(未预备,预备,放弃,提交)。 107 | 108 | 在恢复过程中,带相同的XID的未预备批处理必须被追踪,直到看到EndPrepare标记。如果恢复结束,但是没看到EndPrepare,那么事务就是未预备的,并且等价的,暗示着一个应用回滚。 109 | 110 | 如果恢复以EndPrepare结束,但是没有提交记录,那么事务已经准备好,然后会出现在应用中。 111 | 112 | 如果一个回滚标记在EndPrepare之后被发现,但是没有提交标记,那么事务就是被丢弃了,并且恢复过程必须使用它们之前的数据覆盖被修改的key。 113 | 114 | 如果一个提交标记被发现,那么这个事务就是被提交了。 115 | 116 | ## 延迟预备 117 | 118 | 巨大的事务也可能会花费很长时间。如果一个事物在一段时间之后没有提交(对于一个1k TPS的工作压力,是1分钟),他的序列号会被移入DelayedPrepared,目前是一个简单的,被锁保护的集合。如果最后当前的实现变成了一个瓶颈,我们会改变DelayedPrepared,把他从一个集合(set)修改为一个分片的哈希表,类似于事务锁key的方式。如果对于key锁(发生的更加频繁)已经足够好用,那么用于追踪预备好的事务应该也足够了。 119 | 120 | ## key锁 121 | 122 | 目前,rocksdb支持通过一个TransactionLockMgr中的分片哈希表来实现点锁。每一次申请一个锁,这个key会被哈希然后会检查同一个key是否有锁。如果有,线程挂起,否则,获得锁,并且插入哈希表。这意味着所有锁上的key会存在内存中,对于巨大的事务,可能造成问题。 123 | 124 | 为了解决这个问题,当一个事务被探测到在一个区间内申请了非常多锁的时候,区间锁可以用于近似替代这个巨大集合的点锁。我们这里展现一个初步用于解决这个问题的实现,以展现这是可行的。当到达这个项目的这个阶段的时候,我们会重新考虑其他方案,以及/或者是否并行区间锁是否已经解决了这个问题 125 | 126 | 为了支持去检索,key空间需要被分割成N个逻辑分片,每个分片代表一个key空间内的连续的区间。一个分片key会代表每个分片,并且可以从key本身通过一个应用提供的回调计算得到。如果在一个分片的被上锁的key达到一定阈值,一个分片key会自动写上锁,此时每个独立的key锁会释放。 127 | 128 | 一个集合会被用于持有所有的分片。一个分片会有以下结构: 129 | ``` 130 | struct Partition { 131 | map txn_locks; 132 | enum { UNLOCKED, LOCKED, LOCK_REQUEST } 133 | status = UNLOCKED; 134 | std::shared_ptr part_mutex; 135 | std::shared_ptr part_cv; 136 | int waiters = 0; 137 | } 138 | ``` 139 | 140 | 当我们申请一个key的锁的时候: 141 | 142 | - 检测对应的分片结构。如果它不存在,那么就创建,并插入他。如果状态位UNLOCKED,那么增加txn_locks[id]++,否则,增加waiters,然后在part_cv挂起。当线程被唤醒,减少waiters然后重复这个步骤。 143 | - 在点锁哈希表上请求一个点锁 144 | - 如果点锁获取成功,那么检查txn_locks[id]以确认阈值是否达到。 145 | - 如果点锁超时,减小txn_locks[id]-- 146 | 147 | 为了升级一个锁: 148 | 149 | - 把分片状态设置为LOCK_REQUEST,增加waiters,然后在part_cv挂起,知道txn_locks只包含当前当前事务。被唤醒的时候,减少waiters然后重新检查txn_locks。 150 | - 设置状态为LOCKED 151 | - 通过点锁哈希表,删除本分片所有的点锁。需要移除的锁可以通过tracked_keys_来得到。 152 | - 更新tracked_keys_以移除该分片所有点锁,然后增加分片锁到tracked_keys_ 153 | 154 | 为了解锁一个点锁: 155 | 156 | - 通过点锁哈希表,删除点锁 157 | - 减少对应分片的txn_locks。 158 | - 如果waiters为非零,给part_cv发信号。否则,删除分片。 159 | 160 | 为了解锁一个分批锁(没有用户API触发这个,他发生在事务结束的时候): 161 | 162 | - 如果waiters为非零,给part_cv发信号。否则,删除分片。 163 | 164 | 注意,任何时候,只要从分片读数据,他的互斥锁part_mutex就必须被持有。 165 | 166 | -------------------------------------------------------------------------------- /doc/Two-Phase-Commit-Implementation.md: -------------------------------------------------------------------------------- 1 | 这个文档简要解析了Rocksdb的两阶段提交的实现。 2 | 3 | 这个工程会被分解为五个关注部分: 4 | 5 | - 修改WAL格式 6 | - 拓展已有的事务API 7 | - 修改写流程 8 | - 修改恢复流程 9 | - 与MyRocks整合 10 | 11 | # 修改WAL日志 12 | 13 | WAL包含一个或多条日志。每个日志都是一个或者更多序列化的WriteBatches。恢复过程中,WriteBatches会通过日志重新构建。为了修改WAL格式或者拓展他的功能,我们只需要考虑我们的WriteBatches。 14 | 15 | 一个WriteBatches是一个排序好的记录集合(Put(k,v), Merge(k,v), Delete(k), SingleDelete(k)),他们代表了RocksDB的写操作。每个记录有一个二进制字符串来表示。记录加入到一个WriteBatch的时候,他们的二进制表示内容会被追加到WriteBatch的二进制字符串表示后面。这个二进制字符串的前缀是一个批处理的开始序列号,之后是批处理中记录的数量。如果操作不是应用于default列族,那么每个记录可能会有一个列族修改记录作为前缀。 16 | 17 | 一个WriteBatch可以通过拓展WriteBatch::Handler被遍历。MemTableInserter是一个WriteBatch::Handler 的拓展,他把一个WriteBatch包含的操作插入到正确的列族的Memtable。 18 | 19 | 一个已有的WriteBatch可能有以下逻辑表示: 20 | 21 | Sequence(0);NumRecords(3);Put(a,1);Merge(a,1);Delete(a); 22 | 23 | 为了实现2PC,对WriteBatch格式的修改包括增加四个新的记录。 24 | 25 | - Prepare(xid) 26 | - EndPrepare(xid) 27 | - Commit(xid) 28 | - Rollback(xid) 29 | 30 | 一个支持2PC的WriteBatch可能有以下逻辑表示: 31 | 32 | Sequence(0);NumRecords(6);Prepare(foo);Put(a,b);Put(x,y);EndPrepare();Put(j,k);Commit(foo); 33 | 34 | 可以看到Prepare(xid) 和 EndPrepare()的关系有点类似于括号,会包含ID为'foo'的事务的操作。Commit(xid)和Rollback(xid)标记表示ID为xid的事务的操作需要被提交或者回滚。 35 | 36 | 序列ID分布 37 | 38 | 当一个WriteBatch被插入到一个memtable(通过MemTableInserter插入),每个操作的序列ID等于WriteBatch的序列ID 加上 这个WriteBatch之前的记录消耗的的序列号。这个隐式的WriteBatch 序列号ID映射在2PC加入后将不再存在。在于给Prepare()中包含的操作会消耗序列号ID,方式是以相对Commit()标记的相对的位置进行消耗。这个Commit()标记可能在另一个WriteBatch或者来自他执行准备操作的日志。 39 | 40 | 向后兼容 41 | 42 | WAL格式没有版本号,所以我们需要注意向后兼容。一个当前版本的RocksDB不能回复一个带有2PC标记的WAL日志。实际上他可能会因为无法识别记录id而崩溃。然而,这不重要,只需要给当前版本的RocksDB打补丁让他可以在遇到新的WAL格式的时候跳过prepared节和未知标记即可。 43 | 44 | 当前进度 45 | 46 | 参考 [这里](https://reviews.facebook.net/D54093) 47 | 48 | # 拓展Transaction API 49 | 50 | 我们现阶段只关注悲观事务的2PC。客户端必须提前声明他们是否需要使用2PC语义。例如,客户端代码可能是这样的: 51 | 52 | ``` 53 | TransactionDB* db; 54 | TransactionDB::Open(Options(), TransactionDBOptions(), "foodb", &db); 55 | 56 | TransactionOptions txn_options; 57 | txn_options.two_phase_commit = tr 58 | txn_options.xid = "12345"; 59 | Transaction* txn = db->BeginTransaction(write_options, txn_options); 60 | 61 | txn->Put(...); 62 | txn->Prepare(); 63 | txn->Commit(); 64 | ``` 65 | 66 | 一个事务对象现在拥有有更多的状态,所以我们修改状态的枚举: 67 | 68 | ``` 69 | enum ExecutionStatus { 70 | STARTED = 0, 71 | AWAITING_PREPARE = 1, 72 | PREPARED = 2, 73 | AWAITING_COMMIT = 3, 74 | COMMITED = 4, 75 | AWAITING_ROLLBACK = 5, 76 | ROLLEDBACK = 6, 77 | LOCKS_STOLEN = 7, 78 | }; 79 | ``` 80 | 81 | 事务API会有一个新的成员函数Prepare()。Prepare()会调用WriteImpl,把他自身的环境配置告诉WriteImpl,并且WriteThread访问的ExecutionStatus,XID和WriteBatch。WriteImpl会插入Prepare(xid)标记,然后是WriteBatch的内容,之后是EndPrepare()标记。不会发起memtable插入操作。当同一个事务对象发起提交,再一次,他调用到WriteImpl。这次,只有一个Commit()标记被插入到对应的WAL,并且WriteBatch的内容会被插入到对应的memtable。当对应事务的Rollback()被调用,事务的内容会被清理,并且如果事务已经就绪,调用WriteImpl,以插入一个Rollback(xid)标记。 82 | 83 | 这些所谓的'元标记'(Prepare(xid), EndPrepare(), Commit(xid), Rollback(xid))不会直接插入到一个写批处理中。写流程(WriteImpl)会持有正在写入的事物的环境变量。它使用这个环境来插入相应的标记到WAl中(这样他们就被插入到完整的WriteBatch前面,中间不会有其他WriteBatch)。恢复的时候,这些标记会被MemTableInserter发现,他会使用这个来重新构造之前的准备好的事务。 84 | 85 | 事务时钟超时 86 | 87 | 目前,如果一个事务超时,这个事务提交有一个回调会失败。类似的,如果一个事务超时,那么他的锁就可以被其他事务偷取。这些机制在2PC中应该被保留 —— 差别是超时回调会在准备的时候被调用。如果事务在准备阶段没有超时,那么他不会再提交的时候超时。 88 | 89 | TransactionDB修改 90 | 91 | 为了使用事务,用户必须打开一个TransactionDB。这个TransactionDB实例之后被用于构造Transaction。这个TransactionDB现在记录一个XID到所有已经创建的两阶段提交事务的映射。当一个事务被删除或者回滚,他从映射中被删除。同时有一个API用于查询所有准备好的事务。这个在MyRocks恢复的时候被使用。 92 | 93 | TransactionDB同事还追踪一个所有包含准备段的日志号码的最小堆。当一个事务是'准备好',他的WriteBatch会被写入一个日志,这个日志号会被存储在事务对象以及他的最小堆。当一个事务提交,他的日志号码会从小顶堆中删除,但是他不会被遗忘!现在需要memtable记录他需要的最老日志,直到他被落盘到L0。 94 | 95 | # 写流程的修改 96 | 97 | 写流程可以被分解为两个主要关注的区域。DBImpl::WriteImpl(...)和MemTableInserter。多个客户端线程会调用到WriteImpl。第一个线程会被指定为leader,而一系列跟随的线程会被指定为'跟随者'。leader和一系列的跟随者会被聚在一起,变成一个逻辑组,指向一个'写组'。leader会处理该组的所有WriteBatches请求,把它们组合在一起,然后写出到WAl。根据写组的大小以及当前memtable是否愿意支持并行写入,leader可能会插入所有WriteBatches到memtable 或者 让每个线程分别插入他们的的WriteBatch到memtable。 98 | 99 | 所有memtable插入都是由MemTableInserter处理的。这是一个WriteBatch::Handler的实现 —— 一个WriteBatch迭代处理器。这个处理器遍历WriteBatch的所有元素(Put, Delete, Merge, 等待),并且对当前的MemTable执行对应的调用。MemTableInserter也会处理原地合并,删除和更新。 100 | 101 | 对写路径的修改会包括增加一个可选参数给DBImpl::WriteImpl。这个可选参数会是一个指针,指向写入数据的两阶段事务实例。这个对象会告诉写流程,当前两阶段事务的状态。一个2PC事务会在准备,提交,回滚分别调用一次WriteImpl —— 尽管提交和回滚都是互斥的操作。 102 | 103 | ``` 104 | Status DBImpl::WriteImpl( 105 | const WriteOptions& write_options, 106 | WriteBatch* my_batch, 107 | WriteCallback* callback, 108 | Transaction* txn 109 | ) { 110 | WriteThread::Writer w; 111 | //... 112 | w.txn = txn; // writethreads also have txn context for memtable insert 113 | 114 | // we are now the group leader 115 | int total_count = 0; 116 | uint64_t total_byte_size = 0; 117 | for (auto writer : write_group) { 118 | if (writer->CheckCallback(this)) { 119 | if (writer->ShouldWriteToMem()) 120 | total_count += WriteBatchInternal::Count(writer->batch) 121 | } 122 | } 123 | const SequenceNumber current_sequence = last_sequence + 1; 124 | last_sequence += total_count; 125 | 126 | // now we produce the WAL entry from our write group 127 | for (auto writer : write_group) { 128 | // currently only optimistic transactions use callbacks 129 | // and optimistic transaction do not support 2pc 130 | if (writer->CallbackFailed()) { 131 | continue; 132 | } else if (writer->IsCommitPhase()) { 133 | WriteBatchInternal::MarkCommit(merged_batch, writer->txn->XID_); 134 | } else if (writer->IsRollbackPhase()) { 135 | WriteBatchInternal::MarkRollback(merged_batch, writer->txn->XID_); 136 | } else if (writer->IsPreparePhase()) { 137 | WriteBatchInternal::MarkBeginPrepare(merged_batch, writer->txn->XID_); 138 | WriteBatchInternal::Append(merged_batch, writer->batch); 139 | WriteBatchInternal::MarkEndPrepare(merged_batch); 140 | writer->txn->log_number_ = logfile_number_; 141 | } else { 142 | assert(writer->ShouldWriteToMem()); 143 | WriteBatchInternal::Append(merged_batch, writer->batch); 144 | } 145 | } 146 | //now do MemTable Inserts for WriteGroup 147 | } 148 | ``` 149 | 150 | WriteBatchInternal::InsertInto可能会被修改为只迭代没有Transaction的写者或者COMMIT状态的Transaction。 151 | 152 | 写流程对MemTableInserter的修改 153 | 154 | 如你上面所见,当一个事务已经准备好,事务记录他准备段的日志号。在插入的时候,每个MemTable必须跟踪插入到他内部的准备段的最小日志号码。这个修改会发生在MemTableInserter里。我们会在日志声明周期部分讨论这个值如何使用。 155 | 156 | # 恢复路径的修改 157 | 158 | 当前的恢复路径已经非常适合2PC了。他按时间顺序迭代所有日志里的所有批处理,然后根据日志号码,提供给MemTableInserter。MemTableInserter之后迭代每个批处理,然后把值插入到正确的MemTable。每个MemTable根据当前恢复中的日志编码,知道哪些值他可以忽略。 159 | 160 | 为了使恢复流程可以在2PC下工作,我们只需要修改MemTableInserter,让他能理解我们四个新的'元标记'。 161 | 162 | 记住:当一个2PC事务被提交,他包含多个列族的插入(多个memtable)。这些memtable会在不同时间落盘。我们仍旧使用CF日志号码来避免以恢复,两阶段,已提交的事务的重复插入。 163 | 164 | 考虑下列场景: 165 | 166 | 167 | - 两阶段事务 TXN 插入到 CFA 和 CFB 168 | - TXN 在 LOG 1 准备好 169 | - TXN 在LOG 2标记为 COMMITTED 170 | - TXN 被插入到MemTables 171 | - CFA 落盘到 L0 172 | - CFA 的log_number 现在是 LOG 3 173 | - CFB 没有落盘,并且他仍旧指向LOG 1准备段 174 | - 崩溃恢复 175 | - LOG 1 仍然存在,因为 CFB 在引用 LOG 1 准备段。 176 | - 迭代从LOG 1开始的日志 177 | - CFB把准备好的数据插入memtable, 再次引用 LOG 1 的准备段 178 | - CFA 跳过LOG 2的提交标记的插入,因为他在LOG 3是一致的。 179 | - CFB 落盘到 L0 并且现在 LOG 3 也是一致的了。 180 | - LOG 1, LOG 2 可以被释放了。 181 | 182 | 重建事务 183 | 184 | 如前面所述,恢复路径的修改只要求修改MemTableInserter来处理新的元标记。因为在恢复的时候,我们不能访问一个完整的TransactionDB实例,我们必须凭空构造一个事务。这实质上是为所有恢复起来的准备好的事务构造一个XID->(WriteBatch,log_numb)的映射。当我们遇到一个Commit(xid)标记,我们尝试重新找到这个xid对应的事务,并且重新插入到Mem。如果我们遇到一个rollback(xid)标记,我们删除这个事务。在恢复的最后,我们只有一个包含所有准备好的事务的集合。之后我们通过这些对象构造完整的事务,获取需要的锁。RocksDB现在已经恢复到崩溃/关闭前的状态了。 185 | 186 | 日志生命周期 187 | 188 | 为了找出必须保留的最小日志,我们先找到每个列族的最小log_number_。 189 | 190 | 我们同时必须考虑在TransactionDB中已经准备好的段的堆的最小值。这代表了包含一个准备段但是没有提交的最早的日志。 191 | 192 | 我们同事必须考虑所有Memtable以及还没有落盘的ImmutableMemTables引用的准备段日志的最小值。 193 | 194 | 上面三个的最小值就是最早的还持有没有刷入L0的数据的日志。 195 | 196 | 197 | -------------------------------------------------------------------------------- /doc/OverView.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | RocksDB是Facebook的一个实验项目,目的是希望能开发一套能在服务器压力下,真正发挥高速存储硬件(特别是Flash存储)性能的高效数据库系统。这是一个C++库,允许存储任意长度二进制kv数据。支持原子读写操作。 3 | 4 | RocksDB依靠大量灵活的配置,使之能针对不同的生产环境进行调优,包括直接使用内存,使用Flash,使用硬盘或者HDFS。支持使用不同的压缩算法,并且有一套完整的工具供生产和调试使用。 5 | 6 | RocksDB大量复用了levedb的代码,并且还借鉴了许多HBase的设计理念。原始代码从leveldb 1.5 上fork出来。同时Rocksdb也借用了一些Facebook之前就有的理念和代码。 7 | 8 | # 假设与目标 9 | 10 | ## 性能 11 | RocksDB最初的设计理念就是其应该在高速存储设备以及服务器压力下能有很好的性能表现。他应该能榨取Flash或者RAM子系统提供的所有读写速度潜能。他应该能支持高速的点查询和区间查询。可以通过配置支持很高的随机查询负荷,很高的更新负荷或者两者兼有。其架构应能很简单地对读放大,写放大和存储空间放大进行调优。 12 | 13 | ## 生产环境支持 14 | 15 | RocksDB设计阶段开始就附带内置的工具集合供生产环境部署和调试。主要的参数都应该可以调节以适应不用的硬件上跑的不同的应用程序。 16 | 17 | ## 兼容性 18 | 新版本总是保持向后兼容,已有的应用程序不需要为RocksDB升级进行变更。参考 [RocksDB版本兼容性]() 19 | 20 | # 高度分层架构 21 | RocksDB是一种可以存储任意二进制kv数据的嵌入式存储。RocksDB按顺序组织所有数据,他们的通用操作是Get(key), NewIterator(), Put(key, value), Delete(Key)以及SingleDelete(key)。 22 | 23 | RocksDB有三种基本的数据结构:mentable,sstfile以及logfile。mentable是一种内存数据结构——所有写入请求都会进入mentable,然后选择性进入logfile。logfile是一个在存储上顺序写入的文件。当mentable被填满的时候,他会被刷到sstfile文件并存储起来,然后相关的logfile会在之后被安全地删除。sstfile内的数据都是排序好的,以便于根据key快速搜索。 24 | 25 | sstfile的详细格式参考[这里]() 26 | 27 | # 特性 28 | 29 | ## 列族(Column Families) 30 | 31 | RocksDB支持将一个数据库实例按照许多列族进行分片。所有数据库创建的时候都会有一个用"default"命名的列族,如果某个操作不指定列族,他将操作这个default列族。 32 | 33 | RocksDB在开启WAL的时候保证即使crash,列族的数据也能保持一致性。通过WriteBatch API,还可以实现跨列族的原子操作。 34 | 35 | ## 更新操作 36 | 调用Put API可以将一个键值对写入数据库。如果该键值已经存在于数据库内,之前的数据会被覆盖。调用Write API可以将多个key原子地写入数据库。数据库保证在一个write调用中,要么所有键值都被插入,要么全部都不被插入。如果其中的一些key在数据库中存在,之前的值会被覆盖。 37 | 38 | ## Gets,Iterators以及Snapshots 39 | 40 | 键值对的数据都是按照二进制处理的。键值都没有长度的限制。Get API允许应用从数据库里面提取一个键值对的数据。MultiGet API允许应用一次从数据库获取一批数据。使用MultiGet API获取的所有数据保证相互之间的一致性(版本相同)。 41 | 42 | 数据库中的所有数据都是逻辑上排好序的。应用可以指定一种键值压缩算法来对键值排序。Iterator API允许对database做RangeScan。Iterator可以指定一个key,然后应用程序就可以从这个key开始做扫描。Iterator API还可以用来对数据库内已有的key生成一个预留的迭代器。一个在指定时间的一致性的数据库视图会在Iterator创建的时候被生成。所以,通过Iterator返回的所有键值都是来自一个一致的数据库视图的。 43 | 44 | Snapshot API允许应用创建一个指定时间的数据库视图。Get,Iterator接口可以用于读取一个指定snapshot数据。当然,Snapshot和Iterator都提供一个指定时间的数据库视图,但是他们的内部实现不同。短时间内存在的/前台的扫描最好使用iterator,长期运行/后台的扫描最好使用snapshot。Iterator会对整个指定时间的数据库相关文件保留一个引用计数,这些文件在Iterator释放前,都不会被删除。另一方面,snapshot不会阻止文件删除;作为交换,压缩过程需要知道有snapshot正在使用某个版本的key,并且保证不会在压缩的时候删除这个版本的key。 45 | 46 | Snapshot在数据库重启过程不能保持存在:reload RocksDB库会释放所有之前创建好的snapshot。 47 | 48 | ## 事务(transaction) 49 | 50 | RocksDB支持多操作事务。其分别支持乐观模式和悲观模式,参考 [事务]() 51 | 52 | ## 前缀迭代器 53 | 54 | 多数LSM引擎无法支持高效的RangeScan API,因为他需要对每个文件都进行搜索。不过多数程序也不需要对数据库进行纯随机的区间扫描;多数情况下,应用程序只需要扫描指定前缀的键值即可。RocksDB就利用了这一点。应用可以指定一个键值前缀,配置一个前缀提取器。针对每个键值前缀,RockDB会将其哈希结果存储到bloom。通过bloom,迭代器在扫描指定前缀的键值的时候,就可以避免扫描那些没有这种前缀键值的文件了。 55 | 56 | ## 持续性 57 | 58 | RocksDB有一个事务日志。所有写操作(包括Put,Delete和Merge)都会被存储在memtable的内存缓冲区中同时可选地插入到事务日志里面。一旦重启,他会重新处理所有记录在事务日志里的日志。 59 | 60 | 事务日志可以通过配置,存储到跟SST文件不同的目录去。对于那些将所有数据存储在非持续性快速存储介质的情况,这是非常有必要的。同时,你可以通过往较慢但是持续性好的存储介质上写事务日志,来保证数据不会丢失。 61 | 62 | 每次写操作都有一个标志位,通过WriteOptions来设置,允许指定这个Put操作是不是需要写事务日志。WriteOptions同时允许指定在Put返回成功前,是不是需要调用fsync。 63 | 64 | 在RocksDB内部,使用批处理机制实现了通过一次fsync的调用将批量事务写入日志中。 65 | 66 | ## 错误容忍 67 | 68 | RocksDB使用校验和来检查存储的正确性。每个SST文件块(一般在4K到128K左右)都有一个校验和。一个块一旦被写到存储介质,将不再做修改。RocksDB会动态探测硬件是否支持校验和计算,如果允许,将会使用这种支持。 69 | 70 | ## 多线程压缩 71 | 72 | 如果应用程序对已经存在的key进行了覆盖,就需要使用压缩将多余的拷贝删除。压缩还会处理删除的键值。如果配置得当,压缩可以通过多线程同时进行。 73 | 74 | 整个数据库按顺序存储在一系列的sstfile里面。当memtable写满,他的内容就会被写入一个在Level-0(L0)的文件。被刷入L0的时候,RocksDB删除在memtable里重复的被覆盖的键值。有些文件会被周期性地读入,然后合并为一些更大的文件——这就叫压缩。 75 | 76 | 一个LSM数据库的写吞吐量跟压缩发生的速度有直接的关系,特别是当数据被存储在高速存储介质,如SSD和RAM的时候。RocksDB可以配置为通过多线程进行压缩。当使用多线程压缩的时候,跟单线程压缩相比,在SSD介质上的数据库可以看到数十倍的写速度增长。 77 | 78 | ## 压缩方式 79 | 80 | 一次全局压缩发生在完整的排序好的数据,他们要么是一个L0文件,要么是L1+的某个Level的所有文件。一次压缩会取部分按顺序排列好的连续的文件,把他们合并然后生成一些新的运行数据。 81 | 82 | 分层压缩会将数据存储在数据库的好几层。越新的数据,会在越接近L0层,越老的数据越接近Lmax层。L0层的文件会有些重叠的键值,但是其他层的数据不会。一次压缩过程会从Ln层取一个文件,然后把所有与这个文件的key有交集的Ln+1层的文件都处理一次,然后生成一个新的Ln+1层的文件。与分成压缩模式相比,全局压缩一般有更小的写放大,但是会有更大的空间,读放大。 83 | 84 | 先进先出型压缩会将在老的文件被淘汰的时候删除它,适用于缓存数据。 85 | 86 | 我们还允许开发者开发和测试自己定制的压缩策略。为此,RocksDB设置了合适的钩子来关停内建的压缩算法,然后使用其他API来允许应用使用他们自己的压缩算法。选项disable_auto_compaction如果被设置为真,将关闭自带的压缩算法。GetLiveFilesMetaData API允许外部部件查找所有正在使用的文件,并且决定哪些文件需要被合并和压缩。有需要的时候,可以调用CompactFiles对本地文件进行压缩。DeleteFile接口允许应用程序删除已经被认为过期的文件。 87 | 88 | ## 元数据存储 89 | 90 | 数据库的MANIFEST文件会记录数据库的状态。压缩过程会增加新的文件,然后删除原有的文件,然后通过MAINFEST文件来持久化这些操作。MANIFEST文件里面需要记录的事务会使用一个批量提交算法来减少重复syncs带来的代价。 91 | 92 | ## 避免低速 93 | 94 | 我们还可以使用后台压缩线程将memtable里的数据刷入存储介质的文件上。如果后台压缩线程忙于处理长时间压缩工作,那么一个爆发写操作将很快填满memtable,使得新的写入变慢。可以通过配置部分线程为保留线程来避免这种情况,这些线程将总是用于将memtable的数据刷入存储介质。 95 | 96 | ## 压缩过滤器(compaction filter) 97 | 98 | 某些应用可能需要在压缩的时候对键的内容进行处理。比如,某些数据库,如果提供了生存时间(time-to-live,TTL)功能,可能需要删除已经过期的key。这就可以通过程序定义的压缩过滤器来完成。如果程序希望不停删除已经晚于某个时间的键,就可以使用压缩过滤器丢掉那些已经过期的键。RocksDB的压缩过滤器允许应用程序在压缩过程修改键值内容,甚至删除整个键值对。例如,应用程序可以在压缩的同时进行数据清洗等。 99 | 100 | ## 只读模式 101 | 一个数据库可以用只读模式打开,此时数据库保证应用程序将无法修改任何数据库相关内容。这会带来非常高的读性能,因为它完全无锁。 102 | 103 | ## 数据库调试日志 104 | RocksDB会写很详细的日志到LOG* 文件里面。 这些信息经常被用于调试和分析运行中的系统。日志可以配置为按照特定周期进行翻滚。 105 | 106 | ## 数据压缩 107 | 108 | RocksDB支持snappy,zlib,bzip2,lz4,lz4_hc以及zstd压缩算法。RocksDB可以在不同的层配置不同的压缩算法。通常,90%的数据会落在Lmax层。一个典型的安装会使用ZSTD(或者Zlib,如果没有ZSTD的话)给最下层做压缩算法,然后在其他层使用LZ4(或者snappy,如果没有LZ4)。参考[压缩算法]() 109 | 110 | ## 全量备份,增量备份以及复制 111 | RocksDB支持增量备份。BackupableDB会生成RocksDB备份样本,参考 [How to backup RocksDB?]() 112 | 113 | 增量拷贝需要能找到并且附加上最近所有对数据库的修改。GetUpdatesSince允许应用获取RocksDB最后的几条事务日志。它可以不断获得RocksDB里的事务日志,然后把他们作用在一个远程的备份或者拷贝。 114 | 115 | 典型的复制系统会希望给每个Put加上一些元数据。这些元数据可以帮助检测复制流水线是不是有回环。还可以用于给事务打标签,排序。为此,RocksDB支持一种名为PutLogData的API,应用程序可以用这个给每个Put操作加上元数据。这些元数据只会存储在事务日志而不会存储在数据文件里。使用PutLogData插入的元数据可以通过GetUpdatesSince接口获得。 116 | 117 | RocksDB的事务日志会创建在数据库文件夹。当一个日志文件不再被需要的时候,他会被放倒归档文件夹。之所以把他们放在归档文件夹,是因为后面的某些复制流可能会需要获取这些已经过时的日志。使用GetSotredWalFiles可以获得一个事务日志文件列表。 118 | 119 | ## 在同一个线程支持多个嵌入式数据库 120 | 121 | 一个RocksDB的常见用法是,应用内给他们的数据进行逻辑上的分片。这项技术允许程序做合适的负载均衡以及快速出错恢复。这意味着一个服务器进程可能需要同时操作多个RocksDB数据库。这可以通过一个名为Env的环境对象来实现。例如说,一个线程池会和一个Env关联。如果多个应用程序希望多个数据库实例共享一个进程池(用于后台压缩),那么就应该用同一个Env对象来打开这些数据库。 122 | 123 | 类似的,多个数据库实例可以共享同一个缓存块。 124 | 125 | ## 缓存块——已经压缩以及未压缩的数据 126 | 127 | RocksDB对块使用LRU算法来做读缓存。这个块缓存会被分为两个独立的RAM缓存:第一部分缓存未压缩的块,然后第二部分缓存压缩的块。如果配置了使用压缩块缓存,用户应该同时配置直接IO,而不使用操作系统的页缓存,以避免对压缩数据的双缓存问题。 128 | 129 | ## 表缓存 130 | 表缓存是一种用于缓存打开的文件描述符的结构体。这些文件描述符都是sstfile文件。一个应用可以配置表缓存的最大大小。 131 | 132 | ## IO控制 133 | RocksDB允许用户配置IO应该如何执行。他们可以要求RocksDB对读文件调用fadvise,文件sync的周期,活着允许直接IO。参考[IO]() 134 | 135 | ## StackableDB 136 | RocksDB内带一套封装好的机制,允许在数据库的核心代码上按层添加功能。这个功能通过StackabDB接口实现,比如RocksDB的存活时间功能,就是通过StackableDB接口实现的,他并不是RocksDB的核心接口。这种设计让核心代码可模块化,并且整洁易读。 137 | 138 | ## memtable 139 | 插件式的Memtable: 140 | 141 | RocksDB的memtable的默认实现是跳表(skiplist)。跳表是一个有序集,如果应用的负载主要是区间查询和写操作的时候,非常高效。然而,有些程序压力不是主要写操作和扫描,他们可能根本不用区间扫描。对于这些应用,一个有序集可能不能提供最好的性能。为此,RocksDB提供一个插件式API,允许应用提供自己的memtable实现。这个库自带三种memtable实现:skiplist实现,vector实现以及前缀哈希实现的memtable。vector实现的memtable适用于需要大批量加载数据到数据库的情况。每次写入新的数据都是在vector的末尾追加新数据,当我们需要把memtable的数据刷入L0的时候,我们才进行一次排序。一个前缀哈希的memtable对get,put,以及键值前缀扫描拥有较好的性能。 142 | 143 | memtable流水线: 144 | 145 | RocksDB允许为一个数据库设定任意数量的memtable。当memtable写满的时候,他会被修改为不可变memtable,然后一个后台线程会开始将他的内容刷入存储介质中。同时,新写入的数据将会累积到新申请的memtable里面。如果新的memtable也写入到他的数量限制,他也会变成不可变memtable,然后插入到刷存储流水线。后台线程持续地将流水线里的不可变memtable刷入存储介质中。这个流水线会增加RocksDB的写吞吐,特别是当他在一个比较慢的存储介质上工作的时候。 146 | 147 | memtable写存储的时候的垃圾回收工作: 148 | 149 | 当一个memtable被刷入存储介质,一个内联压缩过程会删除输出流里的重复纪录。类似的,如果早期的put操作最后被delete操作隐藏,那么这个put操作的结果将完全不会写入到输出文件。这个功能大大减少了存储数据的大小以及写放大。当RocksDB被用于一个生产者——消费者队列的时候,这个功能是非常必要的,特别是当队列里的元素存活周期非常短的时候。 150 | 151 | ## 合并操作符 152 | 153 | RocksDB天然支持三种类型的记录,Put记录,Delete记录和Merge记录。当压缩过程遇到Merge记录的时候,他会调用一个应用程序定义的,名为Merge操作符的方法。一个Merge可以把许多Put和Merge操作合并为一个操作。这项强大的功能允许那些需要读——修改——写的应用彻底避免读。它允许应用把操作的意图记录为一个Merge记录,然后RocksDB的压缩过程会用懒操作将这个意图应用到原数值上。这个功能在[合并操作符]()里面有详细说明。 154 | 155 | # 工具 156 | 157 | 有一系列有趣的工具可以用于支持生产环境的数据库。sst_dump工具可以导出一个sst文件里的所有kv键值对。ldb工具可以对数据库进行get,put,scan操作。ldb工具同时还可以导出MANIFEST文件的内容,还可以用于修改数据库的层数。还可以用于强制压缩一个数据库。 158 | 159 | # 测试用例 160 | 我们有大量的单元测试用于测试数据库的特定功能。make check命令可以跑所有的单元测试用例。测试用例会触发RocksDB的特定功能,但是不是用于测试压力下数据的正确性。db_stress测试数据库在压力下的准确性。 161 | 162 | # 性能 163 | 164 | RocksDB的性能通过一个名为db_bench的工具进行测量。db_bench是RocksDB的源码的一部分。在Flash存储下,一些典型的工作负荷下的性能结果可以参考 [这里]()。同时RocksDB在纯内存环境的表现参考[这里]()。 165 | 166 | -------------------------------------------------------------------------------- /doc/RocksJava-Performance-on-Flash-Storage.md: -------------------------------------------------------------------------------- 1 | 在14年4月,我们开始为RocksDB构建一个[Java拓展](RocksJava-Basics.md)。这一页展示RocksJava在Flash存储上的性能测试结果。C++的性能测试结果可以在[这里]()找到 2 | 3 | # 设置 4 | 5 | 所有的性能测试都在同一个机器上进行,下面是测试环境的配置: 6 | 7 | - 测试10亿个键值对。每个key 16 bytes,每个值800 bytes。总数据库裸数据大小大于1TB. 8 | - Intel(R) Xeon(R) CPU E5-2660 v2 @ 2.20GHz, 40 cores. 9 | - 25 MB CPU cache, 144 GB Ram 10 | - CentOS release 5.2 (Final). 11 | - 实验运行于 Funtoo chroot 环境. 12 | - g++ (Funtoo 4.8.1-r2) 4.8.1 13 | - Java(TM) SE Runtime Environment (build 1.7.0_55-b13) 14 | - Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode) 15 | - 版本 85f9bb4 被包括进来. 16 | - 1G rocksdb block cache. 17 | - Snappy 1.1.1 为配置的压缩(compression)算法. 18 | - JEMALLOC不开启. 19 | 20 | # 批量顺序加载key(Test2) 21 | 22 | 这个测试考量使用RocksJava加载10亿个key到数据库的性能。key被按顺序插入。数据库在开始的时候为空的,然后渐渐被填满。加载过程中无读取。下面是RocksJava批量加载的性能: 23 | 24 | ``` 25 | fillseq : 2.48233 micros/op; 311.2 MB/s; 1000000000 ops done; 1 / 1 task(s) finished. 26 | ``` 27 | 28 | 跟我们在[Rocksdb C++性能测试]()的时候类似,RocksJava被配置使用多线程压缩,所以多个线程可以同步压缩多个层的没有交叉的键范围。我们的结果显示,RocksJava可以做大 300+MB/s,或者400K写/s的写入性能。 29 | 30 | 这里是使用RocksJava进行批量加载的命令。 31 | 32 | ``` 33 | bpl=10485760;overlap=10;mcz=0;del=300000000;levels=6;ctrig=4; delay=8; stop=12; wbn=3; mbc=20; mb=67108864;wbs=134217728; dds=0; sync=false; t=1; vs=800; bs=65536; cs=1048576; of=500000; si=1000000; 34 | ./jdb_bench.sh --benchmarks=fillseq --disable_seek_compaction=true --mmap_read=false --statistics=true --histogram=true --threads=$t --key_size=10 --value_size=$vs --block_size=$bs --cache_size=$cs --bloom_bits=10 --compression_type=snappy --cache_numshardbits=4 --open_files=$of --verify_checksum=true --db=/rocksdb-bench/java/b2 --sync=$sync --disable_wal=true --stats_interval=$si --compression_ratio=0.50 --disable_data_sync=$dds --write_buffer_size=$wbs --target_file_size_base=$mb --max_write_buffer_number=$wbn --max_background_compactions=$mbc --level0_file_num_compaction_trigger=$ctrig --level0_slowdown_writes_trigger=$delay --level0_stop_writes_trigger=$stop --num_levels=$levels --delete_obsolete_files_period_micros=$del --max_grandparent_overlap_factor=$overlap --stats_per_interval=1 --max_bytes_for_level_base=$bpl --use_existing_db=false --cache_remove_scan_count_limit=16 --num=1000000000 35 | ``` 36 | 37 | # 随机读(Test4) 38 | 39 | 这个性能测试测量RocksJava在10亿个key的情况下的随机读性能,每个key 16 byte,值800 byte。在这个测试中,RocksJava配置为每个块4KB,snappy压缩被打开。应用有32个线程在对数据库进行随机读。另外,RocksJava被配置为检测每个读的校验和。 40 | 41 | 这个性能测试分两步进行。第一步,数据先使用线性写入加载所有10亿个key到数据库。一旦加载结束,他进入第二部分,这里32个线程会同步发起随机读请求。我们只测量第二部分的性能。 42 | 43 | ``` 44 | readrandom : 7.67180 micros/op; 101.4 MB/s; 1000000000 / 1000000000 found; 32 / 32 task(s) finished. 45 | ``` 46 | 47 | 我们的结果显示,RocksJava可以做到100+ MB/s,或者130K次/s的读请求 48 | 49 | 这里是用来做性能测试的命令: 50 | 51 | ``` 52 | echo "Load 1B keys sequentially into database....." 53 | n=1000000000; r=1000000000; bpl=10485760;overlap=10;mcz=2;del=300000000;levels=6;ctrig=4; delay=8; stop=12; wbn=3; mbc=20; mb=67108864;wbs=134217728; dds=1; sync=false; t=1; vs=800; bs=4096; cs=1048576; of=500000; si=1000000; 54 | ./jdb_bench.sh --benchmarks=fillseq --disable_seek_compaction=true --mmap_read=false --statistics=true --histogram=true --num=$n --threads=$t --value_size=$vs --block_size=$bs --cache_size=$cs --bloom_bits=10 --cache_numshardbits=6 --open_files=$of --verify_checksum=true --db=/rocksdb-bench/java/b4 --sync=$sync --disable_wal=true --compression_type=snappy --stats_interval=$si --compression_ratio=0.50 --disable_data_sync=$dds --write_buffer_size=$wbs --target_file_size_base=$mb --max_write_buffer_number=$wbn --max_background_compactions=$mbc --level0_file_num_compaction_trigger=$ctrig --level0_slowdown_writes_trigger=$delay --level0_stop_writes_trigger=$stop --num_levels=$levels --delete_obsolete_files_period_micros=$del --min_level_to_compress=$mcz --max_grandparent_overlap_factor=$overlap --stats_per_interval=1 --max_bytes_for_level_base=$bpl --use_existing_db=false 55 | 56 | echo "Reading 1B keys in database in random order...." 57 | bpl=10485760;overlap=10;mcz=2;del=300000000;levels=6;ctrig=4; delay=8; stop=12; wbn=3; mbc=20; mb=67108864; wbs=134217728; dds=0; sync=false; t=32; vs=800; bs=4096; cs=1048576; of=500000; si=1000000; 58 | ./jdb_bench.sh --benchmarks=readrandom --disable_seek_compaction=true --mmap_read=false --statistics=true --histogram=true --num=$n --reads=$r --threads=$t --value_size=$vs --block_size=$bs --cache_size=$cs --bloom_bits=10 --cache_numshardbits=6 --open_files=$of --verify_checksum=true --db=/rocksdb-bench/java/b4 --sync=$sync --disable_wal=true --compression_type=none --stats_interval=$si --compression_ratio=0.50 --disable_data_sync=$dds --write_buffer_size=$wbs --target_file_size_base=$mb --max_write_buffer_number=$wbn --max_background_compactions=$mbc --level0_file_num_compaction_trigger=$ctrig --level0_slowdown_writes_trigger=$delay --level0_stop_writes_trigger=$stop --num_levels=$levels --delete_obsolete_files_period_micros=$del --min_level_to_compress=$mcz --max_grandparent_overlap_factor=$overlap --stats_per_interval=1 --max_bytes_for_level_base=$bpl --use_existing_db=true 59 | ``` 60 | 61 | # 多线程读和单线程写(Test5) 62 | 63 | 这个性能测试测量从10亿个key中随机读取1亿个key,同时会并发执行更新操作。这个实验的读取次数被限制为1亿以减少测量时间。类似我们做的test4,每个key还是16 byte,value为800 byte,RocksJava被配置为一个块4Kb,并且snappy压缩打开。在这个测试,有32个读线程,一个独立的随机写线程,写入每秒10k个。下面是随机读和写的结果: 64 | 65 | ``` 66 | readwhilewriting : 9.55882 micros/op; 81.4 MB/s; 100000000 / 100000000 found; 32 / 32 task(s) finished. 67 | 68 | ``` 69 | 70 | 结果显示,同步执行更新的时候,读取速度大概80MB/s,或者每秒100k次读取。 71 | 72 | 下面是用于测试的命令: 73 | 74 | ``` 75 | echo "Load 1B keys sequentially into database....." 76 | dir="/rocksdb-bench/java/b5" 77 | num=1000000000; r=100000000; bpl=536870912; mb=67108864; overlap=10; mcz=2; del=300000000; levels=6; ctrig=4; delay=8; stop=12; wbn=3; mbc=20; wbs=134217728; dds=false; sync=false; vs=800; bs=4096; cs=17179869184; of=500000; wps=0; si=10000000; 78 | ./jdb_bench.sh --benchmarks=fillseq --disable_seek_compaction=true --mmap_read=false --statistics=true --histogram=true --num=$num --threads=1 --value_size=$vs --block_size=$bs --cache_size=$cs --bloom_bits=10 --cache_numshardbits=6 --open_files=$of --verify_checksum=true --db=$dir --sync=$sync --disable_wal=true --compression_type=snappy --stats_interval=$si --compression_ratio=0.5 --disable_data_sync=$dds --write_buffer_size=$wbs --target_file_size_base=$mb --max_write_buffer_number=$wbn --max_background_compactions=$mbc --level0_file_num_compaction_trigger=$ctrig --level0_slowdown_writes_trigger=$delay --level0_stop_writes_trigger=$stop --num_levels=$levels --delete_obsolete_files_period_micros=$del --min_level_to_compress=$mcz --max_grandparent_overlap_factor=$overlap --stats_per_interval=1 --max_bytes_for_level_base=$bpl --use_existing_db=false 79 | echo "Reading while writing 100M keys in database in random order...." 80 | bpl=536870912;mb=67108864;overlap=10;mcz=2;del=300000000;levels=6;ctrig=4;delay=8;stop=12;wbn=3;mbc=20;wbs=134217728;dds=false;sync=false;t=32;vs=800;bs=4096;cs=17179869184;of=500000;wps=10000;si=10000000; 81 | ./jdb_bench.sh --benchmarks=readwhilewriting --disable_seek_compaction=true --mmap_read=false --statistics=true --histogram=true --num=$num --reads=$r --writes_per_second=10000 --threads=$t --value_size=$vs --block_size=$bs --cache_size=$cs --bloom_bits=10 --cache_numshardbits=6 --open_files=$of --verify_checksum=true --db=$dir --sync=$sync --disable_wal=false --compression_type=snappy --stats_interval=$si --compression_ratio=0.5 --disable_data_sync=$dds --write_buffer_size=$wbs --target_file_size_base=$mb --max_write_buffer_number=$wbn --max_background_compactions=$mbc --level0_file_num_compaction_trigger=$ctrig --level0_slowdown_writes_trigger=$delay --level0_stop_writes_trigger=$stop --num_levels=$levels --delete_obsolete_files_period_micros=$del --min_level_to_compress=$mcz --max_grandparent_overlap_factor=$overlap --stats_per_interval=1 --max_bytes_for_level_base=$bpl --use_existing_db=true --writes_per_second=$wps 82 | 83 | ``` 84 | 85 | 86 | -------------------------------------------------------------------------------- /doc/Transactions.md: -------------------------------------------------------------------------------- 1 | 当使用TransactionDB或者OptimisticTransactionDB的时候,RocksDB将支持事务。事务带有简单的BEGIN/COMMIT/ROLLBACK API,并且允许应用并发地修改数据,具体的冲突检查,由Rocksdb来处理。RocksDB支持悲观和乐观的并发控制。 2 | 3 | 注意,当通过WriteBatch写入多个key的时候,RocksDB提供原子化操作。事务提供了一个方法,来保证他们只会在没有冲突的时候被提交。于WriteBatch类似,只有当一个事务提交,其他线程才能看到被修改的内容(读committed)。 4 | 5 | # TransactionDB 6 | 7 | 当使用TransactionDB的时候,所有正在修改的RocksDB里的key都会被上锁,让RocksDB执行冲突检测。如果一个key没法上锁,操作会返回一个错误。当事务被提交,数据库保证这个事务是可以写入的。 8 | 9 | 一个TransactionDB在由大量并发工作压力的时候,相比OptimisticTransactionDB有更好的表现。然而,由于非常过激的上锁策略,使用TransactionDB会有一定的性能损耗。TransactionDB会在所有写操作的时候做冲突检查,包括不使用事务写入的时候。 10 | 11 | 上锁超时和限制可以通过TransactionDBOptions进行调优。 12 | 13 | ```cpp 14 | TransactionDB* txn_db; 15 | Status s = TransactionDB::Open(options, path, &txn_db); 16 | 17 | Transaction* txn = txn_db->BeginTransaction(write_options, txn_options); 18 | s = txn->Put(“key”, “value”); 19 | s = txn->Delete(“key2”); 20 | s = txn->Merge(“key3”, “value”); 21 | s = txn->Commit(); 22 | delete txn; 23 | ``` 24 | 25 | 默认的写策略是WriteCommitted。还可以选择WritePrepared 和WriteUnprepared。更多内容请参考[这里]() 26 | 27 | # OptimisticTransactionDB 28 | 29 | 乐观事务提供轻量级的乐观并发控制给那些多个事务间不会有有高的竞争/干涉的工作场景。 30 | 31 | 乐观事务在预备写的时候不使用任何锁。作为替代,他们把这个操作推迟到在提交的时候检查,是否有其他人修改了正在进行的事务。如果和另一个写入有冲突(或者他无法做决定),提交会返回错误,并且没有任何key都不会被写入。 32 | 33 | 乐观的并发控制在处理那些偶尔出现的写冲突非常有效。然而,对于那些大量事务对同一个key写入导致写冲突频繁发生的场景,却不是一个好主意。对于这些场景,使用TransactionDB是更好的选择。OptimisticTransactionDB在大量非事务写入,而少量事务写入的场景,会比TransactionDB性能更好 34 | 35 | 36 | ```cpp 37 | DB* db; 38 | OptimisticTransactionDB* txn_db; 39 | 40 | Status s = OptimisticTransactionDB::Open(options, path, &txn_db); 41 | db = txn_db->GetBaseDB(); 42 | 43 | OptimisticTransaction* txn = txn_db->BeginTransaction(write_options, txn_options); 44 | txn->Put(“key”, “value”); 45 | txn->Delete(“key2”); 46 | txn->Merge(“key3”, “value”); 47 | s = txn->Commit(); 48 | delete txn; 49 | ``` 50 | 51 | # 从一个事务中读取 52 | 53 | 事务对当前事务中已经批量修改,但是还没有提交的key提供简单的读取操作。 54 | 55 | ```cpp 56 | db->Put(write_options, “a”, “old”); 57 | db->Put(write_options, “b”, “old”); 58 | txn->Put(“a”, “new”); 59 | 60 | vector values; 61 | vector results = txn->MultiGet(read_options, {“a”, “b”}, &values); 62 | // The value returned for key “a” will be “new” since it was written by this transaction. 63 | // The value returned for key “b” will be “old” since it is unchanged in this transaction. 64 | ``` 65 | 使用Transaction::GetIterator(),你还可遍历那些已经存在db的键以及当前事务的键。 66 | 67 | # 设定一个快照 68 | 69 | 默认的,事务冲突检测会校验没有其他人在*事务第一次修改这个key之后*,对这个key做了修改。这个解决方案在多数场景都是足够的。然而,你可能还希望保证没有其他人在*事务开始之后*,对这个key做了修改。可以通过创建事务后调用SetSnapshot来实现。 70 | 71 | 默认行为: 72 | 73 | ```cpp 74 | // Create a txn using either a TransactionDB or OptimisticTransactionDB 75 | txn = txn_db->BeginTransaction(write_options); 76 | 77 | // Write to key1 OUTSIDE of the transaction 78 | db->Put(write_options, “key1”, “value0”); 79 | 80 | // Write to key1 IN transaction 81 | s = txn->Put(“key1”, “value1”); 82 | s = txn->Commit(); 83 | // There is no conflict since the write to key1 outside of the transaction happened before it was written in this transaction. 84 | ``` 85 | 86 | 使用SetSnapshot: 87 | 88 | ```cpp 89 | txn = txn_db->BeginTransaction(write_options); 90 | txn->SetSnapshot(); 91 | 92 | // Write to key1 OUTSIDE of the transaction 93 | db->Put(write_options, “key1”, “value0”); 94 | 95 | // Write to key1 IN transaction 96 | s = txn->Put(“key1”, “value1”); 97 | s = txn->Commit(); 98 | // Transaction will NOT commit since key1 was written outside of this transaction after SetSnapshot() was called (even though this write 99 | // occurred before this key was written in this transaction). 100 | 101 | ``` 102 | 103 | 注意,在前一个例子,如果这是一个TransactionDB,Put会失败。如果是OptimisticTransactionDB,Commit会失败。 104 | 105 | # 可重复读 106 | 107 | 与普通的RocksDB读相似,有可以在ReadOptions指定一个Snapshot来保证事务中的读是可重复读。 108 | 109 | ```cpp 110 | read_options.snapshot = db->GetSnapshot(); 111 | s = txn->GetForUpdate(read_options, “key1”, &value); 112 | … 113 | s = txn->GetForUpdate(read_options, “key1”, &value); 114 | db->ReleaseSnapshot(read_options.snapshot); 115 | ``` 116 | 117 | 注意,在ReadOptions设定一个快照只会影响读出来的数据的版本。他不会影响事务是否可以被提交。 118 | 119 | 如果你已经调用了SetSnapshot,你也可以使用在事务里设定的同一个快照。 120 | 121 | ```cpp 122 | read_options.snapshot = txn->GetSnapshot(); 123 | Status s = txn->GetForUpdate(read_options, “key1”, &value); 124 | ``` 125 | 126 | # 读写冲突保护 127 | 128 | GetForUpdate会保证没有其他写入者会修改任何被这个事务读出的key。 129 | 130 | ```cpp 131 | // Start a transaction 132 | txn = txn_db->BeginTransaction(write_options); 133 | 134 | // Read key1 in this transaction 135 | Status s = txn->GetForUpdate(read_options, “key1”, &value); 136 | 137 | // Write to key1 OUTSIDE of the transaction 138 | s = db->Put(write_options, “key1”, “value0”); 139 | ``` 140 | 如果这个事务是通过TransactionDB创建,Put操作要么超时,要没就会被阻塞直到事务被提交或者放弃。如果这个事务通过OptimisticTransactionDB创建,那么Put会成功,但是事务会在调用txn->Commit()的时候失败。 141 | 142 | ```cpp 143 | // Repeat the previous example but just do a Get() instead of a GetForUpdate() 144 | txn = txn_db->BeginTransaction(write_options); 145 | 146 | // Read key1 in this transaction 147 | Status s = txn->Get(read_options, “key1”, &value); 148 | 149 | // Write to key1 OUTSIDE of the transaction 150 | s = db->Put(write_options, “key1”, “value0”); 151 | 152 | // No conflict since transactions only do conflict checking for keys read using GetForUpdate(). 153 | s = txn->Commit(); 154 | ``` 155 | 156 | # 调优/内存使用 157 | 158 | 在内部,事务需要追踪那些key最近被修改过。现有的内存写buffer会因此被重用。当决定内存中保留多少写buffer的时候,事务仍旧遵从已有的max_write_buffer_number选项。另外,使用事务不影响落盘和压缩。 159 | 160 | 可能切换到使用[乐观]事务DB使用更多的内存。如果你曾经给max_write_buffer_number设置一个非常大的值,一个标准的RocksDB实例永远都不会逼近这个最大内存限制。然而,一个[乐观]事务DB会尝试使用尽可能多的写buffer。这个可以通过减小max_write_buffer_number或者设置max_write_buffer_number_to_maintain为一个小于max_write_buffer_number的值来进行调优。 161 | 162 | OptimisticTransactionDB:在提交的时候,乐观事务会使用内存写buffer来做冲突检测。为此,缓存的数据必须比事务中修改的内容旧。否则,Commit会失败。增加max_write_buffer_number_to_maintain以减小由于缓冲区不足导致的提交失败。 163 | 164 | TransactionDB:如果使用了SetSnapshot,Put/Delete/Merge/GetForUpdate操作会先检查内存的缓冲区来做冲突检测。如果没有足够的历史数据在缓冲区,那么会检查SST文件。增加max_write_buffer_number_to_maintain会减少冲突检测过程中的SST文件的读操作。 165 | 166 | # 保存点 167 | 168 | 出了Rollback,事务还可以通过SavePoint来进行部分回滚。 169 | 170 | ```cpp 171 | s = txn->Put("A", "a"); 172 | txn->SetSavePoint(); 173 | s = txn->Put("B", "b"); 174 | txn->RollbackToSavePoint() 175 | s = txn->Commit() 176 | // Since RollbackToSavePoint() was called, this transaction will only write key A and not write key B. 177 | ``` 178 | 179 | # 底层实现 180 | 181 | 一个高度简明的概括,展示了事务的工作原理。 182 | 183 | ## 读快照 184 | 185 | 每一个RocksDB的更新都是通过插入一个带有强制自增的序列号的项来实现的。给即将要被(事务或者非事务)DB用来创建快照的read_options.snapshot赋值一个序列号,可以只读到小于这个序列号的内容。例如,读快照→ DBImpl::GetImpl 186 | 187 | 除此之外,事务还可以调用TransactionBaseImpl::SetSnapshot,这个接口会调用DBImpl::GetSnapshot。他实现了两个目标: 188 | 189 | - 返回当前的序列号:事务会使用这个序列号(而不是他的写入值的序列号)来检测写-写冲突 → TransactionImpl::TryLock → TransactionImpl::ValidateSnapshot → TransactionUtil::CheckKeyForConflicts 190 | - 确保小于这个序号的值不会被压缩删除。例如 (snapshots_.GetAll)。这些快照必须被调用者释放(DBImpl::ReleaseSnapshot) 191 | 192 | ## 读写冲突检测 193 | 194 | 读写冲突可以通过升级为写写冲突来防止:通过GetForUpdate(而不是Get)来做读操作。 195 | 196 | ## 写写冲突检测:悲观方式 197 | 198 | 写写冲突在写入的时候用一个锁表来进行检测。 199 | 200 | 非事务更新(put,merge,delete)在内部其实是以一个事务来运行的。所以每个更新都是通过事务→ TransactionDBImpl::Put 201 | 202 | 每个更新都会先申请一个锁→ TransactionImpl::TryLock  203 | TransactionLockMgr::TryLock在每个列族只有16个锁→ size_t num_stripes = 16 204 | 205 | Commit只是简单地把批量写写到WAL然后通过调用DBImpl::Write来写入Memtable→ TransactionImpl::Commit 206 | 207 | 为了支持分布式事务,客户端可以在写之后先调用Prepare。他会把数据写入WAL,但是不会写入Memtable,这允许机器崩溃之后恢复→ TransactionImpl::Prepare 如果Prepare被调用,Commit会写一个提交记录到WAL然后把数值写入MemTable。这是通过在批量写增加值之前调用MarkWalTerminationPoint来实现的。 208 | 209 | ## 写写冲突检测:乐观方式 210 | 211 | 写写冲突会在提交的时候检查其最后一个序列号,来检测冲突。 212 | 213 | 每次更新把key加入到一个内存的vector中→ TransactionDBImpl::Put 与 OptimisticTransactionImpl::TryLock 214 | 215 | Commit把OptimisticTransactionImpl::CheckTransactionForConflicts作为回调连接到批量写→ OptimisticTransactionImpl::Commit,他会被DBImpl::WriteImpl通过写入进行回调->CheckCallback 216 | 217 | 冲突检测的逻辑在TransactionUtil::CheckKeysForConflicts中实现 218 | 219 | - 只检测在内存中出现的key的冲突和失败。 220 | - 冲突检测是通过比对每个key的最后的序列号(DBImpl::GetLatestSequenceForKey)与用于写入的序列号来实现的。 221 | 222 | 223 | 224 | --------------------------------------------------------------------------------