关于rocksdb:RocksDB剖析系列-Iterator

参考: https://github.com/facebook/r...https://github.com/facebook/r...https://blog.csdn.net/Z_Stand...https://www.jianshu.com/p/f57...https://book.tidb.io/session3...https://www.iggiewang.cn/2021...IntroductionRocksDB的Iterator在通过高度封装后,能够像C++ stl库为每一个容器结构的迭代器的iterator一样被应用,它能够定位到某个key,并能够从这个key开始进行scan,它也能够被用来进行反向scan。 如果在创立迭代器时,ReadOptions.snapshot被给定,迭代器将返回该snapshot的数据,如果传入的为nullptr,隐性视图将被创立,隐性视图不能转为显性。 用户对Iterator的应用NewIterator 创立一个迭代器,须要传入ReadOptionsSeek 查找一个keySeekToFirst 迭代器挪动到db的第一个key地位,个别用于程序遍历整个db的所有keySeekToLast 迭代器挪动到db的最初一个key地位, 个别用于反向遍历整个db的所有keySeekForPrev挪动到以后key的上一个地位,个别用于遍历(limit, start]之间的keyNext 迭代器挪动到下一个keyPrev迭代器挪动到上一个keyIterator::status()能够返回以后迭代器的error。error蕴含IO error,checksum不匹配,外部谬误等... 如果没有谬误,状态为Status::OK()。如果Iterator::Valid()是true,status必定为OK。而如果Iterator::Valid()为false,可能是因为此时迭代器走到了数据的开端,这种状况status是OK;也有可能是呈现了谬误,这种状况status不是OK。 5.13.x或更早的版本中,这两个API会有不同,须要再查阅文档。 各Iterator DBIterImplementation: db/db_iter.ccInterface: IteratorDBIter是对InternalIterator的封装,它的工作次要是将Internal key解析成user key,并展现进去 Example底层的InternalIterator中 InternalKey(user_key="Key1", seqno=10, Type=Put) | Value = "KEY1_VAL2"InternalKey(user_key="Key1", seqno=9, Type=Put) | Value = "KEY1_VAL1"InternalKey(user_key="Key2", seqno=16, Type=Put) | Value = "KEY2_VAL2"InternalKey(user_key="Key2", seqno=15, Type=Delete) | Value = "KEY2_VAL1"InternalKey(user_key="Key3", seqno=7, Type=Delete) | Value = "KEY3_VAL1"InternalKey(user_key="Key4", seqno=5, Type=Put) | Value = "KEY4_VAL1"DBIter将Internal Key解析为User Key,并裸露接口给用户 Key="Key1" | Value = "KEY1_VAL2"Key="Key2" | Value = "KEY2_VAL2"Key="Key4" | Value = "KEY4_VAL1"MergingIteratorImplementation: table/merging_iterator.ccInterface: InternalIteratorMergingIterator由很多Children Iterator组成 ...

January 25, 2022 · 2 min · jiezi

关于rocksdb:RocksDB剖析系列-BlockBasedTableBuilder源码解读

参考: https://www.jianshu.com/p/9b5...https://zhuanlan.zhihu.com/p/...SST File Format之前在LSM-Tree局部有提过,但过后理解的比拟浅 <beginning_of_file>[data block 1][data block 2]...[data block N][meta block 1: filter block] [meta block 2: index block][meta block 3: compression dictionary block] [meta block 4: range deletion block] [meta block 5: stats block] ...[meta block K: future extended block] (More meta blocks may be added in the future)[metaindex block][Footer] (fixed size; starts at file_size - sizeof(Footer))<end_of_file>与BlockBasedTableBuilder相干的类有以下几个 BlockBasedTable, 该类封装了用于读取磁盘BlockBasedTable类型的SST表的逻辑。BlockBasedTableBuilder, 该类用于在磁盘上构建一个BlockBasedTable类型的SST表。BlockBasedTableFactory, 该类是BlockBasedTable工厂办法的实现,用于创立BlockBasedTable/BlockBasedTableBuilder。Write Block的程序:Write meta blocks, metaindex block and footer in the following order. ...

January 22, 2022 · 1 min · jiezi

关于rocksdb:RocksDB剖析系列-Remote-Compaction

