代码改变世界

数据分析与处理之二(Leveldb 实现原理)

2011-12-04 21:24 by Haippy, ... 阅读, ... 评论, 收藏, 编辑

郑重声明:本篇博客是自己学习 Leveldb 实现原理时参考了郎格科技系列博客整理的,原文地址http://www.samecity.com/blog/Index.asp?SortID=12,只是为了加深印象,本文的配图是自己重新绘制的,大部分内容与原文相似,大家可以浏览原始页面 :-),感兴趣的话可以一起讨论 Leveldb 的实现原理!

LevelDb日知录之一LevelDb 101

  说起LevelDb也许您不清楚但是如果作为IT工程师不知道下面两位大神级别的工程师那您的领导估计会Hold不住了Jeff DeanSanjay Ghemawat。这两位是Google公司重量级的工程师为数甚少的Google Fellow之二

  Jeff Dean其人http://research.google.com/people/jeff/index.html,Google大规模分布式平台BigtableMapReduce主要设计和实现者

  Sanjay Ghemawat其人http://research.google.com/people/sanjay/index.html,Google大规模分布式平台GFS,BigtableMapReduce主要设计和实现工程师

  LevelDb就是这两位大神级别的工程师发起的开源项目简而言之LevelDb是能够处理十亿级别规模Key-Value型数据持久性存储的C++ 程序库正像上面介绍的这二位是Bigtable的设计和实现者如果了解Bigtable的话应该知道在这个影响深远的分布式存储系统中有两个核心的部分Master ServerTablet Server。其中Master Server做一些管理数据的存储以及分布式调度工作实际的分布式数据存储以及读写操作是由Tablet Server完成的LevelDb则可以理解为一个简化版的Tablet Server。

  LevelDb有如下一些特点

    首先LevelDb是一个持久化存储的KV系统Redis这种内存型的KV系统不同LevelDb不会像Redis一样狂吃内存而是将大部分数据存储到磁盘上

    其次LevleDb在存储数据时是根据记录的key值有序存储的就是说相邻的key值在存储文件中是依次顺序存储的而应用可以自定义key大小比较函数LevleDb会按照用户定义的比较函数依序存储这些记录

    再次像大多数KV系统一样LevelDb的操作接口很简单基本操作包括写记录读记录以及删除记录也支持针对多条操作的原子批量操作

    另外LevelDb支持数据快照snapshot)功能使得读取操作不受写操作影响可以在读操作过程中始终看到一致的数据

  除此外LevelDb还支持数据压缩等操作这对于减小存储空间以及增快IO效率都有直接的帮助

  LevelDb性能非常突出官方网站报道其随机写性能达到40万条记录每秒而随机读性能达到6万条记录每秒总体来说LevelDb的写操作要大大快于读操作而顺序读写操作则大大快于随机读写操作至于为何是这样看了我们后续推出的LevelDb日知录估计您会了解其内在原因

LevelDb日知录之二:整体架构

      LevelDb本质上是一套存储系统以及在这套存储系统上提供的一些操作接口为了便于理解整个系统及其处理流程我们可以从两个不同的角度来看待LevleDb:静态角度和动态角度从静态角度可以假想整个系统正在运行过程中不断插入删除读取数据),此时我们给LevelDb照相从照片可以看到之前系统的数据在内存和磁盘中是如何分布的处于什么状态等从动态的角度主要是了解系统是如何写入一条记录读出一条记录删除一条记录的同时也包括除了这些接口操作外的内部操作比如compaction,系统运行时崩溃后如何恢复系统等等方面

     本节所讲的整体架构主要从静态角度来描述之后接下来的几节内容会详述静态结构涉及到的文件或者内存数据结构LevelDb日知录后半部分主要介绍动态视角下的LevelDb,就是说整个系统是怎么运转起来的

     LevelDb作为存储系统数据记录的存储介质包括内存以及磁盘文件如果像上面说的LevelDb运行了一段时间此时我们给LevelDb进行透视拍照那么您会看到如下一番景象

1.1:LevelDb结构

    从图中可以看出构成LevelDb静态结构的包括六个主要部分内存中的MemTableImmutable MemTable以及磁盘上的几种主要文件Current文件Manifest文件log文件以及SSTable文件当然LevelDb除了这六个主要部分还有一些辅助的文件但是以上六个文件和数据结构是LevelDb的主体构成元素

