ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

从 Level0直接将sst文件写入到LevelN 现象 来看rocksdb对compaction的优化

2021-01-15 12:57:34  阅读:280  来源: 互联网

标签:level sst rocksdb bytes LevelN base compaction L0 max


文章目录


关于compaction的详细逻辑介绍可以参考:
1. SST文件详细格式源码解析
2. Compaction 完整实现过程 概览

本文仅关注于讨论标题中提到的优化,不会对compaction细节有过多描述。

ps: 涉及到的源代码都是基于rocksdb 6.4.6版本

现象

最近观察一个k/v存储项目中rocksdb的一些行为,发现一个奇怪的现象。
Rocksdb 纯写,观察底层文件分布的过程中发现L0做完compaction之后直接写入到了L6(num_leves=7),也就是L0做完compaction跳过了L1–L5。
在这里插入图片描述
也就是上面图片中的日志。

这个现象让我们对rocksdb 实现的LSM tree的level compaction机制有点理解上的偏差,level compaction的过程应该是逐层compaction。
在这里插入图片描述
也就是memtable flush 到L0之后,L0向L1compaction,L1会和 L2的sst文件compaction到L2。但是现在,L0 达到4个sst文件触发的compaction最终却输出到了L6。

难以理解。

分析

看到这个有趣现象,首先想到的是确认一下compaction的sst文件output逻辑。

从挑选参与compaction的文件到最后生成一个compaction 对象会通过LevelCompactionBuilder::PickCompaction函数来进行。

Compaction* LevelCompactionBuilder::PickCompaction() {
  // Pick up the first file to start compaction. It may have been extended
  // to a clean cut.
  // 开始挑选一些参与compaction的文件,并确认输出的层
  SetupInitialFiles();
  if (start_level_inputs_.empty()) {
    return nullptr;
  }
  ...
}

确定compaction完成后要输出的Level 则会通过SetupInitialFiles函数。