参考: https://github.com/facebook/r...https://zhuanlan.zhihu.com/p/...Remote Compaction机制使近程地Compaction成为可能,它能够是一个不同的过程,甚至是在近程的主机上。通过将Compaction服务迁徙到近程的主机上,不会有后盾的Compaction服务去影响read和write申请,进步了性能和稳定性。而专用于Compaction的近程主机能够只对Compaction做优化,并用来给不同的DB做Compaction。 Schedule第一步是数据库触发Compaction,它不会在本地进行Compaction,而是在CompactionService中发送 Compaction信息。用户须要实现CompactionService::Start(),来发送Compaction信息给远端过程,从而调度Compaction Compactremote的Compaction Worker须要运行应用发来的Compaction信息运行DB::OpenAndCompact()。Worker会在只读模式下进行Compact,它不能批改LSM Tree,而是将Compact的后果放在一个临时的地位。 Return Result当Compaction完结后,须要向原先的那台db返回后果,蕴含Compact的SST的元数据和一些internal的信息,与调度环节中相似的,此处两台机器的通信须要被实现。 Install & Purge原db通过CompactionService::WaitForComplete()期待后果返回,后果应该传入该函数并返回。在这之后,如果临时工作区和db不在同一个文件系统上,须要先把临时工作区的文件copy过来,而后再做重命名。

January 15, 2022 · 1 min · jiezi

关于rocksdb:RocksDB剖析系列-Compaction策略

Compaction是RocksDB中很重要的机制,而RocksDB默认采纳Leveled Compaction策略。因而先着重剖析Leveled Compaction 文件构造 在磁盘上的文件分为多个level,其中,level 0是非凡的,它蕴含了刚刚被flush的Memtable,因而存在level 0中的数据会比其余level更新。此外,Level 0的各文件之间并不是有序的,而其余Level中的各个sstfile间是有序的,如下图这意味着除Level 0之外的其余Level都能够通过二分搜寻来精确地定位key的地位,而Level 0则不行。因为这个起因,Level 0的文件数目须要被严格控制。除Level 0之外的其余Level都有指标大小,这些指标大小个别呈指数级别增长 当L0层的文件数据达到level0_file_num_compaction_trigger,compaction会被触发,L0层的文件将会merge到L1层。因为L0层的SST文件之间是无序的,可能有重叠的局部,所以在做compaction时,失常会将L0层的所有文件全副merge sort后写到L1层。 在这次Compaction后,L1层的文件大小可能会超过指标大小,这种状况下,L1中的至多一个文件会被merge到L2中 须要留神的是,L1及以下Level的Compaction是并行的。而默认状况下,L0到L1压缩不是并行的。在某些状况下,它可能成为限度Compaction速度的瓶颈。RocksDB仅反对L0到L1的基于子压缩的并行化。要启用它,能够将max_subcompactions设置为大于1。而后,RocksDB将尝试对范畴进行分区,并应用多个线程来执行它: Compaction筛选当多个Level触发Compaction机制时,RocksDB须要决定先对哪个Level做Compaction。为了辨别优先级,对每个Level计算相应的Score 对于非L0层的层,score的计算形式为,level中所有sstfile的大小除以该level对应的指标大小(如果须要Compaction的文件已被选中,它将不蕴含在level总大小中)对于L0,score为(l0文件的总数量除以level0_file_num_compaction_trigger)和(总大小减去max_bytes_for_level_base)的最大值。 须要留神的是,如果文件数量小于level0_file_num_compaction_trigger,即便score再高,也不会从L0触发compaction

December 11, 2021 · 1 min · jiezi

关于rocksdb:RocksDB剖析系列-Logstructured-mergetree

B+ Tree的毛病B+树最大的性能问题是会产生大量的随机IO,随着新数据的插入,叶子节点会缓缓决裂,逻辑上间断的叶子节点在物理上往往不间断,甚至拆散的很远,但做范畴查问时,会产生大量随机读IO。对于大量的随机写也一样,新插入的数据在磁盘上的地位可能相隔很远,会产生大量的随机写IO。 相比B+ Tree,LSM-Tree可能会损失一部分读性能,但换来了微小的写性能的晋升。 LSM-Tree原理 Memtable当一个Memtable被写满时,它将变为immutable的,前面进来的数据将写入一个新的Memtable。而后盾线程将会将Immutable Memtable flush进一个SST文件,并在flush好后将该Memtable销毁 数据结构可抉择的数据结构 SkipList (Default)HashLinkListHashSkipListVectorMemtable默认采纳跳表实现而哈希跳表我之前没有接触过,后果搜寻后发现是先对哈希值取模放入对应的桶中,再在每个哈希桶中保护跳表构造。这样的话应该会晋升随机单个查问的速度,而范畴查问应该会变慢一点。 此外,依据官网文档,只有SkipList反对并发插入。 Flush三种状况下会触发flush操作 当某一个Memtable的大小超出了ColumnFamilyOptions::write_buffer_size当所有列族的Memtable的总大小超出了DBOptions::db_write_buffer_size,或者DBOptions::write_buffer_manager收回了flush的信号。这种状况下size最大的那个Memtable将被flushWAL的大小超过了DBOptions::max_total_wal_size。这种状况下最老的Memtable将被flush,以便清空它相应的WAL中的局部。Write Ahead LogRocksDB的每次更新都会写入两个地位:内存中的memtable(稍后将flush到SST文件)和磁盘上的预写日志(WAL)。而当Memtable中的数据flush后,WAL中对应的数据将被删除 WAL被用作故障后复原数据,它默认是开启的。也能够通过配置敞开它。

