天天看點

TitanDB 中使用Compaction Filter ,産生了預期之外幾十倍的讀I/O

Compaction過程中 産生大量讀I/O 的背景

項目中因大value 需求,引入了PingCap 參考​

​Wisckey​

​ 思想實作的key-value分離存儲 titan, 使用過程中因為有用到Rocksdb本身的 CompactionFilter功能,是以就直接用TitanDB的option 傳入了compaction filter。

使用過程中,單純的通過​

​db->Put​

​接口寫入 就會發現磁盤上大量的讀I/O。

Ps : 相關的現象産生時的基本配置就不貼上來了,這個現象用過titan的compaction filter的同學應該都會比較清楚。

如果沒有用過,但也發現了一些異常,也可以直接向後看。

我們資料寫入量是 key:10B, value: 8K , 磁盤上的讀本身是由于compaction引起的,compaction過程中需要将選擇的sst檔案中的key-value通過疊代器一個一個讀取上來做堆排序。這個過程會産生讀I/O,也就是隻有Compaction 本身會有讀I/O。

問題現象是單次compaction的量也就幾十M,但磁盤上卻産生了數百M的讀I/O。

更加直覺的展現就是通過指令​

​sudo iotop​

​,可以看到此時大量的compaction 線程産生了讀IO

TitanDB 中使用Compaction Filter ,産生了預期之外幾十倍的讀I/O

問題分析

這裡顯然不合理,rocksdb的日志列印出來的LOG 中總共的compaction的帶寬也就幾十M,因為在titandb的key-value分離存儲之後LSM-tree中僅僅存儲了key和key-index,是以單次compaction的過程中理論上并不會攜帶着value參與,這樣的大量I/O不太合理。

繼續向下看,從​

​iotop​

​​的輸出中取出來一個compaction的線程ID,​

​sudo strace -ttt -T -p 209278​

​​ 抓取它的系統調用,可以看到大量的​

​pread64​

​系統調用

1617853714.972743 pread64(14224, "\203\250\206p\20/\0\0\0\fuid:11288154\201^\365.\0\0\n\362]\22"..., 8190, 13057621) = 12057 <0.000445>
1617853714.973241 pread64(13772, "\357\212\255y\20/\0\0\0\fuid:11288198\201^\365.\0\0\n\362]\22"..., 8190, 3267429) = 12057 <0.000013>
1617853714.973284 pread64(15591, "\343\3602\373\20/\0\0\0\fuid:11288239\201^\365.\0\0\n\362]\22"..., 8190, 3279482) = 12057 <0.000230>
...      

可以看到pread64讀到的資料大小是​

​8190B​

​,顯然是我們寫入的value大小,這貨肯定讀了存放value的blobfile

随機抽樣幾個fd ,也就是​