LevelDbLog文件和MemtableBigtable论文中介绍的是一致的当应用写入一条Key:Value记录的时候LevelDb会先往log文件里写入成功后将记录插进Memtable这样基本就算完成了写入操作因为一次写入操作只涉及一次磁盘顺序写和一次内存写入所以这是为何说LevelDb写入速度极快的主要原因

Log文件在系统中的作用主要是用于系统崩溃恢复而不丢失数据假如没有Log文件因为写入的记录刚开始是保存在内存中的此时如果系统崩溃内存中的数据还没有来得及Dump到磁盘所以会丢失数据Redis就存在这个问题)。为了避免这种情况LevelDb在写入内存前先将操作记录到Log文件中然后再记入内存中这样即使系统崩溃也可以从Log文件中恢复内存中的Memtable,不会造成数据的丢失

Memtable插入的数据占用内存到了一个界限后需要将内存的记录导出到外存文件中LevleDb会生成新的Log文件和Memtable,原先的Memtable就成为Immutable Memtable,顾名思义就是说这个Memtable的内容是不可更改的只能读不能写入或者删除新到来的数据被记入新的Log文件和Memtable,LevelDb后台调度会将Immutable Memtable的数据导出到磁盘形成一个新的SSTable文件SSTable就是由内存中的数据不断导出并进行Compaction操作后形成的而且SSTable的所有文件是一种层级结构第一层为Level 0,第二层为Level 1,依次类推层级逐渐增高这也是为何称之为LevelDb的原因

    SSTable中的文件是Key有序的就是说在文件中小key记录排在大Key记录之前各个LevelSSTable都是如此但是这里需要注意的一点是Level 0SSTable文件后缀为.sst)和其它Level的文件相比有特殊性这个层级内的.sst文件两个文件可能存在key重叠比如有两个level 0sst文件文件A和文件B,文件Akey范围是:{bar, car},文件BKey范围是{blue,samecity},那么很可能两个文件都存在key=”blood”的记录对于其它LevelSSTable文件来说则不会出现同一层级内.sst文件的key重叠现象就是说Level L中任意两个.sst文件那么可以保证它们的key值是不会重叠的这点需要特别注意后面您会看到很多操作的差异都是由于这个原因造成的

    SSTable中的某个文件属于特定层级而且其存储的记录是key有序的那么必然有文件中的最小key和最大key,这是非常重要的信息LevelDb应该记下这些信息Manifest就是干这个的它记载了SSTable各个文件的管理信息比如属于哪个Level,文件名称叫啥最小key和最大key各自是多少下图是Manifest所存储内容的示意

2.1:Manifest存储示意图

图中只显示了两个文件manifest会记载所有SSTable文件的这些信息),Level 0test.sst1test.sst2文件同时记载了这些文件各自对应的key范围比如test.sstt1key范围是an”banana”,而文件test.sst2key范围是baby”samecity”,可以看出两者的key范围是有重叠的

Current文件是干什么的呢这个文件的内容只有一个信息就是记载当前的manifest文件名因为在LevleDb的运行过程中随着Compaction的进行SSTable文件会发生变化会有新的文件产生老的文件被废弃Manifest也会跟着反映这种变化此时往往会新生成Manifest文件来记载这种变化Current则用来指出哪个Manifest文件才是我们关心的那个Manifest文件

以上介绍的内容就构成了LevelDb的整体静态结构LevelDb日知录接下来的内容中我们会首先介绍重要文件或者内存数据的具体数据布局与结构

LevelDb日知录之三:log文件

     上节内容讲到log文件在LevelDb中的主要作用是系统故障恢复时能够保证不会丢失数据因为在将记录写入内存的Memtable之前会先写入Log文件这样即使系统发生故障Memtable中的数据没有来得及Dump到磁盘的SSTable文件LevelDB也可以根据log文件恢复内存的Memtable数据结构内容不会造成系统丢失数据在这点上LevelDbBigtable是一致的

     下面我们带大家看看log文件的具体物理和逻辑布局是怎样的LevelDb对于一个log文件会把它切割成以32K为单位的物理Block,每次读取的单位以一个Block作为基本读取单位下图展示的log文件由3Block构成所以从物理布局来讲一个log文件就是由连续的32K大小Block构成的