December 9, 2021 · 1 min · jiezi

关于rocksdb:关于Apple-Silicon-Mac编译RocksDB可能出现的坑

问题形容编译环境MacOSM1 Progcc/opt/homebrew/Cellar/gcc/10.2.0_4/lib/gcc/10/gcc/aarch64-apple-darwin20/10.2.1/include/arm_acle.h: In function 'int __rndr(uint64_t*)':/opt/homebrew/Cellar/gcc/10.2.0_4/lib/gcc/10/gcc/aarch64-apple-darwin20/10.2.1/include/arm_acle.h:201:34: error: invalid conversion from 'uint64_t*' {aka 'long long unsigned int*'} to 'long unsigned int*' [-fpermissive] 201 | return __builtin_aarch64_rndr (__res); | ^~~~~ | | | uint64_t* {aka long long unsigned int*}<built-in>: note: initializing argument 1 of 'int __builtin_aarch64_rndr(long unsigned int*)'/opt/homebrew/Cellar/gcc/10.2.0_4/lib/gcc/10/gcc/aarch64-apple-darwin20/10.2.1/include/arm_acle.h: In function 'int __rndrrs(uint64_t*)':/opt/homebrew/Cellar/gcc/10.2.0_4/lib/gcc/10/gcc/aarch64-apple-darwin20/10.2.1/include/arm_acle.h:207:36: error: invalid conversion from 'uint64_t*' {aka 'long long unsigned int*'} to 'long unsigned int*' [-fpermissive] 207 | return __builtin_aarch64_rndrrs (__res); | ^~~~~ | | | uint64_t* {aka long long unsigned int*}<built-in>: note: initializing argument 1 of 'int __builtin_aarch64_rndrrs(long unsigned int*)'解决问题百思不得其解 最初终于在google搜到了和我一样问题的老哥https://githubmemory.com/repo... ...

December 6, 2021 · 1 min · jiezi

关于rocksdb:RocksDB剖析系列-RocksDB的历史

相干材料The History of RocksDB: http://rocksdb.blogspot.com/2... RocksDB的诞生起因2011年中时,Dhruba Borthakur曾经在HBase/HDFS方面做了五年的开发工作,他十分青睐Hadoop优良的生态,但同时他想挑战一些新的货色:晋升HBase/HDFS的查问服务工作负载。 事实证明他在这方面做得很胜利,在对HBase/HDFS进行了多轮加强后,他曾经可能使HBase/HDFS的提早速度仅为MySQL服务器的两倍,并且HBase/HDFS在雷同硬件雷同工作负载下应用的IOPs仅为MySQL的三倍。 但这时,闪存存储成为了事实,数据集从旋转磁盘迁徙到闪存。而他通过测试后发现,2012年的HDFS/HBase存在一些软件瓶颈,因而无奈无效地应用闪存。如果数据存储在闪存中,那么就须要一个新的存储引擎,以便可能无效地为数据上的随机工作负载提供服务。 为什么RocksDB是嵌入式的?假如说,旋转磁盘上的读取或写入大概须要10毫秒,闪存的读取或写入大概须要100微秒。两台机器之间的网络提早放弃在50微秒。那么在CS架构中,通过网络拜访旋转磁盘上的数据会导致10.05毫秒的提早,网络施加的开销仅为0.5%。而假如用闪存驱动器代替磁盘,对于本地连接的闪存,数据拜访工夫为100微秒,通过网络拜访雷同数据的工夫为150微秒。这种状况下,网络数据拜访的开销比本地数据拜访高50%。简而言之,当闪存提供了极高的IO速度时,网络成为了性能的瓶颈。 为什么不应用现有的嵌入式数据库?过后现有的数据库曾经有很多了:BerkeleyDB, KyotoDB, SQLite3, leveldb。在这些数据库的开源基准测试中,leveldb仿佛是最快的那一个。但并非所有这些数据库都适宜在闪存上存储数据。因为闪存的写持久性无限,对闪存上数据块的更新通常会引入写入放大(write-amplification)。当对leveldb进行测试时,他发现它不适宜现实的服务器工作负载。开源基准测试后果乍一看十分棒,但只是针对一个大小小于测试机器的RAM的数据库。当在一个至多比main memory大5倍的数据库上执行雷同的基准测试时,性能后果令人丧气。 此外,leveldb的单线程压缩,频繁的写暂停导致99%的提早十分大。leveldb并不能生产闪存带来的io量。在闪存的倒退下,须要一个可能在读放大、写放大和空间放大之间进行优雅衡量的数据库。所以,RocksDB诞生了。 RocksDB的愿景带有点查找和范畴扫描的嵌入式键值存储针对疾速存储进行优化,例如闪存和RAM提供全面生产反对的服务器端数据库随CPU内核数和存储IOPs线性扩大须要留神的是,RocksDB不是分布式数据库。它没有内置容错或复制性能。它对数据分片无所不知。目前比拟多的计划应该是Raft+RocksDB。