void LevelCompactionBuilder::SetupInitialFiles() {
  // Find the compactions by size on all levels.
  bool skipped_l0_to_base = false;
  // 逐层选择文件,所以需要遍历所有层
  for (int i = 0; i < compaction_picker_->NumberLevels() - 1; i++) {
    start_level_score_ = vstorage_->CompactionScore(i);
    start_level_ = vstorage_->CompactionScoreLevel(i);
    assert(i == 0 || start_level_score_ <= vstorage_->CompactionScore(i - 1));
    // 当前层满足compaction的条件,即该层维护的一个变量score>1
    // 从该层中选择文件。
    if (start_level_score_ >= 1) {
      if (skipped_l0_to_base && start_level_ == vstorage_->base_level()) {
        // If L0->base_level compaction is pending, don't schedule further
        // compaction from base level. Otherwise L0->base_level compaction
        // may starve.
        continue;
      }
      // 确认compaction结束后的sst文件所在的输出层
      output_level_ =
          (start_level_ == 0) ? vstorage_->base_level() : start_level_ + 1;
      ...
   }
}

可以看到变量output_level_的确定是通过(start_level_ == 0) ? vstorage_->base_level() : start_level_ + 1;这样表达式来取值的。
可以看到如果起始level不为0时(Higher level compaction),则output_level 会在起始层的基础上+1。

但是 如果为0,则会从其他地方取值vstorage_->base_level()

最终取到的是 base_level()函数中的base_level_变量,而这个变量则是在VersionStorageInfo::CalculateBaseBytes函数中修改的。

这个函数是在创建column family的时候根据一些参数来计算接下来LSM 的形态,层和层之间的放大系数来计算 即每一层的大小,并且在每一层compaction完成之后写入MANIFEST的时候都会做一次重新计算。

查看这个函数代码的那一刻基本就知道为什么L0的sst文件会直接输出到L6了,核心就是如下代码的level_compaction_dynamic_level_bytes变量,这个变量是rocksdb options传入进来的。目的是为了能够根据LSM 不同层之间的大小 动态调整层之间的level_max_bytes_(即每一层能存储的数据上限)。

void VersionStorageInfo::CalculateBaseBytes(const ImmutableCFOptions& ioptions,
                                            const MutableCFOptions& options) {
  ...
  
  // level_compaction_dynamic_level_bytes 为false,即不支持动态调整level.
  //  所以刚开始的level就是1
  if (!ioptions.level_compaction_dynamic_level_bytes) {
    base_level_ = (ioptions.compaction_style == kCompactionStyleLevel) ? 1 : -1;
    
    ...
  } else {//level_compaction_dynamic_level_bytes 为true的时候
  
  	...
  	
    // 遍历每一层的文件,取到第一个不为空的level
    // 并保存之前层文件总大小最大的值。
    for (int i = 1; i < num_levels_; i++) {
      uint64_t total_size = 0;
      for (const auto& f : files_[i]) {
        total_size += f->fd.GetFileSize();
      }
      if (total_size > 0 && first_non_empty_level == -1) {
        first_non_empty_level = i;
      }
      if (total_size > max_level_size) {
        max_level_size = total_size;
      }
    }
    
    ...
    
    // 刚开始只有L0有文件,其他层都是空的,且上面的循环是从底层层开始确认文件大小的
    // 所以max_level_size会为0
    if (max_level_size == 0) {
      // No data for L1 and up. L0 compacts to last level directly.
      // No compaction from L1+ needs to be scheduled.
      base_level_ = num_levels_ - 1; // 为base_level_赋值,也就是N-1=6,(N=7)
	}
	...
}

可以看到level_compaction_dynamic_level_bytes 为true的时候我们L0向compaction的输出层会是N-1层。

然后核对了一下项目的参数配置,发现果然这个参数被某人 修改为true。

当然,这个分析过程只是探索一下这个现象,并没有什么性能问题。 本身这个参数在一些场景是有优势的,也是接下来要讲的。

优化

知道了是某人改的这个参数,导致了 L0的compaction 会优先向最底层compaction 这个现象。
接下来详细看看level_compaction_dynamic_level_bytes 参数 的作用。

这个参数核心目的是为了保证rocksdb在写入流量大的情况下维持LSM tree 数据分布的均衡,而不用我们人为介入,调整LSM tree的不同层之间的大小。所谓均衡,就是让数据尽可能均匀分布在整个LSM tree的不同层之间,这样的分布对整个rocksdb的性能会带来什么优势?

  • 先看看level_compaction_dynamic_level_bytes 为 false 的时候 LSM tree不同层之间的大小。
    关于level的配置如下:

    level_compaction_dynamic_level_bytes = false
    max_bytes_for_level_base = 268435456
    max_bytes_for_level_multiplier = 10
    max_bytes_for_level_multiplier_additional[0]=1
    max_bytes_for_level_multiplier_additional[1]=1
    ...
    

    计算不同层之间大小的公式为:
    Target_Size(Ln+1) = Target_Size(Ln) * max_bytes_for_level_multiplier * max_bytes_for_level_multiplier_additional[n]

    在这个配置下L1,L2,L3,L4,L5,L6…的大小分别是递增的:256M, 2.56G, 25.6G,256G,2.56T,25.6T…

    同时compaction的过程中start_level 和 output_level 仅仅会相差1

  • 再看看level_compaction_dynamic_level_bytes 配置为true 的时候 LSM tree不同层之间大小配置的差异
    在这个配置下,会先确定num_levels -1 层的大小,直接将当前level0的大小配置为level(num_levels-1)层大小。
    其他层的大小通过当前如下公式计算:
    Target_Size(Ln-1) = Target_Size(Ln) / max_bytes_for_level_multiplier

    如果Target_Size(Ln-1) 不满足sst文件的最小存储要求, 则会跳过这一层,直到遇到一个能够满足的层才会存储。
    当然num_levels-1 层的大小最开始会最做一个初始化,后续会随着数据的增加而不断动态增加(调整每一层大小的过程是每一轮 compaction完成之后会将compaction的sst文件变更更新新到MANIFEST文件,同时生成一个新的CF对象伴随着重新调整该CF中的相关level配置)。假如Targetsize(level6) = 265G。在这个配置下L1,L2,L3,L4,L5,L6…的大小分别是: 0,0,0.256M,2.56G,25.6G,256G。

    取社区wiki中的一张图更加直观
    在这里插入图片描述

总结

通过这个参数level_compaction_dynamic_level_bytes 的配置,我们能够将90% 以上的数据下沉到最后一层,带来的收益主要是:

  • 后续 针对Delete/多次put的数据一次compaction就能够清理,因为已经在最后一层了。节约存储空间,提升delete效率。
  • 进一步降低Higher level compaction所带来的资源占用,大量数据集中在最后一层,减少了多层之间的写放大。

存在的问题是 会带来一些读性能的影响,数据的随机读取会大量集中在最后一层。

总之,这个配置在写heavy的场景相比于静态的Level配置还是有一定的优势的。

标签:level,sst,rocksdb,bytes,LevelN,base,compaction,L0,max
来源: https://blog.csdn.net/Z_Stand/article/details/112645315

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有