levelDB SSTable-1

创建sstable文件

了解了sstable文件的存储格式,以及Data Block的组织,下面就可以分析如何创建sstable文件了。相关代码在table_builder.h/.cc以及block_builder.h/.cc(构建Block)中。

6.4.1 TableBuilder类

构建sstable文件的类是TableBuilder,该类提供了几个有限的方法可以用来添加k/v对,Flush到文件中等等,它依赖于BlockBuilder来构建Block。

TableBuilder的几个接口说明下:

> void Add(const Slice& key, const Slice& value),向当前正在构建的表添加新的{key, value}对,要求根据Option指定的Comparator,key必须位于所有前面添加的key之后;

> void Flush(),将当前缓存的k/v全部flush到文件中,一个高级方法,大部分的client不需要直接调用该方法;

> void Finish(),结束表的构建,该方法被调用后,将不再会使用传入的WritableFile;

> void Abandon(),结束表的构建,并丢弃当前缓存的内容,该方法被调用后,将不再会使用传入的WritableFile;【只是设置closed为true,无其他操作】

一旦Finish()/Abandon()方法被调用,将不能再次执行Flush或者Add操作。

下面来看看涉及到的类,如图6.3-1所示。

图6.3-1

其中WritableFile和op log一样,使用的都是内存映射文件。Options是一些调用者可设置的选项。

TableBuilder只有一个成员变量Rep* rep_,实际上Rep结构体的成员就是TableBuilder所有的成员变量;这样做的目的,可能是为了隐藏其内部细节。Rep的定义也是在.cc文件中,对外是透明的。

简单解释下成员的含义:

[cpp] view plaincopy

  1. Options options;   // data block的选项
  2. Options index_block_options; // index block的选项
  3. WritableFile* file;  // sstable文件
  4. uint64_t offset; // 要写入data block在sstable文件中的偏移,初始0
  5. Status status; //当前状态-初始ok
  6. BlockBuilder data_block; //当前操作的data block
  7. BlockBuilder index_block; // sstable的index block
  8. std::string last_key; //当前data block最后的k/v对的key
  9. int64_t num_entries; //当前data block的个数,初始0
  10. bool closed;          //调用了Finish() or Abandon(),初始false
  11. FilterBlockBuilder*filter_block; //根据filter数据快速定位key是否在block中
  12. bool pending_index_entry; //见下面的Add函数,初始false
  13. BlockHandle pending_handle; //添加到index block的data block的信息
  14. std::string compressed_output;//压缩后的data block,临时存储,写入后即被清空

Filter block是存储的过滤器信息,它会存储{key, 对应data block在sstable的偏移值},不一定是完全精确的,以快速定位给定key是否在data block中。

下面分析如何向sstable中添加k/v对,创建并持久化sstable。其它函数都比较简单,略过。另外对于Abandon,简单设置closed=true即返回。

6.4.2 添加k/v对

这是通过方法Add(constSlice& key, const Slice& value)完成的,没有返回值。下面分析下函数的逻辑:

S1 首先保证文件没有close,也就是没有调用过Finish/Abandon,以及保证当前status是ok的;如果当前有缓存的kv对,保证新加入的key是最大的。

[cpp] view plaincopy

  1. Rep* r = rep_;
  2. assert(!r->closed);
  3. if (!ok()) return;
  4. if (r->num_entries > 0) {
  5. assert(r->options.comparator->Compare(key, Slice(r->last_key))> 0);
  6. }

S2 如果标记r->pending_index_entry为true,表明遇到下一个data block的第一个k/v,根据key调整r->last_key,这是通过Comparator的FindShortestSeparator完成的。

[cpp] view plaincopy

  1. if (r->pending_index_entry) {
  2. assert(r->data_block.empty());
  3. r->options.comparator->FindShortestSeparator(&r->last_key,key);
  4. std::string handle_encoding;
  5. r->pending_handle.EncodeTo(&handle_encoding);
  6. r->index_block.Add(r->last_key, Slice(handle_encoding));
  7. r->pending_index_entry =false;
  8. }

接下来将pending_handle加入到index block中{r->last_key, r->pending_handle’sstring}。最后将r->pending_index_entry设置为false。

