天天看点

LSM 优化系列(四) -- Rocksdb和Lethe 对Delete问题的优化

文章目录

  • ​​前言​​
  • ​​1. 问题背景​​
  • ​​2. 问题复现​​
  • ​​3. Rocksdb 的 Delete-Aware 优化​​
  • ​​3.1 可配置的 Delete-Aware调度​​
  • ​​3.2 Compaction 逻辑对 delete key的优化​​
  • ​​4. Lethe: A Tunable Delete-Aware LSM Engine . SIGMOD'20​​

前言

本文介绍过程中涉及到的源代码是基于​

​rocksdb 6.4.6​

​ 版本的。

同时需要对rocksdb的Compaction逻辑以及源代码有较为深入的了解,相关的原理介绍之前有一些粗浅的分析,对理解不够深入的同学应该有帮助。

​1. SST文件详细格式源码解析​​

​​2. Compaction 完整实现过程 概览​​

1. 问题背景

基于LSM的存储引擎 在提供了优秀的写性能的基础上 却因为compaction带来了很多其他问题。

本文所要描述的一个场景是LSM引擎的标记删除(将对一个key的删除作为一个key-value 追加写入到引擎之中),这种删除逻辑会带来如下几个问题:

  • 读放大:range scan 的时候需要扫描更多的记录,点查询Bloom Filter 的false positive概率增加。
  • 空间放大:因为delete的type 也是一个key-value,而这种类型的key只有在compaction到最后一层才会被删除,中间层的很多空间就会被浪费在delete的存储之上。
  • 写放大:本身LSM compaction带来的原有问题,而针对delete key的频繁调度compaction 带来的写放大问题更不友好。
  • 用户隐私:用户已经标记删除的数据并没有及时删除,未到最后一层,无法真正删除。
LSM 优化系列(四) -- Rocksdb和Lethe 对Delete问题的优化

如上图:标记D的都是含有delete 的sst文件,加入我们想要查找的key在L2,这个key其实之前已经被删除了。但是数据并没有被清理,对应sst文件的bloom filter也没有更新,所以无法准确判断改key是否还在,则需要一直读到 purpose key所在的sst文件的data block,发现这个key时delete type,最后才向客户端返回not found。

这个查找过程存在数次I/O操作,读sst文件的 filter block,index block, 整个data block,真删除的不存在key的查找 多了 两个block的IO流程(index block 和 data block),代价可想而知。

2. 问题复现

因为业务中存在这样的问题,所以直接上代码,更加直观。

复现如下简单且经典场景(sql删除了一个表中80%的数据,后来想scan剩下的一部分数据):

写入1千万的相同前缀 且字典序递增的key,删除其中的的9百万,从头遍历 直到取得未删除的前500条key。

站在用户的角度:

删除了一批key, 读取剩下的key中的100条

引擎的角度:

用户调用了删除接口,写入了大量的delete type的key。但是 delete type的key还在LSM 上层,一点一点做compaction到最后一层。所以用户调用一批scan 查找的过程相当于将已经标记delete 但是还未清理数据的key都得扫一遍。

通过实现一段rocksdb的复现代码如下:

#include <unistd.h>
#include <atomic>
#include <iostream>
#include <random>
#include <string>
#include <thread>

#include <sys/time.h>

#include "rocksdb/db.h"
#include "rocksdb/table.h"
#include "rocksdb/cache.h"
#include "rocksdb/filter_policy.h"
#include "rocksdb/write_batch.h"
#include "rocksdb/rate_limiter.h"
#include "rocksdb/perf_context.h"
#include "rocksdb/iostats_context.h"
#include "rocksdb/table_properties.h"
#include "utilities/table_properties_collectors.h"
#include "gflags/gflags.h"

#define VALUE_LEN 500 

using namespace google;

DEFINE_int64(time,1200,"read threads' ratio");
DEFINE_int64(thread_num,64,"total threads ");
DEFINE_string(db_dir, "srd_delete", "db's dir is delete");
DEFINE_bool(use_fullcompaction, false, "use full compaction to db");
DEFINE_int64(num_keys, 1000000,"write keys' num");
DEFINE_int64(wait_compaction,10,"wait");
DEFINE_bool(traverse, true, "use traverse");
DEFINE_bool(use_delete_collector, false, "use traverse");

std::atomic<long> g_op_W;

