RocksDB 是Facebook 开源的一个高性能、持久化的KV存储引擎,最初是Facebook 的数据库工程师团队基于Google LevelDB 开发。一般来说我们很少见到过哪个项目直接使用RocksDB 来保存数据,即使未来大概也不会像Redis 那样被业务系统直接使用。
但是越来越多的新生代数据库都选择RocksDB 作为它们的存储引擎。比如:
- CockroachDB(中文名蟑螂,一个开源、可伸缩、跨地域复制且兼容事务的ACID特性的分布式数据库,思路源自Google 的全球性分布式数据库 Spanner。其理念是将数据分布在多数据中心的多台服务器上)
- YugabyteDB,Tidb 作为CockroachDB 的竞争产品,底层也是RocksDB开源项目MyRocks 使用RocksDB 给MySQL 做存储引擎,目的是取代现有的InnoDB 存储引擎。
- MySQL 的亲兄弟 MariaDB 也已经接纳了MyRocks,作为它的存储引擎。
- 实时计算引擎Flink,其State 就是一个KV 存储,它使用的也是RocksDB。
- 此外包括MongoDB、Cassandra、Hbase 等在内的很多数据库,都在开发基于RocksDB 的存储引擎。
这其中的主要原因就是RocksDB 高性能、支持事务。随机读最高可以达到19W/S,平均水平13W/S,覆盖操作可以达到9W/S,多读单写的情况下在10W/S 左右。批量写入5000 万数据,RocksDB 只花了1m26.9s,在几个对照引擎中(LevelDB与RocksDB 与HyperLevelDB 与LMDB),RocksDB 读取和删除方面表现最好,InfluxDB 也认为根据测试用例,RocksDB 作为存储引擎是个非常好的选择
RocksDB VS Redis
说到KV存储,我们最熟悉的就是Redis了。RocksDB 和 Redis 都是KV 存储。其实Redis 和 RocksDB 之间并没有可比性,一个是缓存,一个是数据库存储引擎。Redis 是一个内存数据库,它的性能非常好,主要原因之一是它的数据全都保存在内存中。按照 Redis 官方网站提供的测试数据来看,它的随机读写性能大约为50 万次/秒,当然我们普遍的认可的说法是10W/S。从我们上面的文字可以看到RocksDB 相应的随机读写性能大约为20 万次/秒,虽然其性能还不如 Redis,但是已经可以算是在同一个量级水平了,毕竟一个是内存操作,另一个是磁盘IO操作。
但是我们也知道,Redis 只是一个内存数据库,并不是一个可靠的存储引擎。在Redis 中,数据写到内存中就算成功了,其并不能保证将数据安全地保存到磁盘上。而 RocksDB 则是一个持久化的KV 存储引擎,它需要保证每条数据都已安全地写到磁盘上。这种情况下,RocksDB 的优势就很明显,磁盘的读写性能与内存的读写性能本就相差了一两个数量级,读写磁盘的RocksDB 能达到与读写内存的Redis 相近的性能这就是RocksDB 的价值所在了。一个存储系统的读写性能主要取决于它的存储结构,也就是数据是如何组织的。RocksDB 采用了一个非常复杂的数据存储结构,并且这个存储结构采用了内存和磁盘混合存储的方式,它使用磁盘来保证数据的可靠存储的,并且会利用速度更快的内存来提升读写性能。
RocksDB 为什么能实现这么高的写入性能呢?大多数存储系统为了能够实现快速查找都会采用树或哈希表之类的存储结构。数据在写入的时候必须写到特定的位置上。比如,我们在向B+树中写入一条数据时,必须按照B+树的排序方式,写到某个固定的节点下面。哈希表也与之类似,必须要写到特定的哈希槽中。这样的数据结构会导致在写入数据的时候,不得不先在磁盘的这里写一部分,再到那里写一部分这样跳来跳去地写,即我们所说的“随机写”。MySQL 为了减少随机读写,下了不少功夫。而RocksDB 的数据结构,可以保证写入磁盘的绝大多数操作都是顺序写入的。
Kafka 所采用的也是顺序读写的方式,所以读写性能也非常好。凡事有利也有弊,这种数据基本上是没法查询的因为数据没有结构,只能采用遍历的方式。RocksDB 究竟如何在保证数据顺序写入的前提下,还能兼顾很好的查询性能呢??其实使用了数据结构LSM-Tree。
LSM-Tree 如何兼顾读写性能
LSM-Tree 的全称是The Log-Structured Merge-Tree,是一种非常复杂的复合数据结构。它包含了WAL (Write Ahead Log)、跳表(SkipList)和一个分层的有序表(Sorted String Table, SSTable),LSM-tree 专门为key-value 存储系统设计的,以牺牲部分读取性能为代价提高写入性能,通常适合于写多读少的场景。
数据的写入过程。当LSM-Tree 收到一个写请求时,比如“PUT foo bar”,即把Key foo 的值设置为bar,这条操作命令会被写入磁盘的 WAL 日志中,这是一个顺序写磁盘的操作,性能很好。这个日志的唯一作用就是从故障中恢复系统数据,一旦系统宕机,就可以根据日志把内存中还没有来得及写入磁盘的数据恢复出来。写完日志之后,数据可靠性的问题就解决了。然后数据会被写入内存的MemTable 中,这个 MemTable 就是一个按照Key 组织的跳表(SkipList),跳表的查找性能与平衡树类似,但实现起来更简单一些。写MemTable 是一项内存操作,速度也非常快。
数据写入MemTable 之后就可以返回写入成功的信息了。不过LSM-Tree 在处理写入数据的过程中会直接将Key 写入 MemTable,而不会预先查看MemTable中是否已经存在该Key。很明显MemTable 不能无限制地写入内存,一是内存的容量毕竟有限,另外,MemTable 太大会导致读写性能下降。所以MemTable 有一个固定的上限大小,一般是32MB。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 以外,每层中的文件都是有序的,文件内的KV 也是有序的,这样就比较便于查找了。**LSM-Tree 查找的过程也是分层查找,先在内存中的 MemTable 和Immutable MemTable 中查找,然后再按照顺序依次在磁盘的每层SSTable 文件中查找,一旦找到了就直接返回。看起来这样的查找方式很低效的,可能需要多次查找内存和多个文件才能找到一个Key,但实际上这样一个分层的结构,它会天然形成一个非常有利于查找的状况,即越是经常被读写到的热数据,它在这个分层结构中就越靠上,对这样的Key 查找就越快。**比如最经常读写的Key 很大概率会在内存中,这样不用读写磁盘就能完成查找。即使在内存中查不到真正需要穿透很多层SSTable,一直查到最底层的请求,实际也还是很少的。另外在工程实现上,还会做很多优化。
比如RocksDB 里,为存储的数据逻辑分族,独特的filter 对读优化,使用多个memtable 并在immutbale memtable 提前进行数据合并的优化,在内存中缓存SSTable文件的Key用布隆过滤器避免无谓的查找等,以加速查找过程;不同的合并算法,SSTFile-Indexing 机制,针对sstfile 有自己的block cache 和table cache。这样综合优化下来最终的性能,尤其是查找会相对较好。
流程总结
- 操作命令写入磁盘的 WAL 日志中(顺序写):唯一作用就是从故障中恢复系统数据
- 写入内存的MemTable中(按照Key组织的跳表,MemTable 太大会导致读写性能下降,一般是32MB)
- 直接将Key 写入 MemTable,而不会预先查看MemTable中是否已经存在该Key
- 写入MemTable之后就可以返回写入成功
- MemTable写满之后,就会转换成Immutable(不可变的)MemTable,然后再创建一个空的MemTable继续写
- Immutable MemTable也不能在内存中无限地占地方,而是会有一个后台线程,不停地把Immutable MemTable复制到磁盘文件中,然后释放内存空间。
- 每个Immutable MemTable 对应于一个磁盘文件,这些文件就是SSTable(这些SSTable文件中的Key是有序的,但是文件之间却是完全无序的,所以还是无法查找)
- SSTable 采用分层合并机制,SSTable被分为很多层,每一层的容量都有一个固定的上限。下一层的容量是上一层的10倍。
- 当某一层写满时,就会触发后台线程往下一层合并
- 数据合并到下一层之后,本层的SSTable文件就可以删除了
- 合并的过程也是排序的过程,除了Level 0以外,每层中的文件都是有序的
- LSM-Tree 查找的过程也是分层查找
- 先在内存中的 MemTable 和Immutable MemTable 中查找,然后再按照顺序依次在磁盘的每层SSTable 文件中查找,一旦找到了就直接返回
- 越是经常被读写到的热数据,它在这个分层结构中就越靠上,对这样的Key 查找就越快
- 在内存中缓存SSTable文件的Key用布隆过滤器避免无谓的查找等,以加速查找过程
- 使用多个memtable并在immutbale memtable提前进行数据合并的优
- 为存储的数据逻辑分族,独特的filter 对读优化
总结梳理
虽然其性能还不如 Redis,但是已经可以算是在同一个量级水平了,毕竟一个是内存操作,另一个是磁盘IO操作。RocksDB 的数据结构,可以保证写入磁盘的绝大多数操作都是顺序写入的。
LSM-Tree 如何兼顾读写性能
-
操作命令写入磁盘的 WAL 日志中(顺序写):唯一作用就是从故障中恢复系统数据
-
写入内存的MemTable中(按照Key组织的跳表,MemTable 太大会导致读写性能下降,一般是32MB)
-
直接将Key 写入 MemTable,而不会预先查看MemTable中是否已经存在该Key
-
写入MemTable之后就可以返回写入成功
-
-
MemTable写满之后,就会转换成Immutable(不可变的)MemTable,然后再创建一个空的MemTable继续写
-
Immutable MemTable也不能在内存中无限地占地方,而是会有一个后台线程,不停地把Immutable MemTable复制到磁盘文件中,然后释放内存空间。
-
每个Immutable MemTable 对应于一个磁盘文件,这些文件就是SSTable(这些SSTable文件中的Key是有序的,但是文件之间却是完全无序的,所以还是无法查找)
-
-
SSTable 采用分层合并机制,SSTable被分为很多层,每一层的容量都有一个固定的上限。下一层的容量是上一层的10倍。
-
当某一层写满时,就会触发后台线程往下一层合并
-
数据合并到下一层之后,本层的SSTable文件就可以删除了
-
合并的过程也是排序的过程,除了Level 0以外,每层中的文件都是有序的
-
-
LSM-Tree 查找的过程也是分层查找
-
先在内存中的 MemTable 和Immutable MemTable 中查找,然后再按照顺序依次在磁盘的每层SSTable 文件中查找,一旦找到了就直接返回
-
越是经常被读写到的热数据,它在这个分层结构中就越靠上,对这样的Key 查找就越快
-
在内存中缓存SSTable文件的Key用布隆过滤器避免无谓的查找等,以加速查找过程
-
使用多个memtable并在immutbale memtable提前进行数据合并的优
-
为存储的数据逻辑分族,独特的filter 对读优化
-