1.4-2. SSTable Write

SSTable은 다음과 같은 상황에서 만들어진다.

  1. MemTable로부터 Flush(Minor Compaction)가 일어날 때
  2. Storage에서 Compaction이 일어날 때

이 중 MemTable로부터 Flush가 일어날 때에 초점을 맞춰 SSTable에 어떻게 만들어지는지 다룬다.

MemTable로부터 Flush가 일어날 때

이 때 MemTable로부터 Flush가 일어나는 과정은 다음과 같다.

CompactMemTable이 호출되고, 이로 인해 WriteLevel0Table이 호출되면서 SSTable이 만들어지는데 이 때 BuildTable이 실질적으로 SSTable을 만든다.

BuildTable의 흐름은 다음과 같다.

전체 과정

Sequence 01

순서

  1. TableBuilder 인스턴스를 만든다
  2. TableBuilderAdd메소드를 통해 MemTable의 key-value pair들을 하나하나 추가한다.
  3. TableBuilderFinish메소드를 통해 SSTable을 만드는 과정을 마무리한다.
  4. WritableFile에 있는 내용들을 storage에 쓴다
  5. storage에 저장한 SSTable을 cache에 올려서 사용가능한지 확인해본다.
Status BuildTable(const std::string& dbname, Env* env, const Options& options,
                  TableCache* table_cache, Iterator* iter, FileMetaData* meta) {
  Status s;
  meta->file_size = 0;
  // Make that the Iterator points to the first element
  iter->SeekToFirst();

  std::string fname = TableFileName(dbname, meta->number);
  if (iter->Valid()) {
    WritableFile* file;
    s = env->NewWritableFile(fname, &file);
    if (!s.ok()) {
      return s;
    }

    // 1. Create an instance of TableBuilder
    TableBuilder* builder = new TableBuilder(options, file);
    meta->smallest.DecodeFrom(iter->key());
    Slice key;

    // 2. Add key-value pairs of MemTable one by one via TableBuilder`s "Add" method
    for (; iter->Valid(); iter->Next()) {
      key = iter->key();
      builder->Add(key, iter->value());
    }
    if (!key.empty()) {
      meta->largest.DecodeFrom(key);
    }

    // 3. Complete the process of creating SSTable via TableBuilder's "Finish" method
    s = builder->Finish();
    if (s.ok()) {
      meta->file_size = builder->FileSize();
      assert(meta->file_size > 0);
    }
    delete builder;

    // 4. Write the contents in the WritableFile to storage
    if (s.ok()) {
      s = file->Sync();
    }
    if (s.ok()) {
      s = file->Close();
    }
    delete file;
    file = nullptr;

    if (s.ok()) {
      // 5. Put the SSTable stored in storage into cache and check if it is available
      Iterator* it = table_cache->NewIterator(ReadOptions(), meta->number,
                                              meta->file_size);
      s = it->status();
      delete it;
    }
  }

  // Check for Iterator related errors
  if (!iter->status().ok()) {
    s = iter->status();
  }

  if (s.ok() && meta->file_size > 0) {
    // Keep it
  } else {
    env->RemoveFile(fname);
  }
  return s;
}

TableBuilder::Add

TableBuilder안에 있는 각각의 BlockBuilder들에게 Iterator가 현재 참조하고 있는 key-value pair를 전달하는 역할을 한다.

  1. 현재 BlockBuilder로 만드는 Data Block이 비어있다면, 즉 새로운 Data Block을 구성하기 시작했다면 Index Block에 새 Entry를 추가한다. 이 때 추가되는 Entry는 현재 새로 만들기 시작한 Data Block에 대한 것이 아니라 바로 이전까지 만들던 Data Block에 대한 Entry이다.
  2. Bloom Filter를 사용하는 경우 Filter Block도 업데이트한다.
  3. Data Block에 데이터를 추가한다.
  4. 만약 작성 중인 Data Block이 꽉 찼다면(option으로 지정한 block size이상이 된 경우) Flush를 호출한다.
void TableBuilder::Add(const Slice& key, const Slice& value) {
  Rep* r = rep_;
  
  // ...

  // 1. If the Data Block that BlockBuilder is creating is empty,
  //    add a new entry to the Index Block
  if (r->pending_index_entry) {
    assert(r->data_block.empty());
    r->options.comparator->FindShortestSeparator(&r->last_key, 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;
  }

  // 2. If using Bloom Filter, update the Filter Block as well
  if (r->filter_block != nullptr) {
    r->filter_block->AddKey(key);
  }

  r->last_key.assign(key.data(), key.size());
  r->num_entries++;
  // 3. Add data to the Data Block
  r->data_block.Add(key, value);

  const size_t estimated_block_size = r->data_block.CurrentSizeEstimate();
  // 4. If the Data Block being created is full, call "Flush"
  if (estimated_block_size >= r->options.block_size) {
    Flush();
  }
}

TableBuilder::Finish

MemTable의 모든 key-valur pair들에 대해 Add가 끝났을 때 호출되며, 작성중인 SSTable을 마무리하는 역할을 한다.

  1. Flush를 호출한다.
  2. Bloom Filter를 사용하는 경우 WritableFile에 FilterBlockBuilder로 Filter Block을 추가한다.
  3. WritableFile에 Meta Index Block을 추가한다.
  4. WritableFileBlockBuilder로 만들고 있던 Index Block을 추가한다.
  5. WritableFile에 Footer를 추가한다.
Status TableBuilder::Finish() {
  Rep* r = rep_;
  // 1. Call "Flush"
  Flush();
  assert(!r->closed);
  r->closed = true;

  BlockHandle filter_block_handle, metaindex_block_handle, index_block_handle;

  // 2. If using Bloom Filter, add the Filter Block to the WritableFile.
  if (ok() && r->filter_block != nullptr) {
    WriteRawBlock(r->filter_block->Finish(), kNoCompression,
                  &filter_block_handle);
  }

  // 3. Add the Meta Index Block to the WritableFile
  if (ok()) {
    BlockBuilder meta_index_block(&r->options);
    if (r->filter_block != nullptr) {
      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);
  }

  // 4. Add the Index Block to the WritableFile.
  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);
  }

  // 5. Add the Footer to the WritableFile.
  if (ok()) {
    Footer footer;
    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);
    if (r->status.ok()) {
      r->offset += footer_encoding.size();
    }
  }
  return r->status;
}

TableBuilder::Flush

BlockBuilder로 만들고 있는 Data Block을 storage에 쓰는 역할을 한다.

  1. BlockBuilder로 만들고 있는 Data Block의 contents를 WritableFile에 추가한다.
  2. WritableFile에 쓴 내용을 storage에 쓴다.
  3. Bloom Filter를 사용할 경우 새 Bloom Filter를 만든다.
void TableBuilder::Flush() {
  Rep* r = rep_;
  
  // ...

  // 1. Add the contents of the Data Block being created to the WritableFile
  WriteBlock(&r->data_block, &r->pending_handle);
  if (ok()) {
    r->pending_index_entry = true;
    // 2. Write the contents of WritableFile to storage
    r->status = r->file->Flush();
  }
  // 3. If using Bloom Filter, create a new Bloom Filter
  if (r->filter_block != nullptr) {
    r->filter_block->StartBlock(r->offset);
  }
}