rocksdb::DB* src_db;
rocksdb::Options options;
std::mt19937_64 generator_; // 生成伪随机数

// 返回微秒
inline int64_t now() {
  struct timeval t;
  gettimeofday(&t, NULL);
  return static_cast<int64_t>(t.tv_sec)*1000000 + t.tv_usec;
}

// 打开rocksdb
void OpenDB() {
  options.create_if_missing = true;
  options.compression = rocksdb::kNoCompression;
  options.disable_auto_compactions = FLAGS_use_fullcompaction;
  
  if(FLAGS_use_delete_collector) {
    options.table_properties_collector_factories.emplace_back(rocksdb::NewCompactOnDeletionCollectorFactory(1000000, 1000));
  }
  std::shared_ptr<rocksdb::Cache> cache = rocksdb::NewLRUCache(10737418240);

  rocksdb::BlockBasedTableOptions bbto;
  bbto.whole_key_filtering = true;
  bbto.cache_index_and_filter_blocks = true;
  bbto.filter_policy.reset(rocksdb::NewBloomFilterPolicy(16,false));
  bbto.block_cache = cache;
  options.table_factory.reset(rocksdb::NewBlockBasedTableFactory(bbto));

  options.max_background_compactions = 32;
  auto s = rocksdb::DB::Open(options, FLAGS_db_dir, &src_db);
  std::cout << "open src_db" <<  s.ToString() << std::endl;

}

// 配置手动执行full compaction
void FullCompaction() {
  rocksdb::Slice begin("");

  rocksdb::Iterator *it = src_db->NewIterator(rocksdb::ReadOptions());
  it->SeekToLast();
  std::string end_str = it->key().ToString();
  delete it;

  rocksdb::Slice end(end_str);
  auto s = src_db->CompactRange(rocksdb::CompactRangeOptions(), &begin, &end);
  if (!s.ok()) {
    std::cout << "CompactRange failed " << s.ToString() << std::endl;
  }
}

// 写入 + 删除
void DOWrite(int num) {
  int count = FLAGS_num_keys;
  std::string value(VALUE_LEN, 'x'); 
  while(count > 0) {
    src_db -> Put(rocksdb::WriteOptions(), std::string("test_compaction_129_")+std::to_string(count),value);
    ++ g_op_W;
    count --;
  }

  // 删除90% 的key
  std::cout << "Begin delete "<< std::endl;
  for (int i = 0;i < FLAGS_num_keys - FLAGS_num_keys / 10; ++i) {
    src_db->Delete(rocksdb::WriteOptions(), std::string("test_compaction_129_")+std::to_string(i));
  }
  src_db->SetOptions(src_db->DefaultColumnFamily(),{{"diable_auto_compactions","true"}});
  std::cout << "End delete \n";

  // 开启full compaction
  if(FLAGS_use_fullcompaction) {
    FullCompaction();
    sleep(FLAGS_wait_compaction);
  }
}

void Traverse() {
  // open perf 
  rocksdb::SetPerfLevel(rocksdb::PerfLevel::kEnableTimeExceptForMutex);
  rocksdb::get_perf_context()->Reset();
  rocksdb::get_iostats_context()->Reset();

  rocksdb::Iterator *it = src_db->NewIterator(rocksdb::ReadOptions());
  it->Seek("");
  
  std::cout << "Traverse begin :" << std::endl;
  int64_t ts = now();
  int seek_count = 0;
  for(; it->Valid()&& seek_count<500;  it->Next() ) {
    std::cout << it->key().ToString() << std::endl; 
    seek_count++;
  }

  delete it;
  
  // close perf,查看internal_delete_skipped_count指标
  rocksdb::SetPerfLevel(rocksdb::PerfLevel::kDisable);
  std::cout << "Traverse use time :" << now() - ts << std::endl;
  std::cout << "internal_delete_skipped_count "  
            << rocksdb::get_perf_context()->internal_delete_skipped_count
            << std::endl;
}


int main(int argc, char** argv) {
  ParseCommandLineFlags(&argc,&argv, true);

  OpenDB();

  for(int i = 0;i < FLAGS_thread_num; i++) {
    new std::thread(DOWrite,i);
  }


  long last_opn_W = 0;
  int count = FLAGS_time;
  while(count > 0) {
    sleep(1);
    long nopn_W = g_op_W;
    
    std::cout << "write_speed : " << nopn_W - last_opn_W << std::endl;
    last_opn_W = nopn_W;
    count --;
  }

  if(FLAGS_traverse){
    Traverse();
  }
  return 0;
}      