3.1 log文件布局

        在应用的视野里是看不到这些Block应用看到的是一系列的Key:ValueLevelDb内部会将一个Key:Value对看做一条记录的数据另外在这个数据前增加一个记录头用来记载一些管理信息以方便内部处理3.2显示了一个记录在LevelDb内部是如何表示的

 

3.2 记录结构

       记录头包含三个字段ChechSum是对类型数据字段的校验码为了避免处理不完整或者是被破坏的数据LevelDb读取记录数据时候会对数据进行校验如果发现和存储的CheckSum相同说明数据完整无破坏可以继续后续流程。“记录长度记载了数据的大小,“数据则是上面讲的Key:Value数值对,“类型字段则指出了每条记录的逻辑结构和log文件物理分块结构之间的关系具体而言主要有以下四种类型FULL/FIRST/MIDDLE/LAST。

        如果记录类型是FULL,代表了当前记录内容完整地存储在一个物理Block没有被不同的物理Block切割开如果记录被相邻的物理Block切割开则类型会是其他三种类型中的一种我们以图3.1所示的例子来具体说明

       假设目前存在三条记录Record A,Record BRecord C,其中Record A大小为10K,Record B 大小为80K,Record C大小为12K,那么其在log文件中的逻辑布局会如图3.1所示Record A是图中蓝色区域所示因为大小为10K<32K,能够放在一个物理Block所以其类型为FULL;Record B 大小为80K,Block 1因为放入了Record A,所以还剩下22K,不足以放下Record B,所以在Block 1的剩余部分放入Record B的开头一部分类型标识为FIRST,代表了是一个记录的起始部分Record B还有58K没有存储这些只能依次放在后续的物理Block里面因为Block 2大小只有32K,仍然放不下Record B的剩余部分所以Block 2全部用来放Record B,且标识类型为MIDDLE,意思是这是Record B中间一段数据Record B剩下的部分可以完全放在Block 3类型标识为LAST,代表了这是Record B的末尾数据图中黄色的Record C因为大小为12K,Block 3剩下的空间足以全部放下它所以其类型标识为FULL。

     从这个小例子可以看出逻辑记录和物理Block之间的关系LevelDb一次物理读取为一个Block,然后根据类型情况拼接出逻辑记录供后续流程处理

LevelDb日知录之四:SSTable文件

   SSTableBigtable中至关重要的一块对于LevelDb来说也是如此LevelDbSSTable实现细节的了解也有助于了解Bigtable中一些实现细节

本节内容主要讲述SSTable的静态布局结构我们曾在LevelDb日知录之二整体架构中说过SSTable文件形成了不同Level的层级结构至于这个层级结构是如何形成的我们放在后面Compaction一节细说本节主要介绍SSTable某个文件的物理布局和逻辑布局结构这对了解LevelDb的运行过程很有帮助

  LevelDb不同层级有很多SSTable文件以后缀.sst为特征),所有.sst文件内部布局都是一样的上节介绍Log文件是物理分块的SSTable也一样会将文件划分为固定大小的物理存储块但是两者逻辑布局大不相同根本原因是Log文件中的记录是Key无序的即先后记录的key大小没有明确大小关系.sst文件内部则是根据记录的Key由小到大排列的从下面介绍的SSTable布局可以体会到Key有序是为何如此设计.sst文件结构的关键

4.1 .sst文件的分块结构

  4.1展示了一个.sst文件的物理划分结构Log文件一样也是划分为固定大小的存储块每个Block分为三个部分红色部分是数据存储区蓝色的Type区用于标识数据存储区是否采用了数据压缩算法Snappy压缩或者无压缩两种),CRC部分则是数据校验码用于判别数据是否在生成和传输中出错

  以上是.sst的物理布局下面介绍.sst文件的逻辑布局所谓逻辑布局就是说尽管大家都是物理块但是每一块存储什么内容内部又有什么结构等4.2展示了.sst文件的内部逻辑解释

