7. LevelDB源码剖析之SSTable

7.1 基本原理

上一章提到的MemTable是内存表,当内存表增长到一定程度时(memtable.size> Options::write_buffer_size),Compact动作会将当前的MemTable数据持久化,持久化的文件称之为SSTable(Sorted String Table)。
LevelDB中的SSTable分为不同的层级,这也是LevelDB称之为Level DB的原因,当前版本的最大层级为7(0-6),level-0的数据最新,level-6的数据最旧。除此之外,Compact动作会将多个SSTable合并成少量的几个SSTable,以剔除无效数据,保证数据访问效率并降低磁盘占用。

7.2 物理布局

在存储设备上,一个SSTable被划分为多个Block数据块。每个Block中存储的可能是用户数据、索引数据或任何其他数据。Block除了数据块外,每个Block尾部还带了额外信息,布局如下:

Block组成
  • Compression Type标识Block中的数据是否被压缩,采用了何种压缩算法。
  • CRC则是Block的数字签名,用于校验数据的有效性。

Block是SSTable物理布局的关键。来看Block结构:

Block的物理布局

Block内部由以下两部分组成:

  • 数据记录:每一个Record代表了一条用户记录(Key-Value对)。严格上讲,并不是完整的用户记录,LevelDB在Key上Block做了优化。
  • 重启点信息:亦即索引信息,用于Record快速定位。如Restart[0]永远指向block的相对偏移0,Restart[1]指向重启点Record4的相对偏移。LevelDB在Key存储上做了优化,每个重启点指向的第一条Record记录了完整的Key值,而本重启点之内的其他key仅包含和前一条的差异项。

通过Block的构建过程进一步了解上述结构:

void BlockBuilder::Add(const Slice &key, const Slice &value)
{
  Slice last_key_piece(last_key_);
  assert(!finished_);
  assert(counter_ <= options_->block_restart_interval);
  assert(buffer_.empty() // No values yet?
         || options_->comparator->Compare(key, last_key_piece) > 0);

  //1. 构建Restart Point
  size_t shared = 0;

  //每block_restart_interval条记录创建一个重启点
  if (counter_ < options_->block_restart_interval)  //配置参数,默认为16
  {
    //尚未达到重启点间隔,沿用当前的重启点
    // See how much sharing to do with previous string
    const size_t min_length = std::min(last_key_piece.size(), key.size());
    while ((shared < min_length) && (last_key_piece[shared] == key[shared]))
    {
      shared++;
    }
  }
  else
  {
    //触发并创建新的重启点
    //此时,shared = 0; 重启点中将保存完整key
    // Restart compression
    restarts_.push_back(buffer_.size());  //buffer_.size()为当前数据块偏移
    counter_ = 0;
  }
  const size_t non_shared = key.size() - shared;

  //2. 记录数据
  // shared size | no shared size | value size | no shared key data | value data
  // Add "<shared><non_shared><value_size>" to buffer_
  PutVarint32(&buffer_, shared);
  PutVarint32(&buffer_, non_shared);
  PutVarint32(&buffer_, value.size());

  // Add string delta to buffer_ followed by value
  buffer_.append(key.data() + shared, non_shared);
  buffer_.append(value.data(), value.size());

  // Update state
  last_key_.resize(shared);
  last_key_.append(key.data() + shared, non_shared);
  assert(Slice(last_key_) == key);
  counter_++;
}

Buffer_代表当前数据块,restart_中则包含了重启点信息。当向block中新增一条记录时,首先设置重启点信息,包括:是否创建新的重启点,当前key和last key中公共部分大小。重启点信息整理完毕后,插入Record信息,Record信息的结构如下:

Record: shared size | no shared size | value size | no shared key data | value data

再来看Block构建完成时调用的Finish方法:

Slice BlockBuilder::Finish() {
        // Append restart array
        for (size_t i = 0; i < restarts_.size(); i++) {
            PutFixed32(&buffer_, restarts_[i]);
        }
        PutFixed32(&buffer_, restarts_.size());
        finished_ = true;
        return Slice(buffer_);
    }

此处和图3.1一致,在所有Record之后记录重启点信息,包括每条重启点信息(block中相对偏移)及重启点数量。

重启点机制主要有两点好处:

  1. 索引信息:用于快速定位,读取时通过重启点的二分查找先获取查找数据所属的重启点,随后在重启点内部遍历,时间复杂度为Log(n)。
  2. 空间压缩:有序key值使得相邻记录的key值的重叠度极高,通过上述方式可以有效降低持久化设备占用。

至此,SSTable的物理布局已然清晰。

7.3 逻辑布局

刚刚看过Block的结构,SSTable所有的数据存储都是以Block为基本单元进行的。现在来看SSTable的逻辑布局,这次我们先从实现说起:

void TableBuilder::Add(const Slice &key, const Slice &value)
{
  Rep *r = rep_;
  assert(!r->closed);
  if (!ok())
    return;
  if (r->num_entries > 0)
  {
    assert(r->options.comparator->Compare(key, Slice(r->last_key)) > 0);
  }

  //1. 构建Index
  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 (r->filter_block != NULL)
  {
    r->filter_block->AddKey(key);
  }

  //3. 记录数据
  r->last_key.assign(key.data(), key.size());
  r->num_entries++;
  r->data_block.Add(key, value);

  //4. 数据块(Data Block)大小已达上限,写入文件
  const size_t estimated_block_size = r->data_block.CurrentSizeEstimate();
  if (estimated_block_size >= r->options.block_size)
  {
    Flush();
  }
}

BlockBuilder 用于构建Block,TableBuidler则用于构建Table。Table构建过程中包含如下部分:

  1. 构建Index。Block中采用了重启点机制实现索引功能,在保证性能的同时又降低了磁盘占用。那么此处为何没有采用类似的机制呢?

实际上,此处索引键值的存储也做了优化,具体实现在FindShortestSeparator中,其目的在于获取最短的可以做为索引的“key”值。
举例来说,“helloworld”和”hellozoomer”之间最短的key值可以是”hellox”。
除此之外,另一个FindShortSuccessor方法则更极端,用于找到比指定key值大的最小key,如传入“helloworld”,返回的key值可能是“i”而已。作者专门为此抽象了两个接口,放置于Compactor中,可见其对编码也是是有“洁癖”的。

  1. 构建过滤器。在LevelDB的早期版本是没有过滤器的,1.2中引入过滤器,并默认提供布隆过滤器。过滤器的目的同样在于在于快速定位,LevelDB中,每2k大小构造一个过滤器。
// Generate new filter every 2KB of data
static const size_t kFilterBaseLg = 11;
static const size_t kFilterBase = 1 << kFilterBaseLg;
  1. 记录数据。通过block builder将key-value数据插入到data block中。
  2. 数据块刷新。当数据块(Data Block)大小已达上限时,写入文件。

除了data block写入文件,其他构建块均存储于内存中,此时并未真正持久化。真正持久化动作在Finish中:

Status TableBuilder::Finish()
{
  //1. Data Block
  Rep *r = rep_;
  Flush();
  assert(!r->closed);
  r->closed = true;

  BlockHandle filter_block_handle, metaindex_block_handle, index_block_handle;

  //2. Write filter block
  if (ok() && r->filter_block != NULL)
  {
    WriteRawBlock(r->filter_block->Finish(), kNoCompression,
                  &filter_block_handle);
  }

  //3. Write metaindex block
  if (ok())
  {
    BlockBuilder meta_index_block(&r->options);
    if (r->filter_block != NULL)
    {
      // 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);
    }

    // TODO(postrelease): Add stats and other meta blocks
    WriteBlock(&meta_index_block, &metaindex_block_handle);
  }

  //4. 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);
  }

  //5. Write footer
  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;
}

通过Finish方法,我们可以一窥SSTable的全貌:

SSTable逻辑布局
  1. Data Block:数据块,用户数据存放于此。
  2. Filter Blck:过滤器数据块。
  3. Meta Block:元数据块,当前存放过滤器数据。
  4. Index Block:索引块,用于用户数据快速定位。
  5. Footer:SSTable的注脚区,见下图。
  • Meta Index Block Handle指出了Meta Index Block的起始位置和大小。
  • Index Block Handle指出了Index Block的起始地址和大小。
  • 上述两个字段可以理解为索引的索引,是为了正确读出索引值而设立的,后面跟着一个填充区和魔数。


    SSTable Footer

本节最后,总结如下:

  1. SSTable一旦创建后,将只存在查询行为,在键值查找或SSTable遍历时,必定从重启点开始查找,因此除重启点位置的Record为完整key外,其他均为差异项亦可快速定位。
  1. Table、Block一旦创建后无法修改,TableBuilder负责Table创建,BlockBuilder负责。Table、Block最重要的接口为Iterator* NewIterator(...) const,用于查找、遍历数据。LevelDB中的Iterator稍显复杂,后面会统一备忘。
  2. Table、Block各自采用了类似的索引机制,并形成了Table到Block的多级索引。重启点、Table的索引机制在保证性能的同时又降低了存储空间。
    表3.1、图3.2中一直强调SSTable中存储的是Block,这种描述并不十分准确。表3.1中讲到,SSTable中存储了“Compression Type(是否压缩)”,如果数据被压缩,SSTable中存储的并不是Block数据本身,而是压缩后的数据,使用时则需先对Block解压。

7.4 总结

LevelDB中,所有数据最终被持久化为SSTable。LevelDB之所以称为“Level DB”是因为数据是分层的,每次被压缩的数据level提升一级。每一层的数据都比下一层更新(MemTable中的最新),各层数据均以SSTable格式存储。


转载请注明:【随安居士】http://www.jianshu.com/p/1ed0b94e8a2c

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容