leveldb之SSTable的创建与访问

在上篇对SSTable的介绍: leveldb之SSTable 中,已经分析了SSTable的结构布局,并详细分析了数据区DataBlock的生成和使用,本文将详细分析一个完整的sstable的创建及访问。

来自http://blog.csdn.net/houzengjiang/article/details/7718548 的.sst文件结构图如下


leveldb之SSTable的创建与访问_第1张图片

由上图可知,SSTable主要分为五部分:

1)DataBlock:存储Key-Value记录,分为Data、type、CRC三部分,其中Data部分的详细结构见 leveldb之SSTable

2)MetaBlock:暂时没有使用

3)MetaBlock_index:记录filter的相关信息(本文暂时没有考虑filter)

4)IndexBlock:描述一个DataBlock,存储着对应DataBlock的最大Key值,DataBlock在.sst文件中的偏移量和大小

5)Footer :索引的索引,记录IndexBlock和MetaIndexBlock在SSTable中的偏移量了和大小

1、TableBuilder

leveldb通过TableBuilder类来构建每一个.sst文件,TableBuilder类的成员变量只有一个结构体Rep* rep_,Rep的结构为:

struct TableBuilder::Rep {
  Options options;
  Options index_block_options;
  WritableFile* file;//要生成的.sst文件
  uint64_t offset;
  Status status;
  BlockBuilder data_block;//数据区
  BlockBuilder index_block;//索引
  std::string last_key;//上一个插入的key值,新插入的key必须比它大,保证.sst文件中的key是从小到大排列的
  int64_t num_entries;//.sst文件中存储的所有记录总数
  bool closed;         
  FilterBlockBuilder* filter_block;
  bool pending_index_entry;//当DataBlock为空时,为true
  BlockHandle pending_handle; //BlockHandle只有offset_和size_两个变量,用来记录DataBlock在.sst文件中的偏移量和大小

  std::string compressed_output;//是否需要对DataBlock中的内容进行压缩
};

TableBuilder与BlockBuilder类似,通过Add()函数向文件中加入一条记录,通过Finish()来完成一个SSTable的构建。在下面的分析中,暂时不考虑filter_block

1.1 TableBuilder::Add()

通过Add()函数向一个.sst文件中加入一条记录,主要分为:写index_block,写Data_block,更新相关变量,可能完成一个DataBlock并将数据写入磁盘

void TableBuilder::Add(const Slice& key, const Slice& value) {
  Rep* r = rep_;
  if (r->num_entries > 0) {
    assert(r->options.comparator->Compare(key, Slice(r->last_key)) > 0);//待插入的key值必须比ast_key大
  }

  if (r->pending_index_entry) {//DataBlock为空时,为true
    assert(r->data_block.empty());
    r->options.comparator->FindShortestSeparator(&r->last_key, key);
    std::string handle_encoding;
    r->pending_handle.EncodeTo(&handle_encoding);//handle_encoding记录每个DataBlock的偏移量和大小
    r->index_block.Add(r->last_key, Slice(handle_encoding));//将DataBlock的last_key、offset和size写入到index_block
    r->pending_index_entry = false;//变为false
  }

  r->last_key.assign(key.data(), key.size());//更新last_key
  r->num_entries++;//更新记录总数
  r->data_block.Add(key, value);//将key-value写入一个DataBlock

  const size_t estimated_block_size = r->data_block.CurrentSizeEstimate();//DataBlock的大小
  if (estimated_block_size >= r->options.block_size) {//当DataBlock所占空间超过设定值(默认为4K)时
    Flush();//完成一个DataBlock,并将DataBlock写入到磁盘.sst文件中
  }
}

1.1.1TableBuilder::Flush()

当一个DataBlock超过设定值(默认为4K,1个page)时,执行Flush()操作

void TableBuilder::Flush() {
  Rep* r = rep_;
  WriteBlock(&r->data_block, &r->pending_handle);
  if (ok()) {
    r->pending_index_entry = true;
    r->status = r->file->Flush();
  }
}
首先调用WriteBlock()写入数据,然后对.sst文件执行fflush()将数据写入磁盘

1.1.2TableBuilder::WriteBlock()

WriteBlock()首先调用BlockBuilder::Finish()完成一个DataBlock的创建并返回数据区的内容Slice,然后判断是否需要进行压缩,最后调用WriteRawBlock()写入数据,并调用BlockBuilder::Reset()重新开始一个DataBlock

1.1.3TableBuilder::WriteRawBlock()

由之前对SSTable布局的分析可知,一个.sst文件的数据区分为三部分:DataBlock、Type和CRC

传入三个参数:block_contents为调用BlockBuilder::Finish()返回的数据区的内容,type为是否压缩,handle为DataBlock的偏移量和大小