值得讲讲pending_index_entry这个标记的意义,见代码注释:

直到遇到下一个databock的第一个key时,我们才为上一个datablock生成index entry,这样的好处是:可以为index使用较短的key;比如上一个data block最后一个k/v的key是"the quick brown fox",其后继data block的第一个key是"the who",我们就可以用一个较短的字符串"the r"作为上一个data block的index block entry的key。

简而言之,就是在开始下一个datablock时,Leveldb才将上一个data block加入到index block中。标记pending_index_entry就是干这个用的,对应data block的index entry信息就保存在(BlockHandle)pending_handle。

S3 如果filter_block不为空,就把key加入到filter_block中。

[cpp] view plaincopy

  1. if (r->filter_block != NULL) {
  2. r->filter_block->AddKey(key);
  3. }

S4 设置r->last_key = key,将(key, value)添加到r->data_block中,并更新entry数。

[cpp] view plaincopy

  1. r->last_key.assign(key.data(), key.size());
  2. r->num_entries++;
  3. r->data_block.Add(key,value);

S5 如果data block的个数超过限制,就立刻Flush到文件中。

[cpp] view plaincopy

  1. const size_testimated_block_size = r->data_block.CurrentSizeEstimate();
  2. if (estimated_block_size >=r->options.block_size) Flush();

6.4.3 Flush文件

该函数逻辑比较简单,直接见代码如下:

[cpp] view plaincopy

  1. Rep* r = rep_;
  2. assert(!r->closed); // 首先保证未关闭,且状态ok
  3. if (!ok()) return;
  4. if (r->data_block.empty())return; // data block是空的
  5. // 保证pending_index_entry为false,即data block的Add已经完成
  6. assert(!r->pending_index_entry);
  7. // 写入data block,并设置其index entry信息—BlockHandle对象
  8. WriteBlock(&r->data_block, &r->pending_handle);
  9. //写入成功,则Flush文件,并设置r->pending_index_entry为true,
  10. //以根据下一个data block的first key调整index entry的key—即r->last_key
  11. if (ok()) {
  12. r->pending_index_entry =true;
  13. r->status =r->file->Flush();
  14. }
  15. if (r->filter_block != NULL){ //将data block在sstable中的便宜加入到filter block中
  16. r->filter_block->StartBlock(r->offset); // 并指明开始新的data block
  17. }

6.4.4 WriteBlock函数

在Flush文件时,会调用WriteBlock函数将data block写入到文件中,该函数同时还设置data block的index entry信息。原型为:

void WriteBlock(BlockBuilder* block, BlockHandle* handle)

该函数做些预处理工作,序列化要写入的data block,根据需要压缩数据,真正的写入逻辑是在WriteRawBlock函数中。下面分析该函数的处理逻辑。

S1 获得block的序列化数据Slice,根据配置参数决定是否压缩,以及根据压缩格式压缩数据内容。对于Snappy压缩,如果压缩率太低<12.5%,还是作为未压缩内容存储。

BlockBuilder的Finish()函数将data block的数据序列化成一个Slice。

[cpp] view plaincopy

  1. Rep* r = rep_;
  2. Slice raw = block->Finish(); // 获得data block的序列化字符串
  3. Slice block_contents;
  4. CompressionType type =r->options.compression;
  5. switch (type) {
  6. case kNoCompression: block_contents= raw; break; // 不压缩
  7. case kSnappyCompression: { // snappy压缩格式
  8. std::string* compressed =&r->compressed_output;
  9. if(port::Snappy_Compress(raw.data(), raw.size(), compressed) &&
  10. compressed->size()< raw.size() - (raw.size() / 8u)) {
  11. block_contents =*compressed;
  12. } else { // 如果不支持Snappy,或者压缩率低于12.5%,依然当作不压缩存储
  13. block_contents = raw;
  14. type = kNoCompression;
  15. }
  16. break;
  17. }
  18. }

S2 将data内容写入到文件,并重置block成初始化状态,清空compressedoutput。

[cpp] view plaincopy

  1. WriteRawBlock(block_contents,type, handle);
  2. r->compressed_output.clear();
  3. block->Reset();

6.4.5 WriteRawBlock函数

