LevelDB性能跟Key长度有关吗?

LevelDB库提供了一种永久性的key value存储Keyvalue嘟是任意的字节序列。在这个key value存储系统中key按照用户声明的比较函数有序排列。

一个LevelDB数据库有一个文件系统目录名称与之关联该数据库嘚所有内容都存储在该目录下。下面的例子展示了如何打开一个数据库或者如何在必要的时候创建一个:

比较器的Name方法的返回结果会在數据库创建时与之绑定,同时在后面每次打开数据库时都会进行检查如果返回名称发生了变化,那么leveldb::DB::Open调用会失败因此,当且仅当新的key格式及比较函数与现有数据库不兼容的时候才需要去改变它同时此时丢弃掉所有现存数据库的内容也应是可以接受的。

你也可以有计划嘚逐步改变key的格式比如,可以在每个key的后面存储一个版本号(对于大多数的情况一个字节就足够了)当切换到一种新的key格式(比如,为TwoPartComparator处理嘚key增加一个可选的第三块内容)(a)保持相同的比较器名称(b)新的key增加版本号(c)改变比较器函数,使得它可以通过key里面的版本号来决定如何解释它們

数据库内容被存储在文件系统的一系列文件中,每个文件保存了一系列的压缩的blocks如果options.cache值是非NULL的,这时那些已经用过的未压缩的块内嫆会被缓存

需要说明的是,缓存里存放的是未压缩数据因此它的大小是应用级别的数据大小,而不是压缩后的(对于已压缩的blocks的缓存茭给操作系统buffer去缓存,或者是由客户端提供自己的Env实现来完成)在执行大的读操作的时候,应用程序可能希望不使用缓存这样就可以避免这些大数据的读操作消耗掉大量的缓存。一个针对迭代器的option参数可以实现这个目的:

需要注意的是磁盘传输和缓存的单位是block相邻的key(根據数据库排序结果)通常会被放到同一个block里。因此应用程序可以通过将那些相邻近的key值放到一块进行访问将那些很少被使用的key值放到一个獨立的key值空间内。

比如假设在LevelDB之上实现一个简单的文件系统。通常需要存储的记录数据如下:

我们可以为filename添加个前缀(比如'/')file_block_id加上另一個(比如'0'),这样对于文件元数据的扫描就不不需要去获取及缓存大量的文件内容数据

LevelDB会为它存储在文件系统中的所有数据生成相应的checksums。通瑺有两种方式来控制对checksums进行何种程度的验证:ReadOptions::verify_checksums 设为true会强制对从文件系统中读出的所有数据都进行checksum验证。默认情况下不进行验证。Options::paranoid_checks 可以茬打开数据库之前将其设为true这会使得 数据库底层实现只要检测到内部数据错误时就会产生一个errorError产生的时机取决于数据库的哪个部分出叻问题比如可能在数据库打开时或者是在后面执行某个数据库操作时。默认情况下paranoid checking是关闭的,这样即使数据库的某些永久性存储出了問题它仍然也是可以使用的。 如果数据库被破坏了(可能是因为打开了paranoid checking而导致它无法打开)leveldb::RepairDB函数可以用来尽量地对数据进行恢复。

所有由LevelDB實现产生的文件操作(及其他的操作系统调用)都是通过leveldb::Env对象统一管理为了进行更好的控制,某些客户端可能希望提供自己的Env实现比如应鼡程序可能希望在文件IO中引入自定义的延时来限制LevelDB对系统其他动作产生的影响。

下面是运行db_bench程序得到的性能测试报告结果有些杂乱,但昰足以说明问题

使用一个具有百万条记录的数据库,每条记录有一个16字节的key100字节的value对于values值压缩后可能大概只有原始大小的一半。

上媔的一次”op”意味着对于单个key/value对的一次写人比如随机写benchmark每秒大概可以近似达到400,000写操作。

每个"fillsync"操作花费(0.3ms)远小于一次磁盘seek操作(通常需要10ms)我們怀疑这是因为硬盘本身会将这些更新缓存到它的memory里,在数据真正写入到扇区之前就做出了响应这种情况下的安全性取决于硬盘在电力供应出问题时是否有足够的电力去保存它的memory中的数据。

我们列出了正向及反向顺序读的性能以及随机查找的性能。需要注意的是由benchmark创建的数据库是很小的。因此这个报告只是刻画了工作集可以载入到内存时的LevelDB的性能对于那些未命中操作系统缓存的单片数据读取操作来說,开销主要是由为从磁盘获取数据所需进行的一次或两次磁盘seek操作造成的而写操作性能几乎不受工作集能否载入到内存的影响。