4.2 逻辑布局

  从图4.2可以看出从大的方面可以将.sst文件划分为数据存储区和数据管理区数据存储区存放实际的Key:Value数据数据管理区则提供一些索引指针等管理数据目的是更快速便捷的查找相应的记录两个区域都是在上述的分块基础上的就是说文件的前面若干块实际存储KV数据后面数据管理区存储管理数据管理数据又分为四种不同类型紫色的Meta Block,红色的MetaBlock 索引和蓝色的数据索引块以及一个文件尾部块

  LevelDb 1.2版对于Meta Block尚无实际使用只是保留了一个接口估计会在后续版本中加入内容下面我们看看数据索引区和文件尾部Footer的内部结构

4.3 数据索引

  图4.3是数据索引的内部结构示意图再次强调一下Data Block内的KV记录是按照Key由小到大排列的数据索引区的每条记录是对某个Data Block建立的索引信息每条索引信息包含三个内容以图4.3所示的数据块i的索引Index i来说红色部分的第一个字段记载大于等于数据块i中最大的Key值的那个Key,第二个字段指出数据块i.sst文件中的起始位置第三个字段指出Data Block i的大小有时候是有数据压缩的)。后面两个字段好理解是用于定位数据块在文件中的位置的第一个字段需要详细解释一下在索引里保存的这个Key值未必一定是某条记录的Key,以图4.3的例子来说假设数据块i 的最小Key=“samecity”,最大Key=“the best”;数据块i+1的最小Key=“the fox”,最大Key=“zoo”,那么对于数据块i的索引Index i来说其第一个字段记载大于等于数据块i的最大Key(“the best”)同时要小于数据块i+1的最小Key(“the fox”),所以例子中Index i的第一个字段是:“the c”,这个是满足要求的Index i+1的第一个字段则是zoo”,即数据块i+1的最大Key。

  文件末尾Footer块的内部结构见图4.4,metaindex_handle指出了metaindex block的起始位置和大小inex_handle指出了index Block的起始地址和大小这两个字段可以理解为索引的索引是为了正确读出索引值而设立的后面跟着一个填充区和魔数

4.4 Footer

  上面主要介绍的是数据管理区的内部结构下面我们看看数据区的一个Block的数据部分内部是如何布局的4.1中的红色部分),4.5是其内部布局示意图

4.5 数据Block内部结构

  从图中可以看出其内部也分为两个部分前面是一个个KV记录其顺序是根据Key值由小到大排列的Block尾部则是一些重启点”(Restart Point),其实是一些指针指出Block内容中的一些记录位置

  “重启点是干什么的呢我们一再强调Block内容里的KV记录是按照Key大小有序的这样的话相邻的两条记录很可能Key部分存在重叠比如key i=“the Car”,Key i+1=“the color”,那么两者存在重叠部分the c”,为了减少Key的存储量Key i+1可以只存储和上一条Key不同的部分olor”,两者的共同部分从Key i中可以获得记录的KeyBlock内容部分就是这么存储的主要目的是减少存储开销。“重启点的意思是在这条记录开始不再采取只记载不同的Key部分而是重新记录所有的Key假设Key i+1是一个重启点那么Key里面会完整存储the color”,而不是采用简略的olor”方式Block尾部就是指出哪些记录是这些重启点的

4.6 记录格式

  在Block内容区每个KV记录的内部结构是怎样的4.6给出了其详细结构每个记录包含5个字段key共享长度比如上面的olor”记录key和上一条记录共享的Key部分长度是the c”的长度5;key非共享长度对于olor”来说4;value长度指出Key:ValueValue的长度在后面的Value内容字段中存储实际的Valuekey非共享内容则实际存储olor”这个Key字符串

  上面讲的这些就是.sst文件的全部内部奥秘