​pread64(14224, "\20...)​

​​的第一參數,從程序的fd清單中看看它連結得是哪個檔案​

​ls -l /proc/xxx/fd | grep 209278​

lr-x------ 1 kiwi2 kiwi2 64 Apr  8 11:49 /proc/209235/fd/10029 -> /mnt/db/14/titandb/000681.blob      

果然是從blobfile中讀取的資料,到這裡我們就知道為什麼compaction線程會有這麼多的讀,因為compaction過程中竟然讀了blob file中的value。。。陷入沉思,梳理一下titan的寫入邏輯。

TitanDB 中使用Compaction Filter ,産生了預期之外幾十倍的讀I/O
  1. Key-value 都和以前rocksdb一樣,先寫入memtable
  2. 在Flush過程中形成sst檔案的時候,通過titan自己的table-builder add的過程中來做區分,大于一個門檻值時 分離value寫入到blobfile中,key+key-index 存放到LSM-tree 的sst檔案中
  3. 後續LSM-tree繼續自己的compaction, blobfiles 則在達到觸發gc條件的時候由一個線程池的一個線程排程blobfile的過期清理

也就是titan compaction過程中理論上僅僅是sst檔案中的key + key-index參與,并不會涉及blobfiles 中的value,要不然key-value分離的意義何在?帶寬還是沒有降下來。

接下來的分析就更加明了了,看看這個時候大量讀的compaction線程調用棧,直接上指令​

​sudo pstack 209278​

​(pstack底層也是調用gdb 執行的,不過是quiet指令執行,并不會阻塞線程),最後能看到如下調用棧

#0  0x00007faa05d93f73 in pread64 () from /lib64/libpthread.so.0
#1  0x000000000095f85e in pread (__offset=16132142, __nbytes=12057, __buf=0x3c074a000, __fd=<optimized out>)
#2  rocksdb::PosixRandomAccessFile::Read(unsigned long, unsigned long, rocksdb::Slice*, char*) const ()
#3  0x0000000000a0c0b1 in rocksdb::RandomAccessFileReader::Read(unsigned long, unsigned long, rocksdb::Slice*, char*, bool) const ()
#4  0x000000000081b197 in rocksdb::titandb::BlobFileReader::ReadRecord(rocksdb::titandb::BlobHandle const&, rocksdb::titandb::BlobRecord*, rocksdb::titandb::OwnedSlice*) ()
#5  0x000000000081ba21 in rocksdb::titandb::BlobFileReader::Get(rocksdb::ReadOptions const&, rocksdb::titandb::BlobHandle const&, rocksdb::titandb::BlobRecord*, rocksdb::PinnableSlice*) ()
#6  0x00000000008428e3 in rocksdb::titandb::BlobFileCache::Get(rocksdb::ReadOptions const&, unsigned long, unsigned long, rocksdb::titandb::BlobHandle const&, rocksdb::titandb::BlobRecord*, rocksdb::PinnableSlice*) ()
#7  0x00000000008396b8 in rocksdb::titandb::BlobStorage::Get(rocksdb::ReadOptions const&, rocksdb::titandb::BlobIndex const&, rocksdb::titandb::BlobRecord*, rocksdb::PinnableSlice*) ()
#8  0x00000000007f4a3b in rocksdb::titandb::TitanCompactionFilter::FilterV2 (this=0x3ceb05b00, level=0, key=..., value_type=<optimized out>, value=..., new_value=0x1be783cf8, skip_until=0x1be783d18)
#9  0x0000000000a2fa1a in InvokeFilterIfNeeded (skip_until=0x7fa9f786e730, need_skip=0x7fa9f786e72f, this=0x1be783b00)
#10 rocksdb::CompactionIterator::InvokeFilterIfNeeded (this=0x1be783b00, need_skip=0x7fa9f786e72f, skip_until=0x7fa9f786e730)
#11 0x0000000000a3039a in rocksdb::CompactionIterator::NextFromInput() ()
#12 0x0000000000a31c5a in rocksdb::CompactionIterator::Next (this=0x1be783b00)
#13 0x0000000000a39658 in rocksdb::CompactionJob::ProcessKeyValueCompaction(rocksdb::CompactionJob::SubcompactionState*) ()
#14 0x0000000000a3aa1c in rocksdb::CompactionJob::Run() ()
#15 0x0000000000887a5b in rocksdb::DBImpl::BackgroundCompaction(bool*, rocksdb::JobContext*, rocksdb::LogBuffer*, rocksdb::DBImpl::PrepickedCompaction*, rocksdb::Env::Priority) ()
#16 0x000000000088ab44 in rocksdb::DBImpl::BackgroundCallCompaction(rocksdb::DBImpl::PrepickedCompaction*, rocksdb::Env::Priority) ()
#17 0x000000000088b028 in rocksdb::DBImpl::BGWorkCompaction (arg=<optimized out>)
#18 0x0000000000a1437c in operator() (this=0x7fa9f7870370)
#19 rocksdb::ThreadPoolImpl::Impl::BGThread(unsigned long) ()
#20 0x0000000000a144d3 in rocksdb::ThreadPoolImpl::Impl::BGThreadWrapper (arg=<optimized out>)      

這個調用棧中可以看到

#11 0x0000000000a3039a in rocksdb::CompactionIterator::NextFromInput() ()
#12 0x0000000000a31c5a in rocksdb::CompactionIterator::Next (this=0x1be783b00)
#13 0x0000000000a39658 in rocksdb::CompactionJob::ProcessKeyValueCompaction(rocksdb::CompactionJob::SubcompactionState*) ()
#14 0x0000000000a3aa1c in rocksdb::CompactionJob::Run() ()      

這一些都是正常的compaction邏輯,但是再往上走就進入了compaction filter之中,使用了Titandb的filter函數,并且調用了​

​rocksdb::titandb::BlobStorage::Get​

​​,确實,我們使用者态用了compaction filter,但不應該調用到blob的Get,好吧。。。

直接看Titan的源代碼。

Titan的Compaction Filter實作

在打開TitanDB的時候會将使用者傳入的compaction_filter作為一個子filter傳進來,并且交給titan自己的​

​TitanCompactionFilterFactory​

​來處理

Status TitanDBImpl::OpenImpl(const std::vector<TitanCFDescriptor>& descs,
                             std::vector<ColumnFamilyHandle*>* handles) {
  ......
  std::vector<ColumnFamilyDescriptor> base_descs;
  std::vector<std::shared_ptr<TitanTableFactory>> titan_table_factories;
  for (auto& desc : descs) {
    ......
    if (cf_opts.compaction_filter != nullptr ||
        cf_opts.compaction_filter_factory != nullptr) {
      std::shared_ptr<TitanCompactionFilterFactory> titan_cf_factory =
          std::make_shared<TitanCompactionFilterFactory>(
              cf_opts.compaction_filter, cf_opts.compaction_filter_factory,
              this, desc.options.skip_value_in_compaction_filter, desc.name);
      cf_opts.compaction_filter = nullptr;
      cf_opts.compaction_filter_factory = titan_cf_factory;
    }
  }
  // Open base DB.
  s = DB::Open(db_options_, dbname_, base_descs, handles, &db_);\
  ......
}      

進入​

​TitanCompactionFilterFactory​

​​的​

​CreateCompactionFilter​

​​函數

之前介紹Rocksdb的ComapctionFilter實作的時候知道,引擎對外暴漏了這一些接口,能夠由使用者來指定自己想要過濾什麼樣的key。

​​Rocskdb CompactionFilter實作​​

std::unique_ptr<CompactionFilter> CreateCompactionFilter(
  const CompactionFilter::Context &context) override {
  assert(original_filter_ != nullptr || original_filter_factory_ != nullptr);

  std::shared_ptr<BlobStorage> blob_storage;
  {
    MutexLock l(&titan_db_impl_->mutex_);
    blob_storage = titan_db_impl_->blob_file_set_
      ->GetBlobStorage(context.column_family_id)
      .lock();
  }
  if (blob_storage == nullptr) {
    assert(false);
    // Shouldn't be here, but ignore compaction filter when we hit error.
    return nullptr;
  }

  const CompactionFilter *original_filter = original_filter_;
  std::unique_ptr<CompactionFilter> original_filter_from_factory;
  if (original_filter == nullptr) {
    original_filter_from_factory =
      original_filter_factory_->CreateCompactionFilter(context);
    original_filter = original_filter_from_factory.get();
  }
  return std::unique_ptr<CompactionFilter>(new TitanCompactionFilter(
    titan_db_impl_, cf_name_, original_filter,
    std::move(original_filter_from_factory), blob_storage, skip_value_));
}      

Factory會将​

​TitanCompactionFilter​

​​傳回,且這個filter也攜帶着使用者自定義的Filter​

​original_filter​

​​。也就是comapction 過程中會先執行​

​TitanCompactionFilter​

​​的​

​FilterV2​

​​函數,接着看一下titandb 的​

​FilterV2​

​函數:

Decision FilterV2(int level, const Slice &key, ValueType value_type,
                    const Slice &value, std::string *new_value,
                    std::string *skip_until) const override {
    ......
    BlobRecord record;
    PinnableSlice buffer;
    ReadOptions read_options;
    // 問題源頭
    s = blob_storage_->Get(read_options, blob_index, &record, &buffer);

    if (s.IsCorruption()) {
      // Could be cause by blob file beinged GC-ed, or real corruption.
      // TODO(yiwu): Tell the two cases apart.
      return Decision::kKeep;
    } else if (s.ok()) {
        // 使用者自定義的Filter邏輯
      auto decision = original_filter_->FilterV2(
          level, key, kValue, record.value, new_value, skip_until);
       ...
    }
}      

可以看到這裡會有一個​

​blob_storage_->Get​

​,到此我們就知道為什麼會有一個blobfile 的Get了。

因為使用者在回掉使用​

​original_filter_->FilterV2​

​邏輯的時候需要知道具體的value,是以Titan這裡需要将blobfile中的value傳回去。

解決