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
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5yMxkDMxAzY5EmN2IzM4QjNzYzX4MjMxETM0AzLcFTMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
問題分析
這裡顯然不合理,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的寫入邏輯。
- Key-value 都和以前rocksdb一樣,先寫入memtable
- 在Flush過程中形成sst檔案的時候,通過titan自己的table-builder add的過程中來做區分,大于一個門檻值時 分離value寫入到blobfile中,key+key-index 存放到LSM-tree 的sst檔案中
- 後續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傳回去。