December 4, 2021 · 1 min · jiezi

RocksDB零基础学习二-Memtable-WALWriteahead-Log

前文提到,所有进入RocksDB的数据,会先进入内存,再flush进disk。本文,我们会学习到,RocksDB 如果管理和存储内存中的数据。 图1 RocksDB 架构图(source:https://www.slideshare.net/me... RocksDB有三种基本文件格式是 Memtable/SST file/Log file。Memtable 是一种内存文件数据系统,新写数据会被写进 Memtable,部分请求内容会被写进 Log file。Log file 是一种有利于顺序写的文件系统。Memtable 的内存空间被填满之后,会有一部分老数据被转移到 SST file 里面,这些数据对应的 Log file 里的 log 就会被安全删除。SST file 中的内容是有序的。 MemTable MemTable是一个内存数据结构,他保存了落盘到SST文件前的数据。他同时服务于读和写——新的写入总是将数据插入到memtable,读取在查询SST文件前总是要查询memtable,因为memtable里面的数据总是更新的。一旦一个memtable被写满,他会变成不可修改的,并被一个新的memtable替换。一个后台线程会把这个memtable的内容落盘到一个SST文件,然后这个memtable就可以被销毁了。并且在flush的过程中,会完成数据的压缩 RocksDB默认实现方式是SkipList,适用于范围查询和插入,但是如果是其他场景,RocksDB也提供了另外两种实现方式。具体区别可参考https://github.com/facebook/rocksdb/wiki/MemTable a vector memtable:适用于批量载入过程,每次新增元素都会追加到之前的元素后,当MemTable的数据被刷入L0时,数据会按之前的顺利刷入SST filea prefix-hash memtable:适用于带有指定前缀的查询,插入和scanImmutable Memtable 所有的写操作都是在memtable进行,当memtable空间不足时,会创建一块新的memtable来继续接收写操作,原先的内存将被标识为只读模式,等待被刷入sst。在任何时候,一个CF中,只存在一块active memtable和0+块immutable memtable。刷入时机有以下三个条件来确定: write_buffer_size 设置一块memtable的容量,一旦写满,就标记为只读,然后创建一块新的。max_write_buffer_number 设置memtable的最大存在数(active 和 immutable 共享),一旦active memtable被写满了,并且memtable的数量大于max_write_buffer_number,此时会阻塞写操作。当flush操作比写入慢的时候,会发生这种情况min_write_buffer_number_to_merge 设置刷入sst之前,最小可合并的memtable数,例如,如果设置2,只有当 immutable memtable数量达到2的时候,会被刷入sst,数量为1的时候,则永远不会被刷入。由于每次Get()都需要线性查询每个memtable,所以,如果这个值太高,会影响写的性能 例如: write\_buffer\_size = 512MB;max\_write\_buffer\_number = 5;min\_write\_buffer\_number\_to\_merge = 2;当写入速达到 16MB/s,每32s会创建一块新的memtable,每64s会有2块memtable合并,并刷入sst 。Depending on the working set size,每一刷在512MB 到 1GB之间。为了防止刷入操作影响写性能,内存使用上限是5*512MB = 2.5GB。到达上限后,所有的写请求将会被阻塞。 ...

July 5, 2020 · 1 min · jiezi

leveldbrocksdb

单机基于SSTable。适用于与SSD一起使用。整体https://segmentfault.com/a/11...。mysql写入千条/s,读万应该没问题。redis 写入 万条/s 7M/s(k+v 700bytes,双核)读是写入的1.4倍 mem 3gb 2核。这两个网上搜的,不保证正确,就看个大概吧。SSD上 rocksdb随机和顺序的性能差不多,写要比读性能稍好。随机读写1.7万条/s 14M/s (32核)。batch_write/read下SSD单线程会耗8倍。普通write只快1.2倍。没有再一个机器上的对比。rocksdb在用SSD和batch-write/read下的读写性能还是可以的。 第一章 levelDb架构图 读取过程数据的读取是按照 MemTable、Immutable MemTable 以及不同层级的 SSTable 的顺序进行的,前两者都是在内存中,后面不同层级的 SSTable 都是以 *.ldb 文件的形式持久存储在磁盘上 写入过程1.调用 MakeRoomForWrite 方法为即将进行的写入提供足够的空间;在这个过程中,由于 memtable 中空间的不足可能会冻结当前的 memtable,发生 Minor Compaction 并创建一个新的 MemTable 对象;不可变imm去minor C,新的memtable继续接收写在某些条件满足时,也可能发生 Major Compaction,对数据库中的 SSTable 进行压缩;2.通过 AddRecord 方法向日志中追加一条写操作的记录;3.再向日志成功写入记录后,我们使用 InsertInto 直接插入 memtable 中,内存格式为跳表,完成整个写操作的流程; writebatch并发控制全局的sequence(memcache中记录)。读写事务都申请writebatch。这里即使是并发,也是选择leader(cas+memory_order)将多个writebatch合并一起写入WAL,再依次写入memtable再提交。 ACID版本控制 内存中在每次读的时候用userkey+sequence生成。这个sequence面膜人呢使用当前最新的sequnce。保证整个读过程都读到当前版本的,在写入时,写入成功后才更新sequnce保证最新写入的sequnce+1的内存不会被旧的读取读到。Compaction过程中被引用的version不会删除。被version引用的file也不会删除每次获取current versionn的内容。更新后才会更改current的位置 memtable频繁插入查询,没有删除。需要无写状态下的遍历(dump过程)=》跳表日志文件4M后变sstable.memtable是日志的副本 sstablesstable(默认7个)【上层0,下层7】filemetadata: refs文件被不同version引用次数,allowed_Seeks允许查找次数,number文件序号,file_size,smallest,largest除了level0是内存满直接落盘key范围会有重叠,下层都是经过合并的,没重叠,可以直接通过filemetadata定位在一个文件后二分查找。level0会查找多个文件。上层倒到容量后触发向下一层归并,每一层数据量是比其上一层成倍增长 sstable=>blocks=>entrys DataBlock Key := UserKey + SequenceNum + TypeType := kDelete or kValueMeta Block:比较特殊的Block,用来存储元信息,目前LevelDB使用的仅有对布隆过滤器的存储。写入Data Block的数据会同时更新对应Meta Block中的过滤器。读取数据时也会首先经过布隆过滤器过滤.bloom过滤器:key散列到hash%过滤器容量,1代表有0代表无,判断key在容量范围内是否存在。因为hash冲突有一定存在但并不存在的错误率哈希函数的个数k;=>double-hashing i从0-k, gi(x) = h1(x) + ih2(x) + i^2 mod m,布隆过滤器位数组的容量m;布隆过滤器插入的数据数量n;datablock: ...

April 24, 2019 · 1 min · jiezi

Titan 的设计与实现

作者:郑志铨Titan 是由 PingCAP 研发的一个基于 RocksDB 的高性能单机 key-value 存储引擎,其主要设计灵感来源于 USENIX FAST 2016 上发表的一篇论文 WiscKey。WiscKey 提出了一种高度基于 SSD 优化的设计,利用 SSD 高效的随机读写性能,通过将 value 分离出 LSM-tree 的方法来达到降低写放大的目的。我们的基准测试结果显示,当 value 较大的时候,Titan 在写、更新和点读等场景下性能都优于 RocksDB。但是根据 RUM Conjecture,通常某些方面的提升往往是以牺牲其他方面为代价而取得的。Titan 便是以牺牲硬盘空间和范围查询的性能为代价,来取得更高的写性能。随着 SSD 价格的降低,我们认为这种取舍的意义会越来越明显。设计目标Titan 作为 TiKV 的一个子项目,首要的设计目标便是兼容 RocksDB。因为 TiKV 使用 RocksDB 作为其底层的存储引擎,而 TiKV 作为一个成熟项目已经拥有庞大的用户群体,所以我们需要考虑已有的用户也可以将已有的基于 RocksDB 的 TiKV 平滑地升级到基于 Titan 的 TiKV。因此,我们总结了四点主要的设计目标:支持将 value 从 LSM-tree 中分离出来单独存储,以降低写放大。已有 RocksDB 实例可以平滑地升级到 Titan,这意味着升级过程不需要人工干预,并且不会影响线上服务。100% 兼容目前 TiKV 所使用的所有 RocksDB 的特性。尽量减少对 RocksDB 的侵入性改动,保证 Titan 更加容易升级到新版本的 RocksDB。架构与实现Titan 的基本架构如下图所示:图 1:Titan 在 Flush 和 Compaction 的时候将 value 分离出 LSM-tree,这样做的好处是写入流程可以和 RockDB 保持一致,减少对 RocksDB 的侵入性改动。Titan 的核心组件主要包括:BlobFile、TitanTableBuilder、Version 和 GC,下面将逐一进行介绍。BlobFileBlobFile 是用来存放从 LSM-tree 中分离出来的 value 的文件,其格式如下图所示:图 2:BlobFile 主要由 blob record 、meta block、meta index block 和 footer 组成。其中每个 blob record 用于存放一个 key-value 对;meta block 支持可扩展性,可以用来存放和 BlobFile 相关的一些属性等;meta index block 用于检索 meta block。BlobFile 有几点值得关注的地方:BlobFile 中的 key-value 是有序存放的,目的是在实现 Iterator 的时候可以通过 prefetch 的方式提高顺序读取的性能。每个 blob record 都保留了 value 对应的 user key 的拷贝,这样做的目的是在进行 GC 的时候,可以通过查询 user key 是否更新来确定对应 value 是否已经过期,但同时也带来了一定的写放大。BlobFile 支持 blob record 粒度的 compression,并且支持多种 compression algorithm,包括 Snappy、LZ4 和 Zstd 等,目前 Titan 默认使用的 compression algorithm 是 LZ4 。TitanTableBuilderTitanTableBuilder 是实现分离 key-value 的关键。我们知道 RocksDB 支持使用用户自定义 table builder 创建 SST,这使得我们可以不对 build table 流程做侵入性的改动就可以将 value 从 SST 中分离出来。下面将介绍 TitanTableBuilder 的主要工作流程:图 3:TitanTableBuilder 通过判断 value size 的大小来决定是否将 value 分离到 BlobFile 中去。如果 value size 大于等于 min_blob_size 则将 value 分离到 BlobFile ,并生成 index 写入 SST;如果 value size 小于 min_blob_size 则将 value 直接写入 SST。Titan 和 Badger 的设计有很大区别。Badger 直接将 WAL 改造成 VLog,这样做的好处是减少一次 Flush 的开销。而 Titan 不这么设计的主要原因有两个:假设 LSM-tree 的 max level 是 5,放大因子为 10,则 LSM-tree 总的写放大大概为 1 + 1 + 10 + 10 + 10 + 10,其中 Flush 的写放大是 1,其比值是 42 : 1,因此 Flush 的写放大相比于整个 LSM-tree 的写放大可以忽略不计。在第一点的基础上,保留 WAL 可以使 Titan 极大地减少对 RocksDB 的侵入性改动,而这也正是我们的设计目标之一。VersionTitan 使用 Version 来代表某个时间点所有有效的 BlobFile,这是从 LevelDB 中借鉴过来的管理数据文件的方法,其核心思想便是 MVCC,好处是在新增或删除文件的同时,可以做到并发读取数据而不需要加锁。每次新增文件或者删除文件的时候,Titan 都会生成一个新的 Version ,并且每次读取数据之前都要获取一个最新的 Version。图 4:新旧 Version 按顺序首尾相连组成一个双向链表,VersionSet 用来管理所有的 Version,它持有一个 current 指针用来指向当前最新的 Version。Garbage CollectionGarbage Collection (GC) 的目的是回收空间,一个高效的 GC 算法应该在权衡写放大和空间放大的同时,用最少的周期来回收最多的空间。在设计 GC 的时候有两个主要的问题需要考虑:何时进行 GC挑选哪些文件进行 GCTitan 使用 RocksDB 提供的两个特性来解决这两个问题,这两个特性分别是 TablePropertiesCollector 和 EventListener 。下面将讲解我们是如何通过这两个特性来辅助 GC 工作的。BlobFileSizeCollectorRocksDB 允许我们使用自定义的 TablePropertiesCollector 来搜集 SST 上的 properties 并写入到对应文件中去。Titan 通过一个自定义的 TablePropertiesCollector —— BlobFileSizeCollector 来搜集每个 SST 中有多少数据是存放在哪些 BlobFile 上的,我们将它收集到的 properties 命名为 BlobFileSizeProperties,它的工作流程和数据格式如下图所示:图 5:左边 SST 中 Index 的格式为:第一列代表 BlobFile 的文件 ID,第二列代表 blob record 在 BlobFile 中的 offset,第三列代表 blob record 的 size。右边 BlobFileSizeProperties 中的每一行代表一个 BlobFile 以及 SST 中有多少数据保存在这个 BlobFile 中,第一列代表 BlobFile 的文件 ID,第二列代表数据大小。EventListener我们知道 RocksDB 是通过 Compaction 来丢弃旧版本数据以回收空间的,因此每次 Compaction 完成后 Titan 中的某些 BlobFile 中便可能有部分或全部数据过期。因此我们便可以通过监听 Compaction 事件来触发 GC,通过搜集比对 Compaction 中输入输出 SST 的 BlobFileSizeProperties 来决定挑选哪些 BlobFile 进行 GC。其流程大概如下图所示:图 6:inputs 代表参与 Compaction 的所有 SST 的 BlobFileSizeProperties,outputs 代表 Compaction 生成的所有 SST 的 BlobFileSizeProperties,discardable size 是通过计算 inputs 和 outputs 得出的每个 BlobFile 被丢弃的数据大小,第一列代表 BlobFile 的文件 ID,第二列代表被丢弃的数据大小。Titan 会为每个有效的 BlobFile 在内存中维护一个 discardable size 变量,每次 Compaction 结束之后都对相应的 BlobFile 的 discardable size 变量进行累加。每次 GC 开始时就可以通过挑选 discardable size 最大的 BlobFile 来作为作为候选的文件。Sample每次进行 GC 前我们都会挑选一系列 BlobFile 作为候选文件,挑选的方法如上一节所述。为了减小写放大,我们可以容忍一定的空间放大,所以我们只有在 BlobFile 可丢弃的数据达到一定比例之后才会对其进行 GC。我们使用 Sample 算法来获取每个候选文件中可丢弃数据的大致比例。Sample 算法的主要逻辑是随机取 BlobFile 中的一段数据 A,计其大小为 a,然后遍历 A 中的 key,累加过期的 key 所在的 blob record 的 size 计为 d,最后计算得出 d 占 a 比值 为 r,如果 r >= discardable_ratio 则对该 BlobFile 进行 GC,否则不对其进行 GC。上一节我们已经知道每个 BlobFile 都会在内存中维护一个 discardable size,如果这个 discardable size 占整个 BlobFile 数据大小的比值已经大于或等于 discardable_ratio 则不需要对其进行 Sample。基准测试我们使用 go-ycsb 测试了 TiKV 在 Txn Mode 下分别使用 RocksDB 和 Titan 的性能表现,本节我会简要说明下我们的测试方法和测试结果。由于篇幅的原因,我们只挑选两个典型的 value size 做说明,更详细的测试分析报告将会放在下一篇文章。测试环境CPU:Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz(40个核心)Memory:128GB(我们通过 Cgroup 限制 TiKV 进程使用内存不超过 32GB)Disk:SATA SSD 1.5TB(fio 测试:4KB block size 混合随机读写情况下读写 IOPS 分别为 43.8K 和 18.7K)测试计划数据集选定的基本原则是原始数据大小(不算上写放大因素)要比可用内存小,这样可以防止所有数据被缓存到内存中,减少 Cache 所带来的影响。这里我们选用的数据集大小是 64GB,进程的内存使用限制是 32GB。Value SizeNumber of Keys (Each Key = 16 Bytes)Raw Data Size1KB64M64GB16KB4M64GB我们主要测试 5 个常用的场景:Data Loading Performance:使用预先计算好的 key 数量和固定的 value 大小,以一定的速度并发写入。Update Performance:由于 Titan 在纯写入场景下不需要 GC(BlobFile 中没有可丢弃数据),因此我们还需要通过更新来测试 GC 对性能的影响。Output Size:这一步我们会测量更新场景完成后引擎所占用的硬盘空间大小,以此反映 GC 的空间回收效果。Random Key Lookup Performance:这一步主要测试点查性能,并且点查次数要远远大于 key 的数量。Sorted Range Iteration Performance:这一步主要测试范围查询的性能,每次查询 2 million 个相连的 key。测试结果图 7 Data Loading Performance:Titan 在写场景中的性能要比 RocksDB 高 70% 以上,并且随着 value size 的变大,这种性能的差异会更加明显。值得注意的是,数据在写入 KV Engine 之前会先写入 Raft Log,因此 Titan 的性能提升会被摊薄,实际上裸测 RocksDB 和 Titan 的话这种性能差异会更大。图 8 Update Performance:Titan 在更新场景中的性能要比 RocksDB 高 180% 以上,这主要得益于 Titan 优秀的读性能和良好的 GC 算法。图 9 Output Size:Titan 的空间放大相比 RocksDB 略高,这种差距会随着 Key 数量的减少有略微的缩小,这主要是因为 BlobFile 中需要存储 Key 而造成的写放大。图 10 Random Key Lookup: Titan 拥有比 RocksDB 更卓越的点读性能,这主要得益与将 value 分离出 LSM-tree 的设计使得 LSM-tree 变得更小,因此 Titan 在使用同样的内存量时可以将更多的 index 、filter 和 DataBlock 缓存到 Block Cache 中去。这使得点读操作在大多数情况下仅需要一次 IO 即可(主要是用于从 BlobFile 中读取数据)。图 11 Sorted Range Iteration:Titan 的范围查询性能目前和 RocksDB 相比还是有一定的差距,这也是我们未来优化的一个重要方向。本次测试我们对比了两个具有代表性的 value size 在 5 种不同场景下的性能差异,更多不同粒度的 value size 的测试和更详细的性能报告我们会放在下一篇文章去说明,并且我们会从更多的角度(例如 CPU 和内存的使用率等)去分析 Titan 和 RocksDB 的差异。从本次测试我们可以大致得出结论,在大 value 的场景下,Titan 会比 RocksDB 拥有更好的写、更新和点读性能。同时,Titan 的范围查询性能和空间放大都逊于 RocksDB 。兼容性一开始我们便将兼容 RocksDB 作为设计 Titan 的首要目标,因此我们保留了绝大部分 RocksDB 的 API。目前仅有两个 API 是我们明确不支持的:MergeSingleDelete除了 Open 接口以外,其他 API 的参数和返回值都和 RocksDB 一致。已有的项目只需要很小的改动即可以将 RocksDB 实例平滑地升级到 Titan。值得注意的是 Titan 并不支持回退回 RocksDB。如何使用 Titan创建 DB#include <assert>#include “rocksdb/utilities/titandb/db.h”// Open DBrocksdb::titandb::TitanDB* db;rocksdb::titandb::TitanOptions options;options.create_if_missing = true;rocksdb::Status status = rocksdb::titandb::TitanDB::Open(options, “/tmp/testdb”, &db);assert(status.ok());…或#include <assert>#include “rocksdb/utilities/titandb/db.h”// open DB with two column familiesrocksdb::titandb::TitanDB* db;std::vector<rocksdb::titandb::TitanCFDescriptor> column_families;// have to open default column familycolumn_families.push_back(rocksdb::titandb::TitanCFDescriptor( kDefaultColumnFamilyName, rocksdb::titandb::TitanCFOptions()));// open the new one, toocolumn_families.push_back(rocksdb::titandb::TitanCFDescriptor( “new_cf”, rocksdb::titandb::TitanCFOptions()));std::vector<ColumnFamilyHandle*> handles;s = rocksdb::titandb::TitanDB::Open(rocksdb::titandb::TitanDBOptions(), kDBPath, column_families, &handles, &db);assert(s.ok());Status和 RocksDB 一样,Titan 使用 rocksdb::Status 来作为绝大多数 API 的返回值,使用者可以通过它检查执行结果是否成功,也可以通过它打印错误信息:rocksdb::Status s = …;if (!s.ok()) cerr << s.ToString() << endl;销毁 DBstd::string value;rocksdb::Status s = db->Get(rocksdb::ReadOptions(), key1, &value);if (s.ok()) s = db->Put(rocksdb::WriteOptions(), key2, value);if (s.ok()) s = db->Delete(rocksdb::WriteOptions(), key1);在 TiKV 中使用 Titan目前 Titan 在 TiKV 中是默认关闭的,我们通过 TiKV 的配置文件来决定是否开启和设置 Titan,相关的配置项包括 [rocksdb.titan] 和 [rocksdb.defaultcf.titan], 开启 Titan 只需要进行如下配置即可:[rocksdb.titan]enabled = true注意一旦开启 Titan 就不能回退回 RocksDB 了。未来的工作优化 Iterator我们通过测试发现,目前使用 Titan 做范围查询时 IO Util 很低,这也是为什么其性能会比 RocksDB 差的重要原因之一。因此我们认为 Titan 的 Iterator 还存在着巨大的优化空间,最简单的方法是可以通过更加激进的 prefetch 和并行 prefetch 等手段来达到提升 Iterator 性能的目的。GC 速度控制和自动调节通常来说,GC 的速度太慢会导致空间放大严重,过快又会对服务的 QPS 和延时带来影响。目前 Titan 支持自动 GC,虽然可以通过减小并发度和 batch size 来达到一定程度限制 GC 速度的目的,但是由于每个 BlobFile 中的 blob record 数目不定,若 BlobFile 中的 blob record 过于密集,将其有效的 key 更新回 LSM-tree 时仍然可能堵塞业务的写请求。为了达到更加精细化的控制 GC 速度的目的,后续我们将使用 Token Bucket 算法限制一段时间内 GC 能够更新的 key 数量,以降低 GC 对 QPS 和延时的影响,使服务更加稳定。另一方面,我们也正在研究自动调节 GC 速度的算法,这样我们便可以,在服务高峰期的时候降低 GC 速度来提供更高的服务质量;在服务低峰期的时候提高 GC 速度来加快空间的回收。增加用于判断 key 是否存在的 APITiKV 在某些场景下仅需要判断某个 key 是否存在,而不需要读取对应的 value。通过提供一个这样的 API 可以极大地提高性能,因为我们已经看到将 value 移出 LSM-tree 之后,LSM-tree 本身会变的非常小,以至于我们可以将更多地 index、filter 和 DataBlock 存放到内存当中去,这样去检索某个 key 的时候可以做到只需要少量甚至不需要 IO 。 ...

January 23, 2019 · 4 min · jiezi