RocksDB是用C++编写的嵌入式KV存储引擎,由Facebook基于levelDB开发,它支持多种存储硬件,使用日志结构的数据库引擎(基于LSM-Tree)来存储数据。
话不多说,首先放一个我理解的架构以及读写流程简易版概览图(PC上看排版好些...):
基础概念
Column Family (列族)
:与HBase中的列族不同,在RocksDB中没有列的概念,在这里列族是一个DB里的独立键值空间,可以理解为namespace。每个键值对都必须唯一关联一个列族,如果没有指定列族会使用系统默认的'default'。
内存中:
active memtable (也叫write buffer 写缓冲)
: 它服务于用户的读请求和写请求,memtable中的数据总是最新的,一旦memtable被写满,它会变成一个不可修改的memtable,称为immutable memtable,并生成一个新的active memtable提供读写服务。每个列族拥有一个专属的memtable,用来写入和读取,如上图的两个。memtable支持多种容器实现,如Skiplit(默认)/HashSkiplist/HashLinklist/Vertor等,put进来的key和value会与序列号,type等信息组装成一个数据结构存放在如上的容器中。
immutable memtable (不可变的memtable)
:由active memtable转换而来,它只能提供读服务,不能修改,后台会有线程来触发将其flush到磁盘上,相同列族memtable转换的immutable memtable会merge到一起,生成一个文件(level 0层的sst文件)。
block cache (块缓存)
:用于在内存缓存来自SST文件的热数据的数据结构,提供高速的查询服务。所有对列族的查询共享一个block cache的实例。之所以叫block cache,是因为它缓存sst中的数据,而sst文件中是由多个data block组成的。在RocksDB中有两种缓存的实现,叫做LRUCache和ClockCache。两种类型的缓存都是分片的,来减少锁的竞争。用户可以指定容量大小,容量会平均分配给每个分片,且他们不共享。默认情况下,每个缓存会被分为64个分片,每个分片不少于512KB的容量。详情见官方Block-Cache文档
磁盘上:
SST文件 (Sorted Sequence Table)
:上图绿色块块的部分,它们是存储持久化数据的文件。从命名上可以看出,在SST文件中的key总是有序地组织到一起,因此可以通过二分查找来找到指定的Key或者迭代的位置。sst文件是不可以修改的,RocksDB通过顺序写代替随机写来大幅提升磁盘写入的性能。
SST文件有多种格式,默认为BlockBasedTable,顾名思义是基于数据块(data-block)存储的,主要是遍历memtable或者其他sst,顺序写入,当大小达到块的大小时,做一个压缩(Compression),放入sst文件中,依次类推,除了数据块sst中还有元数据块(meta block)、索引块(index block)等辅助的数据结构。
WAL(Write Ahead Log)
:顾名思义,WAL会把所有memtable的写操作序列化后以日志的形式存储在持久化介质中。当进程发生崩溃的时候,WAL可以用于重新构建memtable,帮助数据库恢复到一个一致的状态。
默认配置下,RocksDB默认通过对每次用户写操作都flush WAL来保证进程crash的一致性。当然,如果你可以容忍数据丢失可以选择一定数量数据后刷盘,或者关闭WAL来提升写的性能。
WAL对应所有列族的memtable写操作序列化,在db实例open的时候创建,活跃的WAL只有一个文件,当达到一定条件后(下面会讲),WAL会转化为一个不可修改的WAL,并生成一个新的WAL继续提供服务。WAL什么时候销毁,格式什么样子,事务怎么支持等,会在之后的笔记中单独讲述(也许...),可以看官方WAL文档。
LSM & Level Compaction
:RocksDB是基于LSM-Tree(Log Structured Merge Tree)的,在RocksDB中使用SST文件分层管理持久化的数据,即level 0到level N(如上图),每层包含0个或多个sst文件,当满足某种规则时,通过Compaction操作将level x中一个或多个文件与下层合并,默认的Compaction方式为Level Compaction,以下也都是基于Level Compaction的流程。
除了level 0层,其他层的sst文件的key范围是不相交的,上面我们知道sst文件内部的key是有序的,因此level 1+的每一层的所有key在这一层整体来看也是有序的,在查找时整体也是二分查找。
level 0层的sst文件key有交集,是因为level 0的文件是直接由内存的immutable memtable flush而来,而多次落盘的memtable之间可能会有交集。例如,我们先对key1到key5这5个key做了put操作,触发落盘后在001.sst中,然后又对key2,key4做了put,再次落盘落在002.sst中,两个文件是有交集的。
那么level 1+的sst文件怎么保证没有交集呢,这就通过了Compaction这个操作来实现,Compaction主要流程(如下图):在level x中选中一个或多个sst文件与level x+1中与这些文件有交集的sst文件做合并成新的sst文件,放入level x+1中(如果没有交集则直接落到level x+1层),并删除这些选中的文件,Compaction完成后如果level x+1也满足Compaction条件则继续向下做Compaction。这样就保证了每层Level不会过大,并且在Compaction的过程中能减少空间放大(有些对相同key多次操作都合并成一个了,例如对上下层都存有对key1做了put操作的记录,我们只需要留最新的put)。
如下入所示,我们选中了level1层的002.sst来做Compaction,首先根据它包含key的范围在level 2层中查找与这些key有交集的sst文件,即其中的黄色三个文件(003.sst/004.sst/005.sst),然后遍历这些文件生成新的sst文件放到下方,我们发现生成了4个文件,他们的文件名分别是(007-010).sst,其中009.sst是因为没有交集而新增的文件。之前的003-005都没了,是因为sst文件是不能修改的,都是通过新增的方式来完成修改操作的(顺序写)。(PS:这个图是示意图,具体合并细节还没有深究)
LSM & Compaction是RocksDB的核心,关于LSM结构带来的各种放大(读放大、写放大、空间放大)、level层数,每层容量,什么时候做Compaction,Compaction中level的选择,level中哪些文件来做Compaction,其他Compaction类型等,东西太多,之后会有单独的笔记解释(也许...),可以看官方Compaction文档。
Write流程
- 写入WAL & memtable:这个是写入的同步流程,会生成一个序列号存入WAL和memtable。
- memtable flush:acitve table满了之后会变成immutable memtable,接着会有异步写入到level 0中的sst文件中。
- WAL convertion:memtable flush后,会生成一个新的WAL文件接受写入,旧的WAL会在之后达到条件后被删除。
- compaction:当某些level满足条件会触发compaction,将本层一个或多个的sst文件与下层的merge成新的sst文件。
Read流程
- 根据数据由新到旧:active memtable -> immutable memtable -> block cache(可能有多级) -> level 0 -> level N.
主要参数简介
active memtable->immutable memtable->sst流程相关参数:
write_buffer_size (默认为64MB)
:单个active memtable 达到该阈值大小会转化为immutable memtable。
min_write_buffer_number_to_merge (默认为 1)
:用于判断是启动flush线程来flush到sst,它表示相同列族下至少需要多少个immutable memtable才会去将他们merge后flush到sst中,1意味着只有要新增immutable memtable就会触发flush操作。
merge的操作可以减少写入sst的key的数量,从而减少各种放大,例如某个able中对相同的key多次put,只需留最后一个put,如果是put & delete只需留后面的delete操作,为什么不能直接删除这个key,是因为磁盘中的sst文件中可能还存在对这个key相关的put操作,需要将delete操作通过Compaction向下传递,直到最下层(level N)的key才可以真正删掉。
max_write_buffer_number
:参数控制了memtable的总数量(所有列族的active和immutable),当写入生成immutable memtable的速度大于flush速度时,memtable总数 >= max_write_buffer_number时,会停止接受写入,直到memtable flush完成,这个由于现象称为Write-Stall,还有其他的情况可能会触发,比如上面提到的Compaction。
max_background_flushes(默认1)
:并发的flush线程数,当上条原因导致的Write-Stall,可以调整这个参数大小。
db_write_buffer_size (默认0, 即不限制)
:所有列族的memtable总大小超过db阈值时,会强制将最大的memtable flush到硬盘。
write_buffer_manager (默认为null)
:用户可以提供自己的写缓冲管理器,会覆盖db_write_buffer_size 来控制总体的memtable使用大小。
max_total_wal_size (默认为0)
:WAL文件总大小,当wal文件的总大小超过整个值时,会选择删除WAL文件控制大小(我的猜测是选择最老的),上面我们说到wal文件记录着所有列族的memtable的写入操作,当某些memtable flush的时候会将wal文件转为只读,并新增一个wal文件来接受写入。
immutable wal文件会根据它持有的最大请求序列号是否已经全部flush到sst来判断是否可以“删除”,如果文件中包含其他列族memtable还没有flush的数据,就不会被删除 ,那么现在达到max_total_wal_size大小了必须删除这个wal,我们需要根据wal文件中持有的最大请求序列号,将所有包含<=这个序列号的memtable做flush操作。这也就意味着可能有些active memtable还没有full,就被flush了,也是导致产生sst小文件的原因之一。
Compaction相关参数:(先鸽,之后写Compaction中再详解)
The End
学习RocksDB主要是因为Flink中线上越来越多的Job拥有很大的state,而目前的内存还远不能满足(主要是贵呀...),于是需要使用RocksDB这种借助硬盘和少量内存来高效地完成对数据存储和查询的存储引擎。写这篇笔记主要是记录下学习RocksDB的过程,加深印象,对于RocksDB在Flink中的调优做基础,内容是从官网以及网上各位大佬那学习总结而来,还有很多没有想明白的事,也许有不对的地方,请多指教。写之前感觉挺好写,写起来发现这个乱呀,主要是有个大体的认识,细节的东西之后笔记在研究。
参考链接:
官方wiki:https://github.com/facebook/rocksdb/wiki
中文版wiki:https://rocksdb.org.cn/doc.html
Compaction原理:https://www.jianshu.com/p/e89cd503c9ae
Compaction流程详解:https://www.cnblogs.com/cchust/p/6007486.html