tair 是淘宝自己开发的一个分布式 key/value 存储引擎. tair 分为持久化和非持久化两种使用方式. 非持久化的 tair 可以看成是一个分布式缓存. 持久化的 tair 将数据存放于磁盘中. 在最新版本的tair trunk中目前实现了以下4种存储引擎。
非持久化:mdb
持久化:fdb、kdb和 ldb
分别基于四种开源的key/value数据库:Memcached、Firebird、Kyoto Cabinet和LevelDB。其中memcached和Firebird是关系型存储数据库,而Kyoto Cabinet和LevelDB是Nosql数据库。
这里我研究的是Google开源的快速轻量级的单机KV存储引擎leveldb的实现方式。
leveldb的基本特性:
限制:
说到存储引擎我立刻想到MySQL的两大常用存储引擎myisam和innodb,关系型数据库的数据存储在逻辑的表空间中,而数据库索引一般都是用的B/B+树系列,包括MySQL及NoSQL中的MongoDB。那为什么在磁盘中要使用B+树来进行文件存储呢?首先磁盘本身是一个顺序读写快,随机读写慢的系统,磁盘的性能主要受限于磁盘的寻道时间,那么如果想高效的从磁盘中找到数据,就需要减少寻道次数,也就是尽量减少磁盘的IO次数,而磁盘IO次数又取决于数据在磁盘上的组织方式。B+树是一种专门针对磁盘存储而优化的N叉排序树,以树节点为单位存储在磁盘中,从根开始查找所需数据所在的节点编号和磁盘位置,将其加载到内存中然后继续查找,直到找到所需的数据。B+树的存储方式使得具有良好的查找、插入和修改的性能,但如果有大量的更新插入删除等综合写入,最后会因为需要循环利用磁盘块而出现较多的随机IO,大量时间消耗在磁盘寻道时间上,如果是一个运行时间很长的B+树,那么几乎所有的请求,都是随机IO。因为磁盘块本身已经不再连续,很难保证可以顺序读取。这是B+树在磁盘结构中最大的问题。
那么如何能够解决这个问题呢?
目前主流的思路有以下几种
(1)放弃部分读性能,使用更加面向顺序写的树的结构来提升写性能。
这个类别里面,从数据结构来说,就我所知并比较流行的是两类,
一类是COLA(Cache-Oblivious Look ahead Array),代表应用是tokuDB。
一类是LSM tree(Log-structured merge Tree)或SSTable,代表的应用有Cassandra、HBase、levelDB 及众多类BigTable存储。
(2)使用ssd,让寻道成为往事。
Leveldb的数据存储方式采用的是LSM(log-structured-merge)的实现方法,简单来说就是将原来的直接维护索引树变为增量写的方式,这样能够保证对磁盘的操作是顺序的。具体的LSM实现可以看”The log-structured merge-tree“这篇论文。Log-Structured的思想最早由 Rosenblum和Ousterhout于1992年在研究日志结构的文件系统时提出。他们将整个磁盘就看做是一个日志,在日志中存放永久性数据及其索引,每次都添加到日志的末尾;通过将很多小文件的存取转换为连续的大批量传输,使得对于文件系统的大多数存取都是顺序性的,从而提高磁盘带宽利用率,故障恢复速度快。 O'Neil等人受到这种思想的启发,借鉴了Log不断追加(而不是修改)的特点,结合B-tree的数据结构,提出了一种延迟更新,批量写入硬盘的数据结构LSM-tree及其算法。LSM-tree努力地在读和写两方面寻找一个平衡点以最小化系统的存取性能的开销,特别适用于插入频率远大于查询频率的应用场景。
LSM树可以看作是一个N阶合并树。数据写操作(包括插入、修改、删除)都在内存中进行,并且都会创建一个新记录(修改会记录新的数据值,而删除会记录一个删除标志),这些数据在内存中仍然还是一棵排序树,当数据量超过设定的内存阈值后,会将这棵排序树和磁盘上最新的排序树合并。当这棵排序树的数据量也超过设定阈值后,和磁盘上下一级的排序树合并。合并过程中,会用最新更新的数据覆盖旧的数据(或者记录为不同版本)。
在需要进行读操作时,总是从内存中的排序树开始搜索,如果没有找到,就从磁盘上的排序树顺序查找。
在LSM树上进行一次数据更新不需要磁盘访问,在内存即可完成,速度远快于B+树。当数据访问以写操作为主,而读操作则集中在最近写入的数据上时,使用LSM树可以极大程度地减少磁盘的访问次数,加快访问速度。
在了解LSM后,需要知道基于LSM结构的存储引擎的数据有什么存储特点,下面简单总结几点。
(1)leveldb中各个存储文件是分层的,新插入的值放在内存表中,称为memtable(通过skiplist实现),该表写满时变为immutable table,并建立新的memtable接收写操作,而immutable table是不可变更的,会通过Compact过程写入level0,其中的数据被组织成sstable的数据文件,所以,同时最多会存在两个memtable(正在写的memtable和immutable memtable)。level0的文件会通过后台的Compact过程写入level1,level1的文件又会写入level2,依次类推,这是”Merge Dump“的流程。
(2)leveldb在写操作时只是单纯的在文件末尾增加一条记录而不会改动原来的数据,更新key直接插入一条新的key/value数据(即key已经存在),而删除操作可以看成插入一条value为空的数据,为了区分真实kv数据和删除操作的mock数据,使用ValueType来标识:
enum ValueType { kTypeDeletion = 0x0, kTypeValue = 0x1 };
(3)考虑节约空间,leveldb对key的存储进行前缀压缩后再写入sstable,每个entry中会记录key与前一个key前缀相同的字节(shared_bytes)以及自己独有的字节(unshared_bytes)。读取时,对block进行遍历,每个key根据前一个key以及shared_bytes/unshared_bytes可以构造出来。
(4)最重要的一点:由于tair是根据hash分区的,而prefix系列的接口是根据prefixkey去hash的。所以能确保拥有相同prefix的key在同一台服务器上。
(1)tair官网介绍
(2)leveldb官网介绍
(3)日志结构的合并树 The Log-Structured Merge-Tree
(4)从LSM-Tree、COLA-Tree谈到StackOverflow、OSQA
(5)为什么文件存储要选用B 树这样的数据结构?