漫谈RocksDB(二)基础讲解——仿佛兮若轻云之蔽月,飘飘兮若流风之回雪

前言

古话说得好:“工欲善其事必先利其器”,要做好一件事情之前先把工具或者武器强化一下还是很值当的。所以本文将会把RocksDB的主要概念向大家讲解一下,方便后面具体内容的展开。本文所提到的概念大家仅需要了解和留个印象,如果不是很理解的话不需要纠结,后续的章节中会详细展开。

正文

RocksDB的概念纷繁复杂,我根据自己的理解将概念分为架构概念、存储概念以及操作概念,分门别类,帮助大家理解。下面就按照这个观其貌、识其物、辩其行的顺序进行讲解。

架构概念

嵌入式数据库

首先RocksDB是一个嵌入式数据库,这个根本特征决定了RocksDB不需要单独部署,也不需要单独的进程,随应用程序一起部署即可,节省资源且便于管理。

另外由于RocksDB未实现真正意义上的分布式(虽然底层可以使用HDFS目录进行存储,但是数据未作分片,所以无法实现真正的分布式),准确来说是一个单机的KV数据库。

但是没有分布式的血统不代表没有一颗成为分布式应用的心。在实际的分布式使用场景中以RocksDB作为某个副本的存储介质,上层通过Paxos或者Raft协议来保证副本之间的数据一致性,完美的明确了RocksDB在分布式应用使用场景中的定位与作用。

LSM

图片

这是RocksDB的LSM树模型,具体的LSM树相关的概念和知识请参考之前的博文俗话说别在一棵树上吊死,那为什么那么多NOSQL都喜欢在LSM树上吊死呢?

 根据这个模型,数据的写入流程为:

  • 任何的写入都会先写到 WAL,然后在写入 Memory Table(Memtable)。当然为了性能,也可以不写入 WAL,但这样就可能面临崩溃丢失数据的风险。Memory Table 通常是一个能支持并发写入的 skiplist,但 RocksDB 同样也支持多种不同的 Memory Table,下面在Memory Table章节详细讲解,用户可以根据实际的业务场景进行选择,如果没有特殊需求,默认即可。

  • 当一个 Memory Table 写满了之后,就会变成 immutable 的 Memory Table,RocksDB 在后台会通过一个 flush 线程根据条件从flush queue中按顺序将 Memory Table flush 到磁盘,生成一个 Sorted String Table(SST) 文件,放在 Level 0 层。当 Level 0 层的 SST 文件个数超过阈值之后,就会通过 Compaction 策略将其放到 Level 1 层,以此类推,直到最底层

Column Family

这是从HBase老师那里学来的,HBase老师就有Column Family的概念,严格上来说HBase是宽列式数据库,列族级别是列式存储,列族内部是行式存储。

由于RocksDB是KV数据库,没有schema的概念,所有数据都是二进制,采用列式存储。不同的Column Family共享WAL,独享sst和memtable,所以Column Family起到了一定的逻辑和资源隔离的作用。

RocksDB的每个键值对都与唯一一个列族(column family)结合。如果没有指定Column Family,键值对将会结合到“default” 列族。

通过共享WAL文件,RocksDB实现了原子写。通过隔离memtable和table文件,RocksDB可以独立配置每个列族并且快速删除它们。

TransactionDB/OptimisticTransactionDB

这个RocksDB两个不同的事务数据库模式,可以理解为悲观事务数据库和乐观事务数据库。对应的是关系型数据库的悲观锁和乐观锁,所以这个应该很好理解,下面来看看两者的具体说明:

  • TransactionDB:简单来说就是在所有写入的时候进行加锁判断,如果无法加锁,则当前数据正在被操作,需要等待;如果加锁成功,则可以在直接操作数据。该类型的事务数据库适用于高并发场景下数据被频繁修改的场景。

  • OptimisticTransactionDB:简单来说就是在写入的数据不使用任何锁,而在提交的时候判断事务是否被修改。如果事务被修改而造成冲突,提交会返回错误,需要重新处理。显而易见,如果频繁修改一条数据,则重新处理的成本过高,所以,该类型数据库适用于大量非事务写入,少量事务写入的场景。

Options

RocksDB的配置项,在实例化RocksDB时作为构造函数传给构造对象。java示例如下:

String dbPath = "/Users/bangcle/Documents/rocksdb/data";
Options options = new Options();
RocksDB rocksDB = RocksDB.open(options, dbPath);

RocksDB有非常多的配置,但是对于多数用户来说,很多选项都是可以不管的,因为他们里面的大多数,都只会影响特定的工作负荷。通常,大多数rocksDB的选项只要保持默认就好了。

下面这些选项,可以帮助我们在大多数情况获得一个合理的开箱即用的性能。建议用户在使用新的rocksdb工程的时候使用以下选项:

cf_options.level_compaction_dynamic_level_bytes = true;
options.max_background_compactions = 4;
options.max_background_flushes = 2;
options.bytes_per_sync = 1048576;
options.compaction_pri = kMinOverlappingRatio;
table_options.block_size = 16 * 1024;
table_options.cache_index_and_filter_blocks = true;
table_options.pin_l0_filter_and_index_blocks_in_cache = true;

如果你有服务使用默认选项运行,而不是使用这些设置,也不太过担心。尽管这些选项比默认选项好,但是它们一般也不会带来明显的性能优化。建议先按照默认配置进行数据处理,如果性能有瓶颈,可以根据需求进行优化,具体的优化措施会在性能优化的章节详细讲解。

存储概念

Memory Table

MemTable是一个内存数据结构,他保存了落盘到SST文件前的数据。他同时服务于读和写:

  • 新的写入总是将数据插入到memtable

  • 读取在查询SST文件前总是要查询memtable,因为memtable里面的数据总是更新的。

一旦一个memtable被写满,他会变成不可修改的,并被一个新的memtable替换。一个后台线程会把这个memtable的内容落盘到一个SST文件,然后这个memtable就可以被销毁了。

影响memtable的最重要的几个选项是:

  • memtable_factory: memtable对象的工厂。通过声明一个工厂对象,用户可以改变底层memtable的实现,并提供事先声明的选项。

  • write_buffer_size:一个memtable的大小

  • db_write_buffer_size:多个列族的memtable的大小总和。这可以用来管理memtable使用的总内存数。

  • write_buffer_manager:除了声明memtable的总大小,用户还可以提供他们自己的写缓冲区管理器,用来控制总体的memtable使用量。这个选项会覆盖db_write_buffer_size

  • max_write_buffer_number:内存中可以拥有刷盘到SST文件前的最大memtable数。

默认的memtable实现是基于skiplist的。除了默认的memtable实现,用户可以使用其他memtable实现,例如HashLinkList,HashSkipList或者Vector,以加快查询速度。下面列出各个MEMTABLE类型的对比表:

MEMTABLE类型SKIPLISTHASHSKIPLISTHASHLINKLISTVECTOR
最佳使用场景通用带特殊key前缀的范围查询带特殊key前缀,并且每个前缀都只有很小数量的行大量随机写压力
索引类型二分搜索哈希+二分搜索哈希+线性搜索线性搜索
是否支持全量db有序扫描天然支持非常耗费资源(拷贝以及排序生成一个临时视图同HashSkipList同HashSkipList
额外内存平均(每个节点有多个指针高(哈希桶+非空桶的skiplist元数据+每个节点多个指针稍低(哈希桶+每个节点的指针低(vector尾部预分配的内存)
Memtable落盘快速,以及固定数量的额外内存慢,并且大量临时内存使用同HashSkipList同HashSkipList
并发插入支持不支持不支持不支持
带Hint插入支持(在没有并发插入的时候不支持不支持不支持

Block Cache

Block Cache是Rocksdb在内存中缓存数据以用于读取的地方。用户可以带上一个期望的空间大小,传一个Cache对象给RocksDB实例。一个缓存对象可以在同一个进程的多个RocksDB实例之间共享,这允许用户控制总的缓存大小。

用户可以配置两个Block Cache,默认的是存储未压缩块的Block Cache,还可以单独配置一个存储压缩块的Block Cache。读取的时候会先拉去未压缩的数据块的缓存,然后才拉取压缩数据块的缓存。在打开直接IO的时候压缩块缓存可以替代OS的页缓存。配置项如下:

# 设置存储未压缩的块
table_options.block_cache = cache;
# 设置存储压缩的块
table_options.block_cache_compressed = another_cache;

RocksDB里面有两种实现方式,分别叫做LRUCache和ClockCache。两个类型的缓存都通过分片来减轻锁冲突。容量会被平均的分配到每个分片,分片之间不共享空间。默认情况下,每个缓存会被分片到64个分片,每个分片至少有512kB空间。

开箱即用的情况下,RocksDB会使用LRU块缓存实现,空间为8MB。如果希望使用自定义的块缓存,调用额外LRUCache()或者NewClockCache()来创建一个缓存对象,然后把它设置到基于块的表选项。用户也可以使用自己实现的缓存,只需要实现Cache接口即可

WAL

RocksDB中的每个更新操作都会写到两个地方:

  • 一个内存数据结构,名为memtable(后面会被刷盘到SST文件)

  • 写到磁盘上的WAL日志。

在出现服务崩溃的时候,WAL日志可以用于完整的恢复memtable中的数据,以保证数据库能恢复到原来的状态并且数据不丢失。

SST

SST文件是RocksDB在磁盘上的file结构,sstfile由block作为基本单位组成,一个sstfile结构由多个data block和meta block组成,其中data block就是数据实体block,meta block为元数据block。sstfile组成的block有可能被压缩(compression),不同level也可能使用不同的compression方式。sstfile如果要遍历block,会逆序遍历,从footer开始。

MANIFEST

RocksDB对文件系统以及存储介质保持不可预知的态度。文件系统操作不是原子的,并且在系统错误的时候容易出现不一致。即使打开了日志系统,文件系统还是不能在一个不合法的重启中保持一致。POSIX文件系统不支持原子化的批量操作。因此,无法依赖RocksDB的数据存储文件中的元数据文件来构建RocksDB重启前的最后的状态。

RocksDB有一个内建的机制来处理这些POSIX文件系统的限制,这个机制就是保存一个名为MANIFEST的ROCKSDB状态变化的事务日志文件。MANIFEST文件用于在重启的时候,恢复rocksdb到最后一个一致的一致性状态。

下面有三个术语需要区分一下:

  • MANIFEST:指通过一个事务日志,来追踪Rocksdb状态迁移的系统

  • Manifest日志:指一个独立的日志文件,它包含RocksDB的状态快照/版本

  • CURRENT:指最后的Manifest日志

Iterator && Snapshot

Iterator方法提供用户RangeScan功能,首先seek到一个特定的key,然后从这个点开始遍历。Iterator也可以实现RangeScan的逆序遍历,当执行Iterator时,用户看到的是一个时间点的一致性视图。

大部分的LSM引擎都不支持高效的RangeScan操作,这是由于执行RangeScan操作时都要访问所有的数据文件导致。但是大部分用户并不仅仅是完全scan所有的数据,相反,很多情况下仅仅需要按照key的前缀字符串去遍历。RocksDB根据这些应用场景,优化了对应的底层实现。用户可以prefix_extractor来声明一个key_prefix,然后RocksDB为每一个key_prefix存储相应的blooms。配置了key_prefix的Iterator操作可以通过对应的bloom bits来避免检索不含有特定key prefix的数据文件,以此可以提高Iterator性能。

Snapshot接口可以创建数据库在某一个时间点的快照。Iterator接口也可以执行在某一个Snapshot上。

某种意义上,Iterator和Snapshot提供了DB在某个时间点的一个一致性视图,但是其实现原理却不一样。快速短期/前台的scan操作比较适合用Iterator,长期/后台操作适合用Snapshot。当使用Iterator时,会对数据库相应时间点的所有底层文件增加引用计数,直到Iterator结束或者释放了引用计数后,这些文件才允许被删除。

Snapshot不关注数据文件是否被删除的问题,Compation进程会感知Snapshot的存在,会保证对应视图的数据不会被删除。当实例重启时,Snapshot会丢失,这是因为RocksDB不会持久化Snapshot相关数据。

操作概念

Flush

在RocksDB中,最终数据的持久化都是保存在SST中,而SST则是由Memtable刷新到磁盘生成的,这就是所谓的Flush过程。此处仅讲述概念,下一篇基础操作中详细讲解Flush过程

Compaction

英文翻译是压缩(与Compression翻译一致,容易混淆),但是我更喜欢称之为合并,因为这个动作做的事情就是SST的合并并下降一层迁移。此处仅讲述概念,下一篇基础操作中详细讲解Compaction过程。

Compression

在每个SST文件里,数据块和索引块会被分别压缩。用户可以指定压缩类型。压缩配置是针对每个列族的。此处仅讲述概念,下一篇基础操作中详细讲解Compression过程。

Ingest

很多时候,我们使用数据库时会有离线向数据库导入数据的需求。比如大量用户在本地的一些离线数据,想要将这一些数据导入到已有的数据库中;或者说NewSQL场景中部分机器离线,重新上线之后的数据增量/全量同步 等场景。这个时候 我们并不想要让这一些数据占用过多的系统资源,更不希望他们对正常的线上业务有影响,所以高效的离线数据导入功能基本上都是数据库必备的功能,RocksDB也不例外。在RocksDB中离线导入的功能是通过Ingest的实现。此处仅讲述概念,下一篇基础操作中详细讲解Ingest过程。

总结

上面的内容比较多,基本上涵盖了RocksDB常用的绝大部分概念,可能需要好好的理解下,这对后续学习RocksDB非常关键,但是相信熟悉HBase以及LSM tree的小伙伴对这些概念应该绝大部分都熟悉,应该比较好上手,如果不是很熟悉,可以看看之前的两篇文章回顾下。

后续会在这篇文章的基础上展开讲一下RocksDB相关的属性以及操作。

最后,笔者长期关注大数据通用技术,通用原理以及NOSQL数据库的技术架构以及使用。如果大家感觉笔者写的还不错,麻烦大家多多点赞和分享转发,也许你的朋友也喜欢。

最后挂个公众号二维码,欢迎大家关注,谢谢大家支持。

 

 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值