在WriteBlock把准备工作都做好后,就可以写入到sstable文件中了。来看函数原型:

void WriteRawBlock(const Slice& data, CompressionType, BlockHandle*handle);

函数逻辑很简单,见代码。

[cpp] view plaincopy

  1. Rep* r = rep_;
  2. handle->set_offset(r->offset); // 为index设置data block的handle信息
  3. handle->set_size(block_contents.size());
  4. nbsp;r->status =r->file->Append(block_contents); // 写入data block内容
  5. if (r->status.ok()) {// 写入1byte的type和4bytes的crc32
  6. chartrailer[kBlockTrailerSize];
  7. trailer[0] = type;
  8. uint32_t crc = crc32c::Value(block_contents.data(),block_contents.size());
  9. crc = crc32c::Extend(crc, trailer, 1);  // Extend crc tocover block type
  10. EncodeFixed32(trailer+1, crc32c::Mask(crc));
  11. r->status =r->file->Append(Slice(trailer, kBlockTrailerSize));
  12. if (r->status.ok()) { // 写入成功更新offset-下一个data block的写入偏移
  13. r->offset +=block_contents.size() + kBlockTrailerSize;
  14. }
  15. }

6.4.6 Finish函数

调用Finish函数,表明调用者将所有已经添加的k/v对持久化到sstable,并关闭sstable文件。

该函数逻辑很清晰,可分为5部分。

S1 首先调用Flush,写入最后的一块data block,然后设置关闭标志closed=true。表明该sstable已经关闭,不能再添加k/v对。

[cpp] view plaincopy

  1. Rep* r = rep_;
  2. Flush();
  3. assert(!r->closed);
  4. r->closed = true;

BlockHandle filter_block_handle,metaindex_block_handle, index_block_handle;

S2 写入filter block到文件中

[cpp] view plaincopy

  1. if (ok() &&r->filter_block != NULL) {
  2. WriteRawBlock(r->filter_block->Finish(), kNoCompression,&filter_block_handle);
  3. }

S3 写入meta index block到文件中

如果filterblock不为NULL,则加入从"filter.Name"到filter data位置的映射。通过meta index block,可以根据filter名字快速定位到filter的数据区。

[cpp] view plaincopy

  1. if (ok()) {
  2. BlockBuildermeta_index_block(&r->options);
  3. if (r->filter_block !=NULL) {
  4. //加入从"filter.Name"到filter data位置的映射
  5. std::string key ="filter.";
  6. key.append(r->options.filter_policy->Name());
  7. std::string handle_encoding;
  8. filter_block_handle.EncodeTo(&handle_encoding);
  9. meta_index_block.Add(key,handle_encoding);
  10. }
  11. // TODO(postrelease): Add stats and other metablocks
  12. WriteBlock(&meta_index_block, &metaindex_block_handle);
  13. }

S4 写入index block,如果成功Flush过data block,那么需要为最后一块data block设置index block,并加入到index block中。

[cpp] view plaincopy

  1. if (ok()) {
  2. if (r->pending_index_entry){ // Flush时会被设置为true
  3. r->options.comparator->FindShortSuccessor(&r->last_key);
  4. std::string handle_encoding;
  5. r->pending_handle.EncodeTo(&handle_encoding);
  6. r->index_block.Add(r->last_key, Slice(handle_encoding)); // 加入到index block中
  7. r->pending_index_entry =false;
  8. }
  9. WriteBlock(&r->index_block, &index_block_handle);
  10. }

S5 写入Footer。

[cpp] view plaincopy

  1. if (ok()) {
  2. Footer footer;
  3. footer.set_metaindex_handle(metaindex_block_handle);
  4. footer.set_index_handle(index_block_handle);
  5. std::string footer_encoding;
  6. footer.EncodeTo(&footer_encoding);
  7. r->status =r->file->Append(footer_encoding);
  8. if (r->status.ok()) {
  9. r->offset +=footer_encoding.size();
  10. }
  11. }

整个写入流程就分析完了,对于Datablock和Filter Block的操作将在Data block和Filter Block中单独分析,下面的读取相同。

levelDB SSTable-1

时间: 2024-12-26 15:35:48

levelDB SSTable-1的相关文章