void TableBuilder::WriteRawBlock(const Slice& block_contents,
                                 CompressionType type,
                                 BlockHandle* handle) {
  Rep* r = rep_;
  handle->set_offset(r->offset);//更新DataBlock在.sst文件中的偏移量和大小
  handle->set_size(block_contents.size());
  r->status = r->file->Append(block_contents);//最终会调用fwrite将数据区内容写入到.sst文件中
  if (r->status.ok()) {
    char trailer[kBlockTrailerSize];
    trailer[0] = type;//第一个字节为type
    uint32_t crc = crc32c::Value(block_contents.data(), block_contents.size());
    crc = crc32c::Extend(crc, trailer, 1);  // Extend crc to cover block type
    EncodeFixed32(trailer+1, crc32c::Mask(crc));//将CRC写入trailer
    r->status = r->file->Append(Slice(trailer, kBlockTrailerSize));//将type和CRC写入.sst文件
    if (r->status.ok()) {
      r->offset += block_contents.size() + kBlockTrailerSize;
    }
  }
}

这样就完成了.sst文件中DataBlock的写入了

1.2TableBuilder::Finish()

调用Finish()来完成一个SSTable的创建,主要包括前面的DataBlock,还有IndexBlock、MetaIndexBlock、Footer等

Status TableBuilder::Finish() {
  Rep* r = rep_;
  Flush();//将数据区的内容全部写入到SSTable中

  BlockHandle filter_block_handle, metaindex_block_handle, index_block_handle;

  // Write metaindex block
  if (ok()) {  
    BlockBuilder meta_index_block(&r->options);
    if (r->filter_block != NULL) {//记录filter相关信息,暂时没有考虑
      // Add mapping from "filter.Name" to location of filter data
      std::string key = "filter.";
      key.append(r->options.filter_policy->Name());
      std::string handle_encoding;
      filter_block_handle.EncodeTo(&handle_encoding);
      meta_index_block.Add(key, handle_encoding);
    }
    WriteBlock(&meta_index_block, &metaindex_block_handle);
  }

  // Write index block
  if (ok()) {
    if (r->pending_index_entry) {
      r->options.comparator->FindShortSuccessor(&r->last_key);
      std::string handle_encoding;
      r->pending_handle.EncodeTo(&handle_encoding);
      r->index_block.Add(r->last_key, Slice(handle_encoding));
      r->pending_index_entry = false;
    }
    WriteBlock(&r->index_block, &index_block_handle);//将indexblock中的所有数据写入到SSTable文件中
  }

  // Write footer
  if (ok()) {
    Footer footer;//footer记录MetaIndexBlock和IndexBlock在SSTable文件中的偏移量和大小
    footer.set_metaindex_handle(metaindex_block_handle);
    footer.set_index_handle(index_block_handle);
    std::string footer_encoding;
    footer.EncodeTo(&footer_encoding);
    r->status = r->file->Append(footer_encoding);//将footer写入到SSTable中
    if (r->status.ok()) {
      r->offset += footer_encoding.size();
    }
  }
  return r->status;
}

file->Append()最终都会调用到fwrite(),将数据写入到磁盘中。

这样就将数据区和数据管理区的所有内容都写入到磁盘中的.sst文件中了


2、Table

Table类用来描述一个SSTable文件,Table类中也只有一个成员变量Rep *rep_,其结构为:

struct Table::Rep {
  Options options;
  Status status;
  RandomAccessFile* file;//.sst文件
  uint64_t cache_id;
  FilterBlockReader* filter;
  const char* filter_data;

  BlockHandle metaindex_handle;  // Handle to metaindex_block: saved from footer
  Block* index_block;
};

其内容主要包括通过SSTable文件中的Footer获得的IndexBlock和MetaIndexBlock(暂时不考虑filter)

2.1Table::Open()

通过Open一个.sst文件将其转换为Table结构,由下面的代码可知SSTable中的Footer是长度固定的,为2*BlockHandle::kMaxEncodedLength + 8,共28字节

void Footer::EncodeTo(std::string* dst) const {
  metaindex_handle_.EncodeTo(dst);
  index_handle_.EncodeTo(dst);
  dst->resize(2 * BlockHandle::kMaxEncodedLength);  // Padding
  PutFixed32(dst, static_cast<uint32_t>(kTableMagicNumber & 0xffffffffu));
  PutFixed32(dst, static_cast<uint32_t>(kTableMagicNumber >> 32));
}
因此可直接从一个SSTable中找到Footer结构体,而Footer是索引的索引,其中存储着IndexBlock和MetaIndexBlock的信息,因此可以很方便的获取IndexBlock。

2.2Table::InternalGet()

可通过InternalGet()来查找对应的记录

1、首先在IndexBlock中找到目标key所在的DataBlock在SSTable文件中的偏移量和大小