LevelDb日知录之五:MemTable详解

   LevelDb日知录前述小节大致讲述了磁盘文件相关的重要静态结构本小节讲述内存中的数据结构Memtable,Memtable在整个体系中的重要地位也不言而喻总体而言所有KV数据都是存储在Memtable,Immutable MemtableSSTable中的Immutable Memtable从结构上讲和Memtable是完全一样的区别仅仅在于其是只读的不允许写入操作Memtable则是允许写入和读取的Memtable写入的数据占用内存到达指定数量则自动转换为Immutable Memtable,等待Dump到磁盘中系统会自动生成新的Memtable供写操作写入新数据理解了Memtable,那么Immutable Memtable自然不在话下

   LevelDbMemTable提供了将KV数据写入删除以及读取KV记录的操作接口但是事实上Memtable并不存在真正的删除操作,删除某个KeyValueMemtable内是作为插入一条记录实施的但是会打上一个Key的删除标记真正的删除操作是Lazy会在以后的Compaction过程中去掉这个KV。

   需要注意的是LevelDbMemtableKV对是根据Key大小有序存储的在系统插入新的KVLevelDb要把这个KV插到合适的位置上以保持这种Key有序性其实LevelDbMemtable类只是一个接口类真正的操作是通过背后的SkipList来做的包括插入操作和读取操作等所以Memtable的核心数据结构是一个SkipList。

   SkipList是由William Pugh发明他在Communications of the ACM June 1990, 33(6) 668-676 发表了Skip lists: a probabilistic alternative to balanced trees,在该论文中详细解释了SkipList的数据结构和插入删除操作

SkipList是平衡树的一种替代数据结构但是和红黑树不相同的是SkipList对于树的平衡的实现是基于一种随机化的算法的这样也就是说SkipList的插入和删除的工作是比较简单的

关于SkipList的详细介绍可以参考这篇文章http://www.cnblogs.com/xuqiang/archive/2011/05/22/2053516.html讲述的很清楚LevelDbSkipList基本上是一个具体实现并无特殊之处

  SkipList不仅是维护有序数据的一个简单实现而且相比较平衡树来说在插入数据的时候可以避免频繁的树节点调整操作所以写入效率是很高的LevelDb整体而言是个高写入系统SkipList在其中应该也起到了很重要的作用Redis为了加快插入操作也使用了SkipList来作为内部实现数据结构

LevelDb日知录之六 写入与删除记录

      在之前的五节LevelDb日知录中我们介绍了LevelDb的一些静态文件及其详细布局从本节开始我们看看LevelDb的一些动态操作比如读写记录Compaction,错误恢复等操作

       本节介绍levelDb的记录更新操作即插入一条KV记录或者删除一条KV记录levelDb的更新操作速度是非常快的源于其内部机制决定了这种更新操作的简单性 

6.1 LevelDb写入记录

        6.1levelDb如何更新KV数据的示意图从图中可以看出对于一个插入操作Put(Key,Value)来说完成插入操作包含两个具体步骤首先是将这条KV记录以顺序写的方式追加到之前介绍过的log文件末尾因为尽管这是一个磁盘读写操作但是文件的顺序追加写入效率是很高的所以并不会导致写入速度的降低第二个步骤是:如果写入log文件成功那么将这条KV记录插入内存中的Memtable前面介绍过Memtable只是一层封装其内部其实是一个Key有序的SkipList列表插入一条新记录的过程也很简单即先查找合适的插入位置然后修改相应的链接指针将新记录插入即可完成这一步写入记录就算完成了所以一个插入记录操作涉及一次磁盘文件追加写和内存SkipList插入操作这是为何levelDb写入速度如此高效的根本原因

        从上面的介绍过程中也可以看出log文件内是key无序的Memtable中是key有序的那么如果是删除一条KV记录呢对于levelDb来说并不存在立即删除的操作而是与插入操作相同的区别是插入操作插入的是Key:Value 而删除操作插入的是Key:删除标记”,并不真正去删除记录而是后台Compaction的时候才去做真正的删除操作

        levelDb的写入操作就是如此简单真正的麻烦在后面将要介绍的读取操作中

LevelDb日知录之七:读取记录

   LevelDb是针对大规模Key/Value数据的单机存储库从应用的角度来看LevelDb就是一个存储工具而作为称职的存储工具常见的调用接口无非是新增KV,删除KV,读取KV,更新Key对应的Value值这么几种操作LevelDb的接口没有直接支持更新操作的接口如果需要更新某个KeyValue,你可以选择直接生猛地插入新的KV,保持Key相同这样系统内的key对应的value就会被更新或者你可以先删除旧的KV, 之后再插入新的KV,这样比较委婉地完成KV的更新操作

     假设应用提交一个Key下面我们看看LevelDb是如何从存储的数据中读出其对应的Value值的7-1LevelDb读取过程的整体示意图

