背景Google 开源NOSQL存储引擎库LevelDB ,在它的基础之上,Facebook 开发出了另一个 NOSQL 存储引擎库 RocksDB。沿用了 LevelDB 的先进技术架构的同时还解决了 LevelDB 的一些短板。RocksDB虽然在代码层面上是在LevelDB原有的代码上进行开发的,但却借鉴了Apache HBase的一些好的idea。在云计算横行的年代,RocksDB也开始支持HDFS,允许从HDFS读取数据。而LevelDB则是一个比较单一的存储引擎。下面将对比一下两款引擎。
LevelDB介绍
Leveldb是一个google实现的非常高效的kv数据库,能够支持billion级别的数据量。 在这个数量级别下还有着非常高的性能,主要归功于它的良好的设计。特别是LMS算法,但是Leveldb是单进程的服务,而且它只是一个 C/c 编程语言的库, 不包含网络服务封装, 所以无法像一般意义的存储服务器(如 MySQL)那样, 用客户端来连接它. LevelDB 自己也声明, 使用者应该封装自己的网络服务器.所以它只能做一个嵌入式数据来使用,目前淘宝的Tair系统将它做了封装。
特点
LevelDB特点:
- LevelDB是一个持久化存储的KV系统,和Redis这种内存型的KV系统不同,LevelDB不会像Redis一样狂吃内存,而是将大部分数据存储到磁盘上。
- LevleDB在存储数据时,是根据记录的key值有序存储的,就是说相邻的key值在存储文件中是依次顺序存储的,而应用可以自定义key大小比较函数。
- LevelDB支持数据快照(snapshot)功能,使得读取操作不受写操作影响,可以在读操作过程中始终看到一致的数据。
- LevelDB还支持数据压缩等操作,这对于减小存储空间以及增快IO效率都有直接的帮助。
LevelDB局限性:
- 这不是一个 SQL 数据库,它没有关系数据模型,不支持 SQL 查询,也不支持索引。
- 同时只能有一个进程(可能是具有多线程的进程)访问一个特定的数据库。
- 该程序库里没有内置client-server 支持,有需要的用户必须自己封装。
架构
设计思路
LevelDB的数据是存储在磁盘上的,采用LSM-Tree的结构实现。LSM-Tree将磁盘的随机写转化为顺序写,从而大大提高了写速度。为了做到这一点LSM-Tree的思路是将索引树结构拆成一大一小两颗树,较小的一个常驻内存,较大的一个持久化到磁盘,他们共同维护一个有序的key空间。写入操作会首先操作内存中的树,随着内存中树的不断变大,会触发与磁盘中树的归并操作,而归并操作本身仅有批量顺序写。LSM 树结构的问题, 写入速度快,读取速度慢,写放大和读放大都较高。如下图所示:
随着数据的不断写入,磁盘中的树会不断膨胀,为了避免每次参与归并操作的数据量过大,以及优化读操作的考虑,LevelDB将磁盘中的数据又拆分成多层,每一层的数据达到一定容量后会触发向下一层的归并操作,每一层的数据量比其上一层成倍增长。这也就是LevelDB的名称来源。
整体结构
LevelDB有几个重要的角色,包括内存数据的Memtable,分层数据存储的SST文件,版本控制的Manifest、Current文件,以及写Memtable前的WAL。这里简单介绍各个组件的作用和在整个结构中的位置。
- Memtable:内存数据结构,跳表实现,新的数据会首先写入这里;
- Log文件:Commit Log,也称为提交日志。写Memtable前会先写Log文件,Log通过append的方式顺序写入。Log的存在使得机器宕机导致的内存数据丢失得以恢复;
- Immutable Memtable:达到Memtable设置的容量上限后,Memtable会变为Immutable为之后向sst文件的归并做准备,顾名思义,Immutable Mumtable不再接受用户写入,同时会有新的Memtable生成;
- SST文件:磁盘数据存储文件。分为Level 0到Level N多层,每一层包含多个SST文件;单层SST文件总量随层次增加成倍增长;文件内数据有序;其中Level0的SST文件由Immutable直接Dump产生,其他Level的SST文件由其上一层的文件和本层文件归并产生;SST文件在归并过程中顺序写生成,生成后仅可能在之后的归并中被删除,而不会有任何的修改操作。
- Manifest文件: 清单文件。SSTable中的文件是按照记录的主键排序的,每个文件有最小的主键和最大的主键。清单文件记录这些元数据,包括属于哪个层级、文件名称、最小主键和最大主键。
- Current文件: 当前文件记录了当前的清单文件名,即当前的Manifest,而Manifest可能有多个。
总之,当写入一个key-value的时候,首先写入log文件中,然后才会写入memtable中,然后当memtable到达一定程度时,然后转变成Immutable memtable,系统此时会重新创建新的memtable用于插入数据。然后Immutable memtable通过压缩数据存储到磁盘SSTable中。
Compaction操作
LevelDB写入操作简单,但是读取操作比较复杂,需要在内存以及各个层级文件中按照从新到老依次查找,代价很高。为了加快读取速度, LevelDB内部执行Compaction操作来对已有的记录进行整理压缩,从而删除一些不再有效的记录,减少数据规模和文件数量。
Compaction分为两种。
- Minor Compaction是指当内存中的MemTable大小到一定值时,将内存数据转储dump到SSTable中。
- Major Compaction是指每个层级下有多个SSTable,当某个层级下的SSTable文件数目超过一定设置后, LevelDB会从这个层级中选择SSTable文件,将和高一级的SSTable文件进行合并。相当于执行多路归并,按照主键顺序依次迭代出所有SSTable文件中的记录,如果没有保存价值则直接丢掉,否则将其写入到新生成的SSTable文件中。
RocksDB介绍
RocksDB是由 Facebook 基于 LevelDB 开发的一款提供键值存储与读写功能的 LSM-tree 架构引擎,最初的目标是提高服务工作负载的性能,最大限度的发挥闪存和RAM的高度率读写性能。 RocksDB不是一个分布式的DB,而是一个高效、高性能、单点的数据库引擎。这是一个c 库,可用于存储键和值,可以是任意大小的字节流。它支持原子读和写。Rocksdb目前已经运用在许多知名的项目中,例如TiKV,MyRocks,CrockRoach等
特点
RocksDB依靠大量灵活的配置,使之能针对不同的生产环境进行调优,包括直接使用内存,使用Flash,使用硬盘或者HDFS。支持使用不同的压缩算法,并且有一套完整的工具供生产和调试使用。
- 为需要存储TB级别数据到本地FLASH或者RAM的应用服务器设计
- 针对存储在高速设备的中小键值进行优化——你可以存储在flash或者直接存储在内存
- 性能随CPU数量线性提升,对多核系统友好
- 弹性架构,RocksDB支持扩展。RocksDB有可插拔式的API,所以应用可以设计个性化的数据结构来cache DB的写数据,这种措施可以降低IOPS
场景
RocksDB的典型场景(低延时访问):
- 需要存储用户的查阅历史记录和网站用户的应用
- 需要快速访问数据的垃圾检测应用
- 需要实时scan数据集
- 需要实时请求Hadoop的应用
- 支持大量写和删除操作的消息队列
不适合场景
- 大value的场景,需要做kv分离;
- 大规模数据的存取
架构
Rocksdb中引入了ColumnFamily(列族, CF)的概念,所谓列族也就是一系列kv组成的数据集。所有的读写操作都需要先指定列族。写操作先写WAL,再写memtable,memtable达到一定阈值后切换为Immutable Memtable,只能读不能写。后台Flush线程负责按照时间顺序将Immu Memtable刷盘,生成level0层的有序文件(SST)。后台合并线程负责将上层的SST合并生成下层的SST。Manifest负责记录系统某个时刻SST文件的视图,Current文件记录当前最新的Manifest文件名。 每个ColumnFamily有自己的Memtable, SST文件,所有ColumnFamily共享WAL、Current、Manifest文件。
整个系统的设计思路很好理解,这种设计的优势很明显,主要有以下几点:
- 所有的刷盘操作都采用append方式,这种方式对磁盘和SSD是相当有诱惑力的;
- 写操作写完WAL和Memtable就立即返回,写效率非常高。
- 由于最终的数据是存储在离散的SST中,SST文件的大小可以根据kv的大小自由配置, 因此很适合做变长存储。
但是这种设计也带来了很多其他的问题:
- 为了支持批量和事务以及上电恢复操作,WAL是多个CF共享的,导致了WAL的单线程写模式,不能充分发挥高速设备的性能优势(这是相对介质讲,相对B树等其他结构还是有优 势);
- 读写操作都需要对Memtable进行互斥访问,在多线程并发写及读写混合的场景下容易形 成瓶颈。
- 由于Level0层的文件是按照时间顺序刷盘的,而不是根据key的范围做划分,所以导致各个文件之间范围有重叠,再加上文件自上向下的合并,读的时候有可需要查找level0层的多个文件及其他层的文件,这也造成了很大的读放大。尤其是当纯随机写入后,读几乎是要查询level0层的所有文件,导致了读操作的低效。
- 针对第三点问题,Rocksdb中依据level0层文件的个数来做前台写流控及后台合并触发, 以此来平衡读写的性能。这又导致了性能抖动及不能发挥高速介质性能的问题。
- 合并流程难以控制,容易造成性能抖动及写放大。尤其是写放大问题,在笔者的使用过程中实际测试的写放大经常达到二十倍左右。这是不可接受的,当前我们也没有找到合适的解决办法,只是暂时采用大value分离存储的方式来将写放大尽量控制在小数据。
总结实际中,可以将Levledb或者Rocksdb作为数据存储系统引擎,在其上面实现分片和多副本,从而实现一个真正的分布式存储系统,例如微信开源的PaxosStore,默认就是以Rocksdb作为其某个副本的存储介质,上层通过Paxos协议来保证副本之间的数据一致性。虽然RocksDB在性能上提升了不少,在文件存储格式上跟LevelDB还是没什么变化的, 稍微有点更新的只是RocksDB对原来LevelDB中sst文件预留下来的MetaBlock进行了具体利用。
Leveldb 与Rocksdb 对比如下:
- Leveldb是单线程合并文件,Rocksdb可以支持多线程合并文件,充分利用多核的特性,加快文件合并的速度,避免文件合并期间引起系统停顿;
- RocksDB增加了合并时过滤器,对一些不再符合条件的K-V进行丢弃,如根据K-V的有效期进行过滤。
- Leveldb只有一个Memtable,若Memtable满了还没有来得及持久化,则会减缓Put操作引起系统停顿;RocksDB支持管道式的Memtable,也就说允许根据需要开辟多个Memtable,以解决Put与Compact速度差异的性能瓶颈问题。
- Leveldb只能获取单个K-V;Rocksdb支持一次获取多个K-V,还支持Key范围查找。
- Levledb不支持备份;Rocksdb支持全量和增量备份。RocksDB允许将已删除的数据备份到指定的目录,供后续恢复。
- 压缩方面RocksDB可采用多种压缩算法,除了LevelDB用的snappy,还有zlib、bzip2。LevelDB里面按数据的压缩率(压缩后低于75%)判断是否对数据进行压缩存储,而RocksDB典型的做法是Level 0-2不压缩,最后一层使用zlib,而其它各层采用snappy。
- RocksDB除了简单的Put、Delete操作,还提供了一个Merge操作,说是为了对多个Put操作进行合并,优化了modify的效率。站在引擎实现者的角度来看,相比其带来的价值,其实现的成本要昂贵很多。个人觉得有时过于追求完美不见得是好事,据笔者所测(包括测试自己编写的引擎),性能的瓶颈其实主要在合并上,多一次少一次Put对性能的影响并无大碍。
- RocksDB提供一些方便的工具,这些工具包含解析sst文件中的K-V记录、解析MANIFEST文件的内容等。有了这些工具,就不用再像使用LevelDB那样,只能在程序中才能知道sst文件K-V的具体信息了。
- RocksDB可以充分挖掘使用flash的IOPS,在随机读、随机写和bulk load时性能优于LevelDB。在随机写和bulk load时,性能优于LevelDB 10倍,在随机读时性能优于LevelDB 30%。
- 其他优化:增加了column family,这样有利于多个不相关的数据集存储在同一个db中,因为不同column family的数据是存储在不同的sst和memtable中,所以一定程度上起到了隔离的作用。将flush和compaction分开不同的线程池,能有效的加快flush,防止stall拖延停顿。增加了对write ahead log(WAL)的特殊管理机制,这样就能方便管理WAL文件,因为WAL是binlog文件。