LevelDB SSTable文件

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

分布式系统领域经典论文翻译集

分布式领域论文译序 sql&nosql年代记 SMAQ:海量数据的存储计算和查询 一.google论文系列 1.      google系列论文译序 2.      The anatomy of a large-scale hypertextual Web search engine (译 zz) 3.      web search for a planet :the google cluster architecture(译) 4.      GFS:google文件系统 (译) 5.  

SSTable and Log Structured Storage: LevelDB

If Protocol Buffers is the lingua franca of individual data record at Google, then the Sorted String Table (SSTable) is one of the most popular outputs for storing, processing, and exchanging datasets. As the name itself implies, an SSTable is a simp

leveldb源码分析--SSTable之逻辑结构

SSTable是leveldb 的核心模块,这也是其称为leveldb的原因,leveldb正是通过将数据分为不同level的数据分为对应的不同的数据文件存储到磁盘之中的.为了理解其机制,我们首先看看SSTable中的基本概念. 首先看看数据的整体存储结构: 可以从图中看到了几个概念:Datablock,Metablock, MetaIndex block, Indexblock, Footer.具体他们的含义可以大致解释如下: 1. Datablock,我们知道文件中的k/v对是有序存储的,他

leveldb源码分析--SSTable之TableBuilder

上一篇文章讲述了SSTable的格式以后,本文结合源码解析SSTable是如何生成的. void TableBuilder::Add(const Slice& key, const Slice& value) { //如果已经插入过数据,那么要保证当前插入的key > 之前最后一次插入的key, // SSTable必须是有序的插入数据 if (r->num_entries > 0) { assert(r->options.comparator->Compar

leveldb源码分析--SSTable之Compaction

对于compaction是leveldb中体量最大的一部分,也应该是最为复杂的部分,为了便于理解我们首先从一些基本的概念开始.下面是一些从doc/impl.html中翻译和整理的内容: Level 0 当日志文件超过一定大小的阈值是 (默认为 1MB): 建立一个新的memtable和日志文件,以后的操作都是用新的memtable和日志文件 后台进行如下操作: 将旧的 memtable写到SSTable中(过程为先转为immtable_table,然后遍历写入) 废弃旧的 memtable 删除

leveldb源码分析--SSTable之block

在SSTable中主要存储数据的地方是data block,block_builder就是这个专门进行block的组织的地方,我们来详细看看其中的内容,其主要有Add,Finish和CurrentSizeEstimate三个函数.Finish的逻辑十分简单就是简单的将restart点信息和restart点个数分别以PutFixed32的格式写入数据最后:CurrentSizeEstimate则是简单的计算当前块需要的存储大小 = 已插入的KV对的大小 + 重启点个数 * 4 + 1 * 4(重启

LevelDB源码之三SSTable

上一节提到的MemTable是内存表,而当内存表增长到一定程度时(memtable.size> Options::write_buffer_size),会将当前的MemTable数据持久化(LevelDB中实际有两份MemTable,后面LevelDB数据库备忘时会讲).持久化的文件(sst文件)称之为Table,LevelDB中的Table分为不同的层级,当前版本的最大层级为7(0-6),table中level0的数据最新,level6的数据最旧. Compaction动作负责触发内存表到SS

leveldb学习之sstable(2)

block写入:block_builder block.h和.cc里定义了block的entry存储格式和restart,提供了entry的查找接口以及迭代器.那么如何往写block里写entry呢?leveldb遵循面向对象的设计理念在block_builder类里提供了相关接口. BlockBuilder相关函数: Add( )将entry顺序写入现有block数据块的末端,排序工作在上层的函数完成. Finish( ),当block写满,完成写入重启点数组和重启点个数的写入 Reset(

leveldb学习:sstable(2)

block写入:block_builder block.h和.cc里定义了block的entry存储格式和restart,提供了entry的查找接口以及迭代器.那么怎样往写block里写entry呢?leveldb遵循面向对象的设计理念在block_builder类里提供了相关接口. BlockBuilder相关函数: Add( )将entry顺序写入现有block数据块的末端,排序工作在上层的函数完毕. Finish( ).当block写满,完毕写入重新启动点数组和重新启动点个数的写入 Res