7-1  LevelDb读取记录流程

  LevelDb首先会去查看内存中的Memtable,如果Memtable中包含key及其对应的value,则返回value值即可如果在Memtable没有读到key,则接下来到同样处于内存中的Immutable Memtable中去读取类似地如果读到就返回若是没有读到,那么只能万般无奈下从磁盘中的大量SSTable文件中查找因为SSTable数量较多而且分成多个Level,所以在SSTable中读数据是相当蜿蜒曲折的一段旅程总的读取原则是这样的首先从属于level 0的文件中查找如果找到则返回对应的value如果没有找到那么到level 1中的文件中去找如此循环往复直到在某层SSTable文件中找到这个key对应的value为止或者查到最高level,查找失败说明整个系统中不存在这个Key)。

   那么为什么是从MemtableImmutable Memtable,再从Immutable Memtable到文件而文件中为何是从低level到高level这么一个查询路径呢道理何在之所以选择这么个查询路径是因为从信息的更新时间来说很明显Memtable存储的是最新鲜的KVImmutable Memtable中存储的KV数据对的新鲜程度次之而所有SSTable文件中的KV数据新鲜程度一定不如内存中的MemtableImmutable Memtable对于SSTable文件来说如果同时在level LLevel L+1找到同一个key,level L的信息一定比level L+1的要新也就是说上面列出的查找路径就是按照数据新鲜程度排列出来的越新鲜的越先查找

   为啥要优先查找新鲜的数据呢这个道理不言而喻举个例子比如我们先往levelDb里面插入一条数据 {key="www.samecity.com"  value="我们"},过了几天samecity网站改名为:69同城此时我们插入数据{key="www.samecity.com"  value="69同城"},同样的key,不同的value;逻辑上理解好像levelDb中只有一个存储记录即第二个记录但是在levelDb中很可能存在两条记录即上面的两个记录都在levelDb中存储了此时如果用户查询key="www.samecity.com",我们当然希望找到最新的更新记录也就是第二个记录返回这就是为何要优先查找新鲜数据的原因

  前文有讲对于SSTable文件来说如果同时在level LLevel L+1找到同一个key,level L的信息一定比level L+1的要新这是一个结论理论上需要一个证明过程否则会招致如下的问题为神马呢从道理上讲呢很明白因为Level L+1的数据不是从石头缝里蹦出来的也不是做梦梦到的那它是从哪里来的Level L+1的数据是从Level L 经过Compaction后得到的如果您不知道什么是Compaction,那么........也许以后会知道的),也就是说您看到的现在的Level L+1层的SSTable数据是从原来的Level L中来的现在的Level L比原来的Level L数据要新鲜所以可证现在的Level L比现在的Level L+1的数据要新鲜

  SSTable文件很多如何快速地找到key对应的valueLevelDblevel 0一直都爱搞特殊化level 0和其它level中查找某个key的过程是不一样的因为level 0下的不同文件可能key的范围有重叠某个要查询的key有可能多个文件都包含这样的话LevelDb的策略是先找出level 0中哪些文件包含这个key(manifest文件中记载了level和对应的文件及文件里key的范围信息LevelDb在内存中保留这种映射表), 之后按照文件的新鲜程度排序新的文件排在前面之后依次查找读出key对应的value。而如果是非level 0的话因为这个level的文件之间key是不重叠的所以只从一个文件就可以找到key对应的value。

  最后一个问题,如果给定一个要查询的key和某个key range包含这个keySSTable文件那么levelDb是如何进行具体查找过程的呢levelDb一般会先在内存中的Cache中查找是否包含这个文件的缓存记录如果包含则从缓存中读取如果不包含则打开SSTable文件同时将这个文件的索引部分加载到内存中并放入Cache这样Cache里面就有了这个SSTable的缓存项但是只有索引部分在内存中之后levelDb根据索引可以定位到哪个内容Block会包含这条key,从文件中读出这个Block的内容在根据记录一一比较如果找到则返回结果如果没有找到那么说明这个levelSSTable文件并不包含这个key,所以到下一级别的SSTable中去查找

  从之前介绍的LevelDb的写操作和这里介绍的读操作可以看出相对写操作读操作处理起来要复杂很多所以写的速度必然要远远高于读数据的速度也就是说LevelDb比较适合写操作多于读操作的应用场合而如果应用是很多读操作类型的那么顺序读取效率会比较高因为这样大部分内容都会在缓存中找到尽可能避免大量的随机读取操作