2、然后根据找到的IndexBlock中的key,offset,size找到对应的DataBlock

3、DataBlock包含实际数据区、type和CRC,调用Table::ReadBlock()来从中找到实际的数据区

4、然后在数据区中对目标key进行查找

3、TableCache

由之前对Cache的分析:leveldb之cache 可知,内存访问效率比磁盘访问效率要高得多,因此leveldb将通过Cache在内存中缓存最近使用到的一些文件,以提高访问效率。.sst文件主要对应的是TableCache,通过TableCache将最近使用到的.sst文件缓存在内存中,类Table通过成员变量cache_来管理缓存文件,cache_的成员变量key对应的则是每个SSTable的文件名。

3.1TableCache::Get()

通过Get()进行查找相应记录

Status TableCache::Get(const ReadOptions& options,
                       uint64_t file_number,
                       uint64_t file_size,
                       const Slice& k,
                       void* arg,
                       void (*saver)(void*, const Slice&, const Slice&)) {
  Cache::Handle* handle = NULL;
  Status s = FindTable(file_number, file_size, &handle);//查找.sst文件
  if (s.ok()) {
    Table* t = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table;
    s = t->InternalGet(options, k, arg, saver);//在目标.sst文件中查找目标记录
    cache_->Release(handle);
  }
  return s;
}
在查找时,首先调用FindTable()来查找目标记录所在的.sst文件,然后在.sst文件中调用InternalGet()查找目标记录(见上面的2.2),这样就完成了查找操作,并将.sst文件与缓存cache联系起来了。

3.2TableCache::FindTable()

Status TableCache::FindTable(uint64_t file_number, uint64_t file_size,
                             Cache::Handle** handle) {
  Status s;
  char buf[sizeof(file_number)];
  EncodeFixed64(buf, file_number);
  Slice key(buf, sizeof(buf));//.sst文件名
  *handle = cache_->Lookup(key);//首先在现有的缓存中进行查找,具体实现见<a target=_blank href="http://blog.csdn.net/u012658346/article/details/45486051">leveldb之cache</a> 
  if (*handle == NULL) {//如果文件不存在于缓存中
    std::string fname = TableFileName(dbname_, file_number);
    RandomAccessFile* file = NULL;
    Table* table = NULL;
    s = env_->NewRandomAccessFile(fname, &file);//打开一个.sst文件
    if (!s.ok()) {
      std::string old_fname = SSTTableFileName(dbname_, file_number);
      if (env_->NewRandomAccessFile(old_fname, &file).ok()) {
        s = Status::OK();
      }
    }
    if (s.ok()) {
      s = Table::Open(*options_, file, file_size, &table);//然后将.sst文件转换为Table
    }

    if (!s.ok()) {
      assert(table == NULL);
      delete file;
    } else {
      TableAndFile* tf = new TableAndFile;
      tf->file = file;
      tf->table = table;
      *handle = cache_->Insert(key, tf, 1, &DeleteEntry);//如果此文件不在cache_中,则将其加入到缓存中
    }
  }
  return s;
}

4、总结

1.类TableBuilder用来写入一个.sst文件:通过Add()向文件中加入一条记录,通过Finish()完成一个.sst文件的创建和写入

2.类Table利用成员变量index_block来描述一个.sst文件,通过Open()从一个.sst文件中获取index_block的内容,通过InternalGet()在一个.sst文件中查找目标记录

3.类TableCache通过成员变量cache_来将最近使用的.sst文件存放在内存中进行管理(LRU思想)。

4.SSTable的查找:

leveldb在查找一条记录时,首先是在Memtable中查找,当在Memtable中没有找到时,才在SSTable中查找。SSTable是存放在磁盘中的,而访问磁盘速度非常慢,因此leveldb将最近使用的SSTable文件缓存在内存中,以提高访问效率,这是通过TableCache实现的。

在SSTable中查找时,具体的步骤为:

1)通过cache->Lookup()在缓存中查找目标所在的.sst文件,当其不在缓存中时,在内存中创建一个.sst文件并调用cache->Insert()将其加入到缓存中。

2)在找到的.sst文件中调用InternalGet(),首先在index_block中进行查找,找到对应的DataBlock在.sst文件中的偏移和大小。由于DataBlock是由Block、type(是否压缩)和CRC三部分组成的,因此需要调用ReadBlock()获取真正的数据区。

3)然后调用block_iter->Seek(k)在数据区中进行查找,由于数据区包含多个重启点,因此首先是在重启点中进行二分查找,找到目标对应的重启点。然后从重启点开始找到重启点对应的一部分记录,并在其中查找目标key值。

这样就完成了在SSTable中的完整查找操作。


你可能感兴趣的:(leveldb之SSTable的创建与访问)