编译时需要加入gflags 的链接,最终可以通过如下命令进行测试:

测试原生compaction逻辑下 traverse过程的耗时

​./test_delete -use_fullcompaction=false -time=300 -db_dir=./src_delete -thread_num=1 -num_keys=10000000 -use_delete_collector=false​

可以发现最终的输出:

Traverse use time :2018292      

也就是在客户端看来,只获取100个存在的key就话费2s的时间。。。这简直不能忍,这样的存储引擎如何让用户安心使用。

这里尝试使用 Full Compaction 进行优化,即rocksdb自己提供的手动compaction接口​

​CompacRange​

​,在删除操作执行完成之后对整个db进行了一次全量的compaction操作,再次进行遍历获取100个存在key的耗时统计。

以上的代码也有增加,使用如下命令运行

​./test_delete -use_fullcompaction=true -time=300 -db_dir=./src_delete -thread_num=1 -num_keys=10000000 -use_delete_collector=false​

可以看到这次的运行耗时下降了3个量级,仅仅只有364us:

Traverse use time :364      

那是不是我们这个问题就解决了呢?显然不可能。

全量compaction 带来的代价是极大的,所有的sst文件不论之前有没有参与过compaction都会被调度起来,巨量的I/O 和 CPU 洪峰,且跟随本节点数据量的大小持续一段时间;这个过程可能直接让系统的可用性极低甚至掉零,更为严重的是某位不讲武德的仁兄看到优化效果如此给力,不经过严格的测试直接线上跑 ,那这位仁兄就可以准备好挂个p0 走人了。

况且存储引擎作为持久化存储的核心,这样的危险操作显然不会交给用户来做,所以本身引擎内部要有足量机制降低 未compaction清理的delete key对scan性能的影响,这也是引擎的职责所在。

总结这个问题放在引擎中处理 的几个关节点 如下:

  • delete的过程是通过compaction进行的,而compaction对于用户来说是黑盒,用户很难精确控制compaction过程中的delete key清理时机。
  • 实际LSM的应用场景中 用户驱动删除的workload 相对较少,除了用户主动删除之外LSM 引擎也会应用在关系数据库之中 出现drop 表等被动删除场景,所以在用户不知道的情况下引擎也会产生大范围的数据清理和迁移。
  • 对非连续的key的删除调度。比如key的相同前缀是基于table id,删除时则基于某一时间戳进行删除。这个过程需要调度seek进行大量的查找,代价极大。

接下来将介绍一下 rocksdb 以及 业界对该场景的优化手段。rocksdb这里比较熟悉,会深入源代码详细介绍一番;业界对该优化的过程将介绍几篇已有的优化论文。

3. Rocksdb 的 Delete-Aware 优化

基于以上问题,接下来会介绍Rocksdb 在该场景所做的两个优化feature。

Rocksdb 在保证不增加LSM 的读代价的情况下 针对delete type的key增加了两方面的优化逻辑:

  • 用户态可配置的delete 密集型 sst文件的compaction优先级调度。
  • compaction本身针对 delete key 逻辑的 处理优化。

3.1 可配置的 Delete-Aware调度

​OpenDB​

​函数中增加如下rocksdb配置:

options.table_properties_collector_factories.emplace_back(rocksdb::NewCompactOnDeletionCollectorFactory(10000, 1000));      

同时需要包含头文件:

#include "rocksdb/table_properties.h"
#include "utilities/table_properties_collectors.h"      

查看以上函数声明:

std::shared_ptr<CompactOnDeletionCollectorFactory>
    NewCompactOnDeletionCollectorFactory(
        size_t sliding_window_size,
        size_t deletion_trigger) {
  return std::shared_ptr<CompactOnDeletionCollectorFactory>(
      new CompactOnDeletionCollectorFactory(
          sliding_window_size, deletion_trigger));
}

// A factory of a table property collector that marks a SST
// file as need-compaction when it observe at least "D" deletion
// entries in any "N" consecutive entires.
//
// @param sliding_window_size "N"
// @param deletion_trigger "D"
CompactOnDeletionCollectorFactory(size_t sliding_window_size,
                                  size_t deletion_trigger)
  : sliding_window_size_(sliding_window_size),
deletion_trigger_(deletion_trigger) {}      