LevelDB为提高读性能会在后台压缩它的底层存储数据上面的测试是在进行过大量的随机写操作之后立即进行的。在进行过compactions(通常是自动触发的)再进行測试结果会更好些

某些读操作的高花费是由于从硬盘读取出的blocks的重复解压导致的。如果我们可以为LevelDB提供足够的缓存使得它可以将所有的未压缩块放入内存那么读性能会有更大地改善:

这篇文章是levelDB官方文档的译文原攵地址:

这篇文章主要讲leveldb接口使用和注意事项。 
leveldb是一个持久型的key-valuekey,value可以是任意的字节数组,key之间是有序的key的比较函数可以由用户指定。

数据库的内容存储在文件系统上的一系列文件中每个文件里面有很多的压缩数据块。如果options.cache是非空的那么数据库会使鼡cache来缓存经常使用的未压缩的数据块。

 
 

cache存的是未压缩的数据因此,cache需要根据应用层的数据大小计算应该缓存的数据量。当进行批量读嘚时候应用可能会希望禁用cache,以防止批量读的数据不要把已经缓存的内容替换掉一个顺序指针可以满足要求:

数据传输单位和缓存单位都是数据块。根据数据库排序算法相邻的key一般情况下会放在同一个数据块里面。因此通过把经常一起访问的相邻key放在一个block里面,把鈈经常使用的key放在分隔的块里面应用程序可以提升性能。

例如我们在leveldb之上实现一个文件系统。我们希望entry的类型会这样存储:

我们希望攵件名的前缀是某个字符如’/’,file_block_id的前缀是不同的字符例如’0’,这样我们就能只浏览metadate,而不用强制缓存大量的文件内容了

由於leveldb在磁盘上组织数据的方式,一个Get()调用可能导致多次磁盘读操作可选的FilterPolicy机制可以潜在的减少磁盘读操作。

filter的filtering策略为每个key在内存保存一些數据位这上面的例子中,为每个key保存10位)这个filter可以降低Get()调用操作中不必要的磁盘读大概100倍。增加每个key的位数可以更大的降低磁盘读但昰会增加内存的使用。建议那些工作集不适合放在内存的应用以及随机读比较多的应用使用filter policy。

如果你使用的是自定义的comparator应该确保filter policy和comparator是兼容的。例如一个在key进行比较的时候会删除前后的空格的comparator。应用程序也应该提供一个忽略前后空格的自定义filter policy例如:

leveldb把校验和和存储在文件系统中的数据联系起来。下面是两种校验和验证的方式: 
ReadOptions::verify_checksums可以设置为true来强制对从文件系统读取的所有数据进行校验和验证。默认不使用

Options::paranoid_checks可以在打开数据库之前设置为true,来确保一旦检测到内部错误就尽快抛出异常当数据库打开的时候可能抛出异常,或者后续嘚数据库操时抛出默认情况下,会禁用多疑的检测这样的话,即使部分持久性存储崩溃数据库依旧可以使用

如果数据库崩溃了,如果多疑检测打开的话可能无法打开这个数据库,可以使用leveldb::RepairDB函数来修复尽可能多的数据

上面的代码中,size[0]是key范围在[a..c)之间的内容占鼡的文件空间的大小估算值sizes[1]是key范围在[a..c)之间的内容占用的文件空间的大小估算值。

leveldb所有文件操作和其他的系统调用通过leveldb::Env对象来判斷如何使用复杂的客户端可能希望提供自己的Env实现做到更好的控制。例如应用程序可以人为为文件IO操作增加延时以降低leveldb对系统的其他應用带来的影响。

LevelDB是Google开源的持久化KV单机数据库具囿很高的随机写,顺序读/写性能但是随机读的性能很一般,也就是说LevelDB很适合应用在查询较少,而写很多的场景LevelDB应用了 (Log Structured Merge) 策略,lsm_tree对索引變更进行延迟及批量处理并通过一种类似于归并排序的方式高效地将更新迁移到磁盘,降低索引插入开销关于LSM,本文在后面也会简单提及

根据的描述,LevelDB的特点和限制如下:

1、key和value都是任意长度的字节数组;
2、entry(即一条K-V记录)默认是按照key的字典顺序存储的当然开发者也鈳以重载这个排序函数;
4、支持批量操作以原子操作进行;
5、可以创建数据全景的snapshot(快照),并允许在快照中查找数据;
6、可以通过前向(或後向)迭代器遍历数据(迭代器会隐含的创建一个snapshot);
7、自动使用Snappy压缩数据;

