You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

74 lines
11 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 24 | RocksDB不丢数据的高性能KV存储
你好,我是李玥。
上节课我们在讲解CockroachDB的时候提到过CockroachDB的存储引擎是一个分布式的KV存储集群它用了一系列成熟的技术来解决集群问题但是在集群的每个节点上还需要一个单机的KV存储来保存数据这个地方CockroachDB直接使用RocksDB作为它的KV存储引擎。
[RocksDB](https://github.com/facebook/rocksdb)是Facebook开源的一个高性能持久化KV存储。目前你可能很少见到过哪个项目会直接使用RocksDB来保存数据在未来RocksDB大概率也不会像Redis那样被业务系统直接使用。那我们为什么要关注它呢
因为越来越多的新生代数据库都不约而同地选择RocksDB作为它们的存储引擎。在将来很有可能出现什么样的情况呢我们使用的很多不同的数据库它们背后采用的存储引擎都是RocksDB。
我来给你举几个例子。我们上节课讲到的CockroachDB用到了RocksDB作为它的存储引擎。再说几个比较有名的[MyRocks](http://myrocks.io/)这个开源项目你看它这个名字就知道它是干什么的了。它在用RocksDB给MySQL做存储引擎目的是取代现有的InnoDB存储引擎。并且MySQL的亲兄弟MariaDB已经接纳了MyRocks作为它的一个可选的存储引擎。还有大家都经常用的实时计算引擎[Flink](https://flink.apache.org/)用过的同学都知道Flink的State就是一个KV的存储它使用的也是RocksDB。还有包括MongoDB、Cassandra等等很多的数据库都在开发基于RocksDB的存储引擎。
今天这节课我们就一起来了解一下RocksDB这颗“未来之星”。
## 同样是KV存储RocksDB有哪些不同
说到KV存储我们最熟悉的就是Redis了接下来我们就来对比一下RocksDB和Redis这两个KV存储。
其实Redis和RocksDB之间没什么可比性一个是缓存一个是数据库存储引擎放在一起比就像“关公战秦琼”一样。那我们把这两个KV放在一起对比目的不是为了比谁强谁弱而是为了让你快速了解RocksDB能力。
我们知道Redis是一个内存数据库它之所以能做到非常好的性能主要原因就是它的数据都是保存在内存中的。从Redis官方给出的测试数据来看它的随机读写性能大约在50万次/秒左右。而RocksDB相应的随机读写性能大约在20万次/秒左右虽然性能还不如Redis但是已经可以算是同一个量级的水平了。
这里面你需要注意到的一个重大差异是Redis是一个内存数据库并不是一个可靠的存储。数据写到内存中就算成功了它并不保证安全地保存到磁盘上。而RocksDB它是一个持久化的KV存储它需要保证每条数据都要安全地写到磁盘上这也是很多数据库产品的基本要求。这么一比我们就看出来RocksDB的优势了我们知道磁盘的读写性能和内存读写性能差着一两个数量级读写磁盘的RocksDB能和读写内存的Redis做到相近的性能这就是RocksDB的价值所在了。
RocksDB为什么能在保证数据持久化的前提下还能做到这么强的性能呢我们之前反复讲到过一个存储系统它的读写性能主要取决于什么取决于它的存储结构也就是数据是如何组织的。
RocksDB采用了一个非常复杂的数据存储结构并且这个存储结构采用了内存和磁盘混合存储方式使用磁盘来保证数据的可靠存储并且利用速度更快的内存来提升读写性能。或者说RocksDB的存储结构本身就自带了内存缓存。
那我们知道内存缓存可以很好地提升读性能但是写入数据的时候你是绕不过要写磁盘的。因为要保证数据持久化数据必须真正写到磁盘上才行。RocksDB为什么能做到这么高的写入性能还是因为它特殊的数据结构。
大多数存储系统为了能做到快速查找都会采用树或者哈希表这样的存储结构数据在写入的时候必须写入到特定的位置上。比如说我们在往B+树中写入一条数据必须按照B+树的排序方式,写入到某个固定的节点下面。哈希表也是类似,必须要写入到特定的哈希槽中去。
这些数据结构会导致在写入数据的时候不得不在磁盘上这里写一点儿再去那里写一点儿这样跳来跳去地写也就是我们说的“随机写”。而RocksDB它的数据结构可以让绝大多数写入磁盘的操作都是顺序写。那我们知道无论是SSD还是HDD顺序写的性能都要远远好于随机写这就是RocksDB能够做到高性能写入的根本原因。
那我们在《[21 | 类似“点击流”这样的海量数据应该如何存储?](https://time.geekbang.org/column/article/224162)》这节课中讲到过Kafka也是采用顺序读写的方式所以它的读写性能也是超级快。但是这种顺序写入的数据基本上是没法查询的因为数据没有结构想要查询的话只能去遍历。RocksDB究竟使用了什么样的数据结构在保证数据顺序写入的前提下还能兼顾很好的查询性能呢这种数据结构就是**LSM-Tree**。
## LSM-Tree如何兼顾读写性能
LSM-Tree的全称是**The Log-Structured Merge-Tree**是一种非常复杂的复合数据结构它包含了WALWrite Ahead Log、跳表SkipList和一个分层的有序表SSTableSorted String Table。下面这张图就是LSM-Tree的结构图图片来自于论文: [An Efficient Design and Implementation of LSM-Tree based Key-Value Store on Open-Channel SSD](http://ranger.uta.edu/~sjiang/pubs/papers/wang14-LSM-SDF.pdf)
![](https://static001.geekbang.org/resource/image/c0/6e/c0ba7aa330ea79a8a1dfe3a58547526e.jpg)
看起来非常复杂是吧?实际上它的结构比这个图更复杂。那我们尽量忽略没那么重要的细节,把它的核心原理讲清楚。首先需要注意的是,这个图上有一个横向的实线,是内存和磁盘的分界线,上面的部分是内存,下面的部分是磁盘。
我们先来看数据是如何写入的。当LSM-Tree收到一个写请求比如说PUT foo bar把Key foo的值设置为bar。首先这条操作命令会被写入到磁盘的WAL日志中图中右侧的Log这是一个顺序写磁盘的操作性能很好。这个日志的唯一作用就是用于故障恢复一旦系统宕机可以从日志中把内存中还没有来得及写入磁盘的数据恢复出来。这个地方用的还是之前我们多次讲过的复制状态机理论。
写完日志之后数据可靠性的问题就解决了。然后数据会被写入到内存中的MemTable中这个MemTable就是一个按照Key组织的跳表SkipList跳表和平衡树有着类似的查找性能但实现起来更简单一些。写MemTable是个内存操作速度也非常快。数据写入到MemTable之后就可以返回写入成功了。这里面有一点需要注意的是**LSM-Tree在处理写入的过程中直接就往MemTable里写并不去查找这个Key是不是已经存在了**。
这个内存中MemTable不能无限地往里写一是内存的容量毕竟有限另外MemTable太大了读写性能都会下降。所以MemTable有一个固定的上限大小一般是32M。MemTable写满之后就被转换成Immutable MemTable然后再创建一个空的MemTable继续写。这个Immutable MemTable也就是只读的MemTable它和MemTable的数据结构完全一样唯一的区别就是不允许再写入了。
Immutable MemTable也不能在内存中无限地占地方会有一个后台线程不停地把Immutable MemTable复制到磁盘文件中然后释放内存空间。每个Immutable MemTable对应一个磁盘文件MemTable的数据结构跳表本身就是一个有序表写入的文件也是一个按照Key排序的结构这些文件就是SSTable。把MemTable写入SSTable这个写操作因为它是把整块内存写入到整个文件中这同样是一个顺序写操作。
到这里虽然数据已经保存到磁盘上了但还没结束因为这些SSTable文件虽然每个文件中的Key是有序的但是文件之间是完全无序的还是没法查找。这里SSTable采用了一个很巧妙的分层合并机制来解决乱序的问题。
SSTable被分为很多层越往上层文件越少越往底层文件越多。每一层的容量都有一个固定的上限一般来说下一层的容量是上一层的10倍。当某一层写满了就会触发后台线程往下一层合并数据合并到下一层之后本层的SSTable文件就可以删除掉了。合并的过程也是排序的过程除了Level 0第0层也就是MemTable直接dump出来的磁盘文件所在的那一层。以外每一层内的文件都是有序的文件内的KV也是有序的这样就比较便于查找了。
然后我们再来说LSM-Tree如何查找数据。查找的过程也是分层查找先去内存中的MemTable和Immutable MemTable中找然后再按照顺序依次在磁盘的每一层SSTable文件中去找只要找到了就直接返回。这样的查找方式其实是很低效的有可能需要多次查找内存和多个文件才能找到一个Key但实际的效果也没那么差因为这样一个分层的结构它会天然形成一个非常有利于查找的情况越是被经常读写的热数据它在这个分层结构中就越靠上对这样的Key查找就越快。
比如说最经常读写的Key很大概率会在内存中这样不用读写磁盘就完成了查找。即使内存中查不到真正能穿透很多层SStable一直查到最底层的请求还是很少的。另外在工程上还会对查找做很多的优化比如说在内存中缓存SSTable文件的Key用布隆过滤器避免无谓的查找等来加速查找过程。这样综合优化下来可以获得相对还不错的查找性能。
## 小结
RocksDB是一个高性能持久化的KV存储被很多新生代的数据库作为存储引擎。RocksDB在保证不错的读性能的前提下大幅地提升了写入性能这主要得益于它的数据结构LSM-Tree。
LSM-Tree通过混合内存和磁盘内的多种数据结构将随机写转换为顺序写来提升写性能通过异步向下合并分层SSTable文件的方式让热数据的查找更高效从而获得还不错的综合查找性能。
通过分析LSM-Tree的数据结构可以看出来这种数据结构还是偏向于写入性能的优化更适合在线交易类场景因为在这类场景下需要频繁写入数据。
## 思考题
我们刚刚讲了LSM-Tree是如何读写数据的但是并没有提到数据是如何删除的。课后请你去看一下RocksDB或者是LevelDB相关的文档总结一下LSM-Tree删除数据的过程也欢迎你在留言区分享你的总结。
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。