总结一下:

​NewCompactOnDeletionCollectorFactory​

​函数声明一个用户配置的表格属性收集器,这里的表格指的是sstable。

该函数需要传入的两个参数​

​sliding_window_size​

​​和 ​

​deletion_trigger​

​ 表示删除收集器 的滑动窗口 和 触发删除的key的个数。

LSM 优化系列(四) -- Rocksdb和Lethe 对Delete问题的优化

在每个sst文件内维护一个滑动窗口,滑动窗口维护了一个窗口大小,在当前窗口大小内出现的delete-key的个数超过了窗口的大小,那么这个sst文件会被标记为​

​need_compaction_​

​,从而在下一次的compaction过程中被优先调度。

达到将delete-key较多的sst文件快速下沉到底层,delete的数据被真正删除的目的,可以理解为这是一种更加激进的compaction调度策略。

使用者仅仅需要调整两个参数的比例,即可决定该sst文件是否应该被优先调度compaction。

接下来详细说一下Rocksdb的这个配置如何实现deletes 超过一定比例之后被compaction优先调度:

用户配置了​

​table_properties_collector_factories​

在Compaction的核心处理key-value的函数​

​CompactionJob::ProcessKeyValueCompaction​

​​ 中调度 ​

​BlockBasedTableBuilder::Add​

​​ 函数 构造sst文件中不同存储区域的builder,这里面会使用用户构造的collector 进行 指定key的收集,通过如下函数​

​NotifyCollectTableCollectorsOnAdd​

​​ 进入 ​

​InternalAdd​

​ 。

bool NotifyCollectTableCollectorsOnAdd(
    const Slice& key, const Slice& value, uint64_t file_size,
    const std::vector<std::unique_ptr<IntTblPropCollector>>& collectors,
    Logger* info_log) {
  bool all_succeeded = true;
  for (auto& collector : collectors) {
    Status s = collector->InternalAdd(key, value, file_size); 
    all_succeeded = all_succeeded && s.ok();
    if (!s.ok()) {
      LogPropertiesCollectionError(info_log, "Add" /* method */,
                                   collector->Name());
    }
  }
  return all_succeeded;
}      

因我我们reset的是​

​NewCompactOnDeletionCollectorFactory​

​​ collector,所以会通过​

​UserKeyTablePropertiesCollector::InternalAdd​

​​ 进入​

​CompactOnDeletionCollector::AddUserKey​

​​ collector的​

​AddUserKey​

​函数:

  • 增加滑动窗口内 真正key的数量,并根据是否达到了窗口阈值 来及时更新内部key的数量 以及 降低旧的delete key的数量
  • 根据传入的key的类型来 增加滑动窗口内 delete key的数量,达到删除阈值时会打入​

    ​need_compaction_​

    ​标记(这个标记会在后续的compaction逻辑中将当前sst文件标记为优先级较高的sst文件)
Status CompactOnDeletionCollector::AddUserKey(const Slice& /*key*/,
                                              const Slice& /*value*/,
                                              EntryType type,
                                              SequenceNumber /*seq*/,
                                              uint64_t /*file_size*/) {
  ...
  // 这里的bucket_size_可以理解为我们设置的滑动窗口大小
  // 第一个if语句中做的事情主要是更新滑动窗口中 key的数量 以及 旧的delete key的数量
  if (num_keys_in_current_bucket_ == bucket_size_) {
    // When the current bucket is full, advance the cursor of the
    // ring buffer to the next bucket.
    current_bucket_ = (current_bucket_ + 1) % kNumBuckets;

    // Update the current count of observed deletion keys by excluding
    // the number of deletion keys in the oldest bucket in the
    // observation window.
    assert(num_deletions_in_observation_window_ >=
        num_deletions_in_buckets_[current_bucket_]);
    num_deletions_in_observation_window_ -=
        num_deletions_in_buckets_[current_bucket_];
    num_deletions_in_buckets_[current_bucket_] = 0;
    num_keys_in_current_bucket_ = 0;
  }

  num_keys_in_current_bucket_++;
  // 根据key的类型来调整是否达到触发删除的阈值,从而打入need_compaction_标记
  if (type == kEntryDelete) { 
    num_deletions_in_observation_window_++;
    num_deletions_in_buckets_[current_bucket_]++;
    if (num_deletions_in_observation_window_ >= deletion_trigger_) {
      need_compaction_ = true;
    }
  }
  ...
}      