LevelDb日知录之八:Compaction操作

     前文有述对于LevelDb来说写入记录操作很简单删除记录仅仅写入一个删除标记就算完事但是读取记录比较复杂需要在内存以及各个层级文件中依照新鲜程度依次查找代价很高为了加快读取速度levelDb采取了compaction的方式来对已有的记录进行整理压缩通过这种方式来删除掉一些不再有效的KV数据减小数据规模减少文件数量等

     levelDbcompaction机制和过程与Bigtable所讲述的是基本一致的Bigtable中讲到三种类型的compaction: minor ,majorfull。所谓minor Compaction,就是把memtable中的数据导出到SSTable文件中major compaction就是合并不同层级的SSTable文件full compaction就是将所有SSTable进行合并

     LevelDb包含其中两种minormajor。

    我们将为大家详细叙述其机理

    先来看看minor Compaction的过程Minor compaction 的目的是当内存中的memtable大小到了一定值时将内容保存到磁盘文件中8.1是其机理示意图 

8.1 minor compaction

     8.1可以看出memtable数量到了一定程度会转换为immutable memtable,此时不能往其中写入记录只能从中读取KV内容之前介绍过immutable memtable其实是一个多层级队列SkipList,其中的记录是根据key有序排列的所以这个minor compaction实现起来也很简单就是按照immutable memtable中记录由小到大遍历并依次写入一个level 0 的新建SSTable文件中写完后建立文件的index 数据这样就完成了一次minor compaction。从图中也可以看出对于被删除的记录minor compaction过程中并不真正删除这个记录原因也很简单这里只知道要删掉key记录但是这个KV数据在哪里?那需要复杂的查找所以在minor compaction的时候并不做删除只是将这个key作为一个记录写入文件中至于真正的删除操作在以后更高层级的compaction中会去做

     当某个level下的SSTable文件数目超过一定设置值后levelDb会从这个levelSSTable中选择一个文件level>0),将其和高一层级的level+1SSTable文件合并这就是major compaction。

    我们知道在大于0的层级中每个SSTable文件内的Key都是由小到大有序存储的而且不同文件之间的key范围文件内最小key和最大key之间不会有任何重叠Level 0SSTable文件有些特殊尽管每个文件也是根据Key由小到大排列但是因为level 0的文件是通过minor compaction直接生成的所以任意两个level 0下的两个sstable文件可能再key范围上有重叠所以在做major compaction的时候对于大于level 0的层级选择其中一个文件就行但是对于level 0来说指定某个文件后level中很可能有其他SSTable文件的key范围和这个文件有重叠这种情况下要找出所有有重叠的文件和level 1的文件进行合并level 0在进行文件选择的时候可能会有多个文件参与major compaction。

  levelDb在选定某个level进行compaction还要选择是具体哪个文件要进行compaction,levelDb在这里有个小技巧就是说轮流来比如这次是文件A进行compaction,那么下次就是在key range上紧挨着文件A的文件B进行compaction,这样每个文件都会有机会轮流和高层的level 文件进行合并

如果选好了level L的文件Alevel L+1层的文件进行合并那么问题又来了应该选择level L+1哪些文件进行合并levelDb选择L+1层中和文件Akey range上有重叠的所有文件来和文件A进行合并

   也就是说选定了level L的文件A,之后在level L+1中找到了所有需要合并的文件B,C,D…..等等剩下的问题就是具体是如何进行major 合并的就是说给定了一系列文件每个文件内部是key有序的如何对这些文件进行合并使得新生成的文件仍然Key有序同时抛掉哪些不再有价值的KV 数据

    8.2说明了这一过程

8.2 SSTable Compaction

  Major compaction的过程如下对多个文件采用多路归并排序的方式依次找出其中最小的Key记录也就是对多个文件中的所有记录重新进行排序之后采取一定的标准判断这个Key是否还需要保存如果判断没有保存价值那么直接抛掉如果觉得还需要继续保存那么就将其写入level L+1层中新生成的一个SSTable文件中就这样对KV数据一一处理形成了一系列新的L+1层数据文件之前的L层文件和L+1层参与compaction 的文件数据此时已经没有意义了所以全部删除这样就完成了L层和L+1层文件记录的合并过程

  那么在major compaction过程中判断一个KV记录是否抛弃的标准是什么呢其中一个标准是:对于某个key来说如果在小于L层中存在这个Key,那么这个KVmajor compaction过程中可以抛掉因为我们前面分析过对于层级低于L的文件中如果存在同一Key的记录那么说明对于Key来说有更新鲜的Value存在那么过去的Value就等于没有意义了所以可以删除

 