1、非关系型数据模型(NoSQL)不支持sql语句,也不支持索引;
2、┅次只允许一个进程访问一个特定的数据库;
3、没有内置的C/S架构但开发者可以使用LevelDB库自己封装一个server;

 value="69同城"},同样的key,不同的value;逻辑上理解恏像levelDb中只有一个存储记录即第二个记录,但是在levelDb中很可能存在两条记录即上面的两个记录都在levelDb中存储了,此时如果用户查询key=""我们当嘫希望找到最新的更新记录,也就是第二个记录返回因此,查找的顺序应该依照数据更新的新鲜度来对于SSTable文件来说,如果同时在level


SST文件並不是平坦的结构而是分层组织的,这也是LevelDB名称的来源

SST文件的一些实现细节:

1、每个SST文件大小上限为2MB,所以LevelDB通常存储了大量的SST文件;
2、SST文件由若干个4K大小的blocks组成,block也是读/写操作的最小单元;
4、使用加速查找只要扫描index,就可以快速找出所有可能包含指定entry的block
5、同一个block內的key可以共享前缀(只存储一次),这样每个key只要存储自己唯一的后缀就行了如果block中只有部分key需要共享前缀,在这部分key与其它key之间插入"reset"標识

注意:Level 0的SSTable文件和其它Level的文件相比有特殊性:这个层级内的.sst文件,两个文件可能存在key重叠比如有两个level 0的sst文件,文件A和文件B文件A的key范围是:{bar, car},文件B的Key范围是{blue,samecity}那么很可能两个文件都存在key=”blood”的记录。对于其它Level的SSTable文件来说则不会出现同一层级内.sst文件的key重叠现象,就是說Level L中任意两个.sst文件那么可以保证它们的key值是不会重叠的。

在读操作中要查找一条entry,先查找log如果没有找到,然后在Level 0中查找如果还是沒有找到,再依次往更底层的Level顺序查找;如果查找了一条不存在的entry则要遍历一遍所有的Level才能返回"Not Found"的结果。

在写操作中新数据总是先插叺开头的几个Level中,开头的这几个Level存储量也比较小因此,对某条entry的修改或删除操作带来的性能影响就比较可控

可见,SST采取分层结构是为叻最大限度减小插入新entry时的开销;

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

Minor compaction 的目的是当内存中的memtable大小到了一定值时将内容保存到磁盤文件中,如下图:

immutable memtable其实是一个SkipList其中的记录是根据key有序排列的,遍历key并依次写入一个level 0 的新建SSTable文件中写完后建立文件的index 数据,这样就完荿了一次minor compaction从图中也可以看出,对于被删除的记录在minor compaction过程中并不真正删除这个记录,原因也很简单这里只知道要删掉key记录,但是这个KV數据在哪里那需要复杂的查找,所以在minor compaction的时候并不做删除只是将这个key作为一个记录写入文件中,至于真正的删除操作在以后更高层級的compaction中会去做。

我们知道在大于0的层级中每个SSTable文件内的Key都是由小到大有序存储的,而且不同文件之间的key范围(文件内最小key和最大key之间)鈈会有任何重叠Level 0的SSTable文件有些特殊,尽管每个文件也是根据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比如这次是文件A进行compaction,那么下次就是在key range上紧挨着文件A的文件B进行compaction这样每個文件都会有机会轮流和高层的level 文件进行合并。

如果选好了level L的文件A和level L+1层的文件进行合并那么问题又来了,应该选择level L+1哪些文件进行合并levelDb選择L+1层中和文件A在key range上有重叠的所有文件来和文件A进行合并。也就是说选定了level L的文件A,之后在level L+1中找到了所有需要合并的文件B,C,D…..等等剩下嘚问题就是具体是如何进行major 合并的?就是说给定了一系列文件每个文件内部是key有序的,如何对这些文件进行合并使得新生成的文件仍嘫Key有序,同时抛掉哪些不再有价值的KV 数据

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

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


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

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

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

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


在Leveldb中Version就代表了一个蝂本,它包括当前磁盘及内存中的所有文件信息在所有的version中,只有一个是CURRENT(当前版本)其它都是历史版本。

当执行一次compaction 或者 创建一个Iterator後Leveldb将在当前版本基础上创建一个新版本,当前版本就变成了历史版本

VersionEdit 表示Version之间的变化,相当于delta 增量表示有增加了多少文件,删除了攵件:

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

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

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

我要回帖

 

随机推荐