在collector中达到触发删除的标记之后会在​

​BlockBasedTableBuilder::NeedCompact()​

​​中体现,而这个函数则会被​

​CompactionJob::FinishCompactionOutputFile​

​调用

bool BlockBasedTableBuilder::NeedCompact() const {
  for (const auto& collector : rep_->table_properties_collectors) {
    if (collector->NeedCompact()) {
      return true;
    }
  }
  return false;
}      

​CompactionJob::FinishCompactionOutputFile​

​​同样是在compaction的核心逻辑​

​ProcessKeyValueCompaciton​

​ 中调用,以sst文件为单位 将构造好的一个个builder写入到具体的sst文件中的数据区域,写入过程中对要写入的sst文件打入标记(如果collector中确认delete keys超过了设置的阈值)

meta->marked_for_compaction = sub_compact->builder->NeedCompact();      

​marked_for_compaction​

​​标记会在函数​

​VersionStorageInfo::ComputeFilesMarkedForCompaction​

​​ 被使用,其中将有marked_for_compaction标记文件添加到一个动态数组之中​

​files_marked_for_compaction_​

​,这个数组会在当前compaction完成之后再次被调度,将动态数组中的sst文件优先进行compaction的调度。

void VersionStorageInfo::ComputeFilesMarkedForCompaction() {
  files_marked_for_compaction_.clear();
  int last_qualify_level = 0;

  // Do not include files from the last level with data
  // If table properties collector suggests a file on the last level,
  // we should not move it to a new level.
  for (int level = num_levels() - 1; level >= 1; level--) {
    if (!files_[level].empty()) {
      last_qualify_level = level - 1;
      break;
    }
  }

  for (int level = 0; level <= last_qualify_level; level++) {
    for (auto* f : files_[level]) {
      if (!f->being_compacted && f->marked_for_compaction) {
        // 将被打入标记的文件添加到动态数组之中
        files_marked_for_compaction_.emplace_back(level, f); 
      }
    }
  }
}      

后续的调度过程会在​

​DBImpl::BackgroundCompaction​

​​中,通过如下逻辑调用 ​

​LevelCompactionPicker​

​​的​

​NeedsCompaction​

​​来进行判断,判断的过程主要是确认​

​files_marked_for_compaction_​

​​不为空即返回true,从而再次将文件添加到当前column family的待compaction队列,最后通过​

​MaybeScheduleFlushOrCompaction​

​再次调度。

if (cfd->NeedsCompaction()) {
            // Yes, we need more compactions!
            AddToCompactionQueue(cfd);
            ++unscheduled_compactions_;
            MaybeScheduleFlushOrCompaction();
          }      

整个链路相对来说比较长且复杂的,需要对Compaction的调度代码较为熟悉的话会更容易理解。

最后能够在rocksdb的LOG日志来确认是否触发了Marked的逻辑:

LSM 优化系列(四) -- Rocksdb和Lethe 对Delete问题的优化

​"compaction_reason": "ManualCompaction",​

​​ 这样的字段,且后续的Compaction job的输出中能够看到​

​records in​

​​的数据 和​

​records dropped​

​的数据的比例满足我们设置的collector中的 sliding-window-size 和 delete-trigger的比例即可。

最后的性能可以通过上一章节中的 问题复现 的代码看到。

运行如下命令:

​./test_delete -use_fullcompaction=false -time=300 -db_dir=./src_delete -thread_num=1 -num_keys=10000000 -use_delete_collector=true​

可以看到相同的delete场景,最后的traverse性能相比于原来能够提升4倍,这个数据在不同的delete 比重以及collector设置的参数下有差异的。

Traverse use time :674629      

3.2 Compaction 逻辑对 delete key的优化

Compaction调度过程不再深入描述了,直接到上小节说过的​

​ProccessKeyValueCompaction​

​函数中会对key-value一个一个处理,会通过Compaction迭代器的移动(基本的SeekToFirst,Next等)来达到一个一个处理的目的。

处理的过程通过调用​

​NextFromInput​

​函数来进行,这个函数会处理不同的key-type,其中关于delete类型的主要处理逻辑如下代码(这一部分全部粘贴到下面了,可以简单看看):

我们知道原本LSM 中的delete key 只能在最后一层将之前所有的写入包括自己都清理掉,因为到了最后一层就知道之前已经没有人再使用这个key了。