LevelDb日知录之九 levelDb中的Cache

  书接前文前面讲过对于levelDb来说读取操作如果没有在内存的memtable中找到记录要多次进行磁盘访问操作假设最优情况即第一次就在level 0中最新的文件中找到了这个key,那么也需要读取2次磁盘一次是将SSTable的文件中的index部分读入内存这样根据这个index可以确定key是在哪个block中存储第二次是读入这个block的内容然后在内存中查找key对应的value。

  levelDb中引入了两个不同的Cache:Table CacheBlock Cache。其中Block Cache是配置可选的即在配置文件中指定是否打开这个功能

9.1 table cache

   9.1table cache的结构Cachekey值是SSTable的文件名称Value部分包含两部分一个是指向磁盘打开的SSTable文件的文件指针这是为了方便读取内容另外一个是指向内存中这个SSTable文件对应的Table结构指针table结构在内存中保存了SSTableindex内容以及用来指示block cache用的cache_id ,当然除此外还有其它一些内容

  比如在get(key)读取操作中如果levelDb确定了key在某个level下某个文件Akey range范围内那么需要判断是不是文件A真的包含这个KV。此时levelDb会首先查找Table Cache,看这个文件是否在缓存里如果找到了那么根据index部分就可以查找是哪个block包含这个key。如果没有在缓存中找到文件那么打开SSTable文件将其index部分读入内存然后插入Cache里面index里面定位哪个block包含这个Key 。如果确定了文件哪个block包含这个key,那么需要读入block内容这是第二次读取

9.2 block cache

   Block Cache是为了加快这个过程的9.2是其结构示意图其中的key是文件的cache_id加上这个block在文件中的起始位置block_offset。value则是这个Block的内容

  如果levelDb发现这个blockblock cache那么可以避免读取数据直接在cache里的block内容里面查找keyvalue就行如果没找到呢那么读入block内容并把它插入block cachelevelDb就是这样通过两个cache来加快读取速度的从这里可以看出如果读取的数据局部性比较好也就是说要读的数据大部分在cache里面都能读到那么读取效率应该还是很高的而如果是对key进行顺序读取效率也应该不错因为一次读入后可以多次被复用但是如果是随机读取您可以推断下其效率如何

LevelDb日知录之十 Version、VersionEdit、VersionSet

  Version 保存了当前磁盘以及内存中所有的文件信息一般只有一个Version叫做"current" version(当前版本)。Leveldb还保存了一系列的历史版本这些历史版本有什么作用呢

当一个Iterator创建后Iterator就引用到了current version(当前版本),只要这个Iterator不被delete那么被Iterator引用的版本就会一直存活这就意味着当你用完一个Iterator需要及时删除它

  当一次Compaction结束后会生成新的文件合并前的文件需要删除),Leveldb会创建一个新的版本作为当前版本原先的当前版本就会变为历史版本

  VersionSet 是所有Version的集合管理着所有存活的Version。

  VersionEdit 表示Version之间的变化相当于delta 增量表示有增加了多少文件删除了文件下图表示他们之间的关系

Version0 +VersionEdit-->Version1

  VersionEdit会保存到MANIFEST文件中当做数据恢复时就会从MANIFEST文件中读出来重建数据

  leveldb的这种版本的控制让我想到了双buffer切换buffer切换来自于图形学中用于解决屏幕绘制时的闪屏问题在服务器编程中也有用处

  比如我们的服务器上有一个字典库每天我们需要更新这个字典库我们可以新开一个buffer,将新的字典库加载到这个新buffer等到加载完毕将字典的指针指向新的字典库

leveldbversion管理和双buffer切换类似但是如果原version被某个iterator引用那么这个version会一直保持直到没有被任何一个iterator引用此时就可以删除这个version

注:博文参考了郎格科技博客:http://www.samecity.com/blog/Index.asp?SortID=12