关于Titandb Ratelimiter 失效问题的一个bugfix
本文簡(jiǎn)單討論一下在TitanDB 中使用Ratelimiter的一個(gè)bug,也算是一個(gè)重要bug了,相關(guān)fix已經(jīng)提了PR到tikv 社區(qū)了pull-210。
這個(gè)問(wèn)題導(dǎo)致的現(xiàn)象是ratelimiter 在titandb Flush/GC 生成blobfiled的過(guò)程中無(wú)法生效,也就是無(wú)法限制titandb的主要寫 I/O。而我們想要享受Titandb在大value的寫放大紅利,卻會(huì)引入巨量的寫帶寬(GC),這個(gè)時(shí)候?qū)τ诖蠖鄶?shù)讀敏感的場(chǎng)景 titandb GC出現(xiàn)時(shí)就是一場(chǎng)長(zhǎng)尾災(zāi)難。
而ratelimiter則是這個(gè)場(chǎng)景的救星,它能夠有效控制磁盤寫入,降低了大量排隊(duì)的寫請(qǐng)求對(duì)讀的影響。我們現(xiàn)在使用的非Intel Optane系列的ssd/NVM等設(shè)備,請(qǐng)求在磁盤內(nèi)部其實(shí)都是順序處理,也就是寫請(qǐng)求多了,后續(xù)跟著的讀請(qǐng)求延時(shí)必然會(huì)上漲。這個(gè)時(shí)候,我們能夠有效控制磁盤的寫入帶寬,再加上titandb的GC并非持續(xù)性的波峰,而是間斷性得調(diào)度,這樣我們通過(guò)ratelimiter做一個(gè)均衡的限速,將GC出現(xiàn)時(shí)的磁盤I/O波峰打平到一段時(shí)間內(nèi)處理完成,這樣我們的讀請(qǐng)求延時(shí)就很棒了。
然而,事與愿違,titan的ratelimiter有一些細(xì)節(jié)上的bug。
使用如下測(cè)試腳本:
./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 \ # 寫入的過(guò)程中磁盤帶寬僅由flush/GC產(chǎn)生--use_titan=true \ # 使用titandb--titan_max_background_gc=2 \--rate_limiter_bytes_per_sec=134217728 \ # 開(kāi)啟ratelimiter,限速到128M--rate_limiter_auto_tuned=false
測(cè)試1:
測(cè)試rocksdb的ratelimiter是否生效 ,將--use_titan=false
能夠看到磁盤寫I/O行為非常穩(wěn)定得被限制到128M左右:
測(cè)試2:
測(cè)試titan的ratelimiter是否生效,將--use_titan=true直接打開(kāi)
可以看到此時(shí)IO完全無(wú)法有效控制住,I/O線程有high和user線程池,也就是titandb的flush和GC
發(fā)現(xiàn)了問(wèn)題,接下來(lái)看看問(wèn)題原因:
我們上層傳入了ratelimiter,而ratelimiter的調(diào)度則在數(shù)據(jù)寫入具體文件之前調(diào)度的,titan這里復(fù)用了rocksdb的ratelimiter,也就是ratelimiter的調(diào)度最終都會(huì)通過(guò)同一個(gè)入口WritableFileWriter::Append函數(shù)。
Titan這里到達(dá)這個(gè)入口的途徑就是在Flush/GC 創(chuàng)建Blobfile的時(shí)候進(jìn)入的。
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());}
}
我們通過(guò)systemtap確認(rèn)一下titandb這里的ratelimiter指針是否為空, 如果為空,那問(wèn)題就不在writablefile那里了。
!#/bin/stapglobal timesprobe process("/home/test_binary").function("rocksdb::titandb::BlobFileBuilder::Add").call {printf("rate_limiter addr : %x\n ", $file_->ratelimiter_$)
}
發(fā)現(xiàn)有地址,且和LOG文件中打出的rate_limiter地址一樣,說(shuō)明ratelimiter確實(shí)是下發(fā)到了底層文件寫入這里。
那就繼續(xù)深入唄,看看是否執(zhí)行到了ratelimiter邏輯里面。
這里被titan寫的單測(cè)誤導(dǎo)了很久
blob_gc_job_test.cc,他們自己實(shí)現(xiàn)了一個(gè)ratelimiter的RequestToken,乍一看和rocksdb的RequestToken很像,但少了一個(gè)條件,一般人還看不出來(lái):size_t RequestToken(size_t bytes, size_t alignment,Env::IOPriority io_priority, Statistics* stats,RateLimiter::OpType op_type) override {// 少了一個(gè)對(duì)io_priority 的判斷if (IsRateLimited(op_type)) {if (op_type == RateLimiter::OpType::kRead) {read = true;} else {write = true;}}return bytes; }
因?yàn)檫@個(gè)單測(cè)除了少了一個(gè)判斷之外,其他邏輯都沒(méi)有問(wèn)題,結(jié)果老是認(rèn)為問(wèn)題出在了RequestToken上某一個(gè)函數(shù)里,可能是從Append到RequestToken之間的某一個(gè)邏輯沒(méi)有進(jìn)入到,也就是無(wú)法進(jìn)入到實(shí)際的RequestToken里面。
然而抓遍了中間部分函數(shù)的調(diào)用棧,人正常的邏輯,,,沒(méi)有絲毫問(wèn)題,通過(guò)Append進(jìn)入之后需要不斷填充一個(gè)buffer,當(dāng)這個(gè)buffer達(dá)到1M之后(可以通過(guò)參數(shù)writable_file_max_buffer_size配置)會(huì)調(diào)用一次WritableFileWriter::Flush,沒(méi)有direct_io的配置的話這里面必然會(huì)進(jìn)入到WriteBufferred函數(shù)中,和rocksdb的邏輯一毛一樣。。。wtf
萬(wàn)般無(wú)奈,只能回到RequestToken邏輯中了,stap打印了一下進(jìn)入函數(shù)之后的各個(gè)參數(shù)的值。。。發(fā)現(xiàn)io_priority為啥大多數(shù)是2,偶爾是0/1。。。而rocksdb都是0/1,顯然2肯定是無(wú)法進(jìn)入到實(shí)際的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才能實(shí)際進(jìn)入到 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;
}
問(wèn)題顯然出現(xiàn)在了io_priority這里,然后大概看了一下什么時(shí)候會(huì)對(duì)io_priority進(jìn)行賦值。
它描述的是一個(gè)文件被ratelimiter拿到的時(shí)候該以什么樣的優(yōu)先級(jí)處理,如果設(shè)置的是高優(yōu)先級(jí)IO_HIGH,則ratelimiter會(huì)優(yōu)先滿足這個(gè)文件的寫入,不會(huì)限速得太狠;如果是IO_LOW則會(huì)盡可能得對(duì)它進(jìn)行限速;而IO_HIGH則是文件創(chuàng)建時(shí)的默認(rèn)優(yōu)先級(jí), 不會(huì)進(jìn)行任何限速。
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實(shí)際會(huì)在compaction/Flush 創(chuàng)建sst文件的時(shí)候?qū)λ麄冞M(jìn)行各自的優(yōu)先級(jí)賦值,保證能夠被限速。
邏輯分別在WriteL0Table–>BuildTable 和 OpenCompactionOutputFile–>writable_file->SetIOPriority(Env::IO_LOW)中,然而我們?cè)趖itan中的主體IO在blobfile的寫入上,也就是創(chuàng)建Blobfile 的handle之后需要對(duì)blobfile的io_priority進(jìn)行設(shè)置,才能保證ratelimiter能夠拿到有效的I/O優(yōu)先級(jí)。
看一下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;}
到這里基本就清楚問(wèn)題的原因了,顯然titan創(chuàng)建blobfile并沒(méi)有添加有效的io_priority,而且ratelimiter的單測(cè)寫的不夠嚴(yán)謹(jǐn)導(dǎo)致誤導(dǎo)了很多人。
修復(fù)的話可以之間看這個(gè)pull-210 就可以了。
總結(jié)
以上是生活随笔為你收集整理的关于Titandb Ratelimiter 失效问题的一个bugfix的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Rocksdb 通过ingestfile
- 下一篇: git 对之前的commit 进行重新签