这里可能有人会问,用户已经删除了,为什么还一定要到最后一层才将key的数据清理掉呢?

因为rocksdb提供了snapshot功能,即在当前delete之前可能存在用户打入的snapshot,即rocksdb需要保证那个时刻的key能够被读到,所以删除的过程中都需要确认当前key之前版本是否有snapshot的sequence number,如果没有才允许后续的删除判断。

} else if (compaction_ != nullptr && ikey_.type == kTypeDeletion &&
               IN_EARLIEST_SNAPSHOT(ikey_.sequence) &&
               ikeyNotNeededForIncrementalSnapshot() &&
               compaction_->KeyNotExistsBeyondOutputLevel(ikey_.user_key,
                                                          &level_ptrs_)) {
      // TODO(noetzli): This is the only place where we use compaction_
      // (besides the constructor). We should probably get rid of this
      // dependency and find a way to do similar filtering during flushes.
      //
      // For this user key:
      // (1) there is no data in higher levels
      // (2) data in lower levels will have larger sequence numbers
      // (3) data in layers that are being compacted here and have
      //     smaller sequence numbers will be dropped in the next
      //     few iterations of this loop (by rule (A) above).
      // Therefore this deletion marker is obsolete and can be dropped.
      //
      // Note:  Dropping this Delete will not affect TransactionDB
      // write-conflict checking since it is earlier than any snapshot.
      //
      // It seems that we can also drop deletion later than earliest snapshot
      // given that:
      // (1) The deletion is earlier than earliest_write_conflict_snapshot, and
      // (2) No value exist earlier than the deletion.
      ++iter_stats_.num_record_drop_obsolete;
      if (!bottommost_level_) {
        ++iter_stats_.num_optimized_del_drop_obsolete;
      }
      input_->Next();
    } else if ((ikey_.type == kTypeDeletion) && bottommost_level_ &&
               ikeyNotNeededForIncrementalSnapshot()) {
      // Handle the case where we have a delete key at the bottom most level
      // We can skip outputting the key iff there are no subsequent puts for this
      // key
      ParsedInternalKey next_ikey;
      input_->Next();
      // Skip over all versions of this key that happen to occur in the same snapshot
      // range as the delete
      while (input_->Valid() && ParseInternalKey(input_->key(), &next_ikey) &&
             cmp_->Equal(ikey_.user_key, next_ikey.user_key) &&
             (prev_snapshot == 0 ||
              DEFINITELY_NOT_IN_SNAPSHOT(next_ikey.sequence, prev_snapshot))) {
        input_->Next();
      }
      // If you find you still need to output a row with this key, we need to output the
      // delete too
      if (input_->Valid() && ParseInternalKey(input_->key(), &next_ikey) &&
          cmp_->Equal(ikey_.user_key, next_ikey.user_key)) {
        valid_ = true;
        at_next_ = true;
      }
    }      

以上逻辑的优化主要体现在第一个if语句之中,即当前delete 的key可能不在最后一层,通过这个函数​

​KeyNotExistsBeyondOutputLevel​

​判断后续的层中没有当前key了, 那么就可以直接删除;这个逻辑会增加一定的CPU比较代价,但是它的收益是可观的,能够在较高的层中将key直接删除。

4. Lethe: A Tunable Delete-Aware LSM Engine . SIGMOD’20

这是一篇 今年顶会 SIGMOD中发表的论文,核心就是LSM 引擎带来的delete问题。

在该篇论文中详细描述了delete 对LSM带来的问题,且现有的引擎没有很好的办法很好得解决大范围删除的问题。

Lethe通过:

  • 增加统计信息和compaction策略,优化sort key大范围删除带来的问题
  • 设计新的storage layout,来提升secondary key的大范围删除。

提出了了两种策略:

  • FADE(fast deletion): 更加激进的compaction调度策略。相比于rocksdb的可配置策略,这里增加了文件的TTL,优先针对达到TTL的文件进行挑选(可以根据TTL主动触发调度),调度compaction删除。
  • KiWi(Key Weaving storage layout): 提高支持非sort key的range delete,牺牲了一定的查询代价,提升了范围删除的效率。这个设计 感觉和rocksdb的DeleteRange有点类似,就是不知道性能差异大吗?

一个X-engine的大佬已经有一篇该论文的详细分享了。

​​Lethe 如何优化 LSM-Tree delete 难题​​