关于Titandb Ratelimiter 失效问题的一个bugfix
本文簡單討論一下在TitanDB 中使用Ratelimiter的一個bug,也算是一個重要bug了,相關fix已經提了PR到tikv 社區了pull-210。
這個問題導致的現象是ratelimiter 在titandb Flush/GC 生成blobfiled的過程中無法生效,也就是無法限制titandb的主要寫 I/O。而我們想要享受Titandb在大value的寫放大紅利,卻會引入巨量的寫帶寬(GC),這個時候對于大多數讀敏感的場景 titandb GC出現時就是一場長尾災難。
而ratelimiter則是這個場景的救星,它能夠有效控制磁盤寫入,降低了大量排隊的寫請求對讀的影響。我們現在使用的非Intel Optane系列的ssd/NVM等設備,請求在磁盤內部其實都是順序處理,也就是寫請求多了,后續跟著的讀請求延時必然會上漲。這個時候,我們能夠有效控制磁盤的寫入帶寬,再加上titandb的GC并非持續性的波峰,而是間斷性得調度,這樣我們通過ratelimiter做一個均衡的限速,將GC出現時的磁盤I/O波峰打平到一段時間內處理完成,這樣我們的讀請求延時就很棒了。
然而,事與愿違,titan的ratelimiter有一些細節上的bug。
使用如下測試腳本:
./titandb_bench \--benchmarks="fillrandom,stats" \--max_background_compactions=32 \--max_background_flushes=4 \--max_write_buffer_number=6 \--target_file_size_base=67108864 \--max_bytes_for_level_base=536870912 \--statistics=true \--stats_dump_period_sec=5 \--num=5000000 \--duration=300 \--threads=10 \--value_size=8192 \--key_size=16 \--key_id_range=10000000 \--enable_pipelined_write=false \--db=./db_bench_test \--wal_dir=./db_bench_test \--num_multi_db=1 \--allow_concurrent_memtable_write=true \--disable_wal=true \ # 寫入的過程中磁盤帶寬僅由flush/GC產生--use_titan=true \ # 使用titandb--titan_max_background_gc=2 \--rate_limiter_bytes_per_sec=134217728 \ # 開啟ratelimiter,限速到128M--rate_limiter_auto_tuned=false
測試1:
測試rocksdb的ratelimiter是否生效 ,將--use_titan=false
能夠看到磁盤寫I/O行為非常穩定得被限制到128M左右:
測試2:
測試titan的ratelimiter是否生效,將--use_titan=true直接打開
可以看到此時IO完全無法有效控制住,I/O線程有high和user線程池,也就是titandb的flush和GC
發現了問題,接下來看看問題原因:
我們上層傳入了ratelimiter,而ratelimiter的調度則在數據寫入具體文件之前調度的,titan這里復用了rocksdb的ratelimiter,也就是ratelimiter的調度最終都會通過同一個入口WritableFileWriter::Append函數。
Titan這里到達這個入口的途徑就是在Flush/GC 創建Blobfile的時候進入的。
void BlobFileBuilder::Add(const BlobRecord& record, BlobHandle* handle) {if (!ok()) return;encoder_.EncodeRecord(record);handle->offset = file_->GetFileSize();handle->size = encoder_.GetEncodedSize();live_data_size_ += handle->size;// 寫blobfilestatus_ = file_->Append(encoder_.GetHeader());if (ok()) {status_ = file_->Append(encoder_.GetRecord());num_entries_++;// The keys added into blob files are in order.if (smallest_key_.empty()) {smallest_key_.assign(record.key.data(), record.key.size());}assert(cf_options_.comparator->Compare(record.key, Slice(smallest_key_)) >=0);assert(cf_options_.comparator->Compare(record.key, Slice(largest_key_)) >=0);largest_key_.assign(record.key.data(), record.key.size());}
}
我們通過systemtap確認一下titandb這里的ratelimiter指針是否為空, 如果為空,那問題就不在writablefile那里了。
!#/bin/stapglobal timesprobe process("/home/test_binary").function("rocksdb::titandb::BlobFileBuilder::Add").call {printf("rate_limiter addr : %x\n ", $file_->ratelimiter_$)
}
發現有地址,且和LOG文件中打出的rate_limiter地址一樣,說明ratelimiter確實是下發到了底層文件寫入這里。
那就繼續深入唄,看看是否執行到了ratelimiter邏輯里面。
這里被titan寫的單測誤導了很久
blob_gc_job_test.cc,他們自己實現了一個ratelimiter的RequestToken,乍一看和rocksdb的RequestToken很像,但少了一個條件,一般人還看不出來:size_t RequestToken(size_t bytes, size_t alignment,Env::IOPriority io_priority, Statistics* stats,RateLimiter::OpType op_type) override {// 少了一個對io_priority 的判斷if (IsRateLimited(op_type)) {if (op_type == RateLimiter::OpType::kRead) {read = true;} else {write = true;}}return bytes; }
因為這個單測除了少了一個判斷之外,其他邏輯都沒有問題,結果老是認為問題出在了RequestToken上某一個函數里,可能是從Append到RequestToken之間的某一個邏輯沒有進入到,也就是無法進入到實際的RequestToken里面。
然而抓遍了中間部分函數的調用棧,人正常的邏輯,,,沒有絲毫問題,通過Append進入之后需要不斷填充一個buffer,當這個buffer達到1M之后(可以通過參數writable_file_max_buffer_size配置)會調用一次WritableFileWriter::Flush,沒有direct_io的配置的話這里面必然會進入到WriteBufferred函數中,和rocksdb的邏輯一毛一樣。。。wtf
萬般無奈,只能回到RequestToken邏輯中了,stap打印了一下進入函數之后的各個參數的值。。。發現io_priority為啥大多數是2,偶爾是0/1。。。而rocksdb都是0/1,顯然2肯定是無法進入到實際的Request邏輯的,被屏蔽在了外面。
如下是rocksdb的令牌桶限速入口:
size_t RateLimiter::RequestToken(size_t bytes, size_t alignment,Env::IOPriority io_priority, Statistics* stats,RateLimiter::OpType op_type) {// 必須保證io_priority < 2才能實際進入到 Request邏輯if (io_priority < Env::IO_TOTAL && IsRateLimited(op_type)) {bytes = std::min(bytes, static_cast<size_t>(GetSingleBurstBytes()));if (alignment > 0) {// Here we may actually require more than burst and block// but we can not write less than one page at a time on direct I/O// thus we may want not to use ratelimiterbytes = std::max(alignment, TruncateToPageBoundary(alignment, bytes));}Request(bytes, io_priority, stats, op_type);}return bytes;
}
問題顯然出現在了io_priority這里,然后大概看了一下什么時候會對io_priority進行賦值。
它描述的是一個文件被ratelimiter拿到的時候該以什么樣的優先級處理,如果設置的是高優先級IO_HIGH,則ratelimiter會優先滿足這個文件的寫入,不會限速得太狠;如果是IO_LOW則會盡可能得對它進行限速;而IO_HIGH則是文件創建時的默認優先級, 不會進行任何限速。
WritableFile(): last_preallocated_block_(0),preallocation_block_size_(0),io_priority_(Env::IO_TOTAL),write_hint_(Env::WLTH_NOT_SET),strict_bytes_per_sync_(false) {}
所以,rocksdb實際會在compaction/Flush 創建sst文件的時候對他們進行各自的優先級賦值,保證能夠被限速。
邏輯分別在WriteL0Table–>BuildTable 和 OpenCompactionOutputFile–>writable_file->SetIOPriority(Env::IO_LOW)中,然而我們在titan中的主體IO在blobfile的寫入上,也就是創建Blobfile 的handle之后需要對blobfile的io_priority進行設置,才能保證ratelimiter能夠拿到有效的I/O優先級。
看一下titan的FileManager::NewFile的邏輯:
Status NewFile(std::unique_ptr<BlobFileHandle>* handle) override {auto number = db_->blob_file_set_->NewFileNumber();auto name = BlobFileName(db_->dirname_, number);Status s;std::unique_ptr<WritableFileWriter> file;{std::unique_ptr<WritableFile> f;s = db_->env_->NewWritableFile(name, &f, db_->env_options_);if (!s.ok()) return s;file.reset(new WritableFileWriter(std::move(f), name, db_->env_options_));}handle->reset(new FileHandle(number, name, std::move(file)));{MutexLock l(&db_->mutex_);db_->pending_outputs_.insert(number);}return s;}
到這里基本就清楚問題的原因了,顯然titan創建blobfile并沒有添加有效的io_priority,而且ratelimiter的單測寫的不夠嚴謹導致誤導了很多人。
修復的話可以之間看這個pull-210 就可以了。
總結
以上是生活随笔為你收集整理的关于Titandb Ratelimiter 失效问题的一个bugfix的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Rocksdb 通过ingestfile
- 下一篇: 穆图最后一关怎么打