Rocksdb 内存“不释放”问题 分析
文章目錄
- 問(wèn)題場(chǎng)景描述
- 問(wèn)題復(fù)現(xiàn)
- 編寫(xiě)隨機(jī)寫(xiě) 測(cè)試工具
- 使用工具抓取內(nèi)存分配過(guò)程
- 源碼分析
- memtable邏輯
- table_cache邏輯
- 總結(jié)
整體的IO場(chǎng)景到底層的源碼分析過(guò)程如上導(dǎo)圖,接下來(lái)將詳細(xì)闡述具體的過(guò)程。
問(wèn)題場(chǎng)景描述
我們的rocksdb作為單機(jī)存儲(chǔ)引擎,跑在用分布式一致性協(xié)議raft 封裝的一個(gè)分布式存儲(chǔ)集群之上。基本的IO架構(gòu)圖如下:
針對(duì)該分布式存儲(chǔ)集群,上層使用的是隨機(jī)IO ,即每個(gè)raft交給rocksdb的請(qǐng)求所轉(zhuǎn)化的key都是隨機(jī)的。此時(shí),rocksdb底層當(dāng)然調(diào)用的是put的接口來(lái)持久化key-value數(shù)據(jù)。
問(wèn)題現(xiàn)象是(同事給出的,我們只看到一個(gè)結(jié)果) 隨著IO的持續(xù)寫(xiě)入,大概每個(gè)節(jié)點(diǎn)rocksdb數(shù)據(jù)的存儲(chǔ)量都達(dá)到20G以上之后,top看到的IO 進(jìn)程物理內(nèi)存資源和實(shí)際的抓取的rocksdb tcmalloc分配的堆內(nèi)存大小無(wú)法匹配,差距達(dá)到2-3倍。這個(gè)時(shí)候?yàn)榱伺懦齬aft對(duì)內(nèi)存消耗的影響,他將raft的寫(xiě)log邏輯去掉,IO僅僅經(jīng)過(guò)協(xié)議棧到達(dá)底層rocksdb,但是他看到的日志以及內(nèi)存占用仍然還是無(wú)法匹配,且內(nèi)存持續(xù)增大無(wú)法釋放,是不是rocksdb內(nèi)部的存在內(nèi)存泄露?
問(wèn)題復(fù)現(xiàn)
業(yè)務(wù)場(chǎng)景 也就是隨機(jī)put,且每次都必先,那么復(fù)現(xiàn)就很簡(jiǎn)單了,那單獨(dú)的rocksdb來(lái)進(jìn)行隨機(jī)寫(xiě)測(cè)試,并抓取內(nèi)存分布情況。
編寫(xiě)隨機(jī)寫(xiě) 測(cè)試工具
這里說(shuō)明一下為什么不實(shí)用rocksdb原生的db_bench進(jìn)行測(cè)試,它功能更多,配置更強(qiáng)。
但是我們想要打印我們自己想看的東西,且排除它自己工具本身接口過(guò)多而產(chǎn)生的干擾,所以就直接自己寫(xiě)一個(gè)小工具,方便易用,抓取內(nèi)存信息更為方便。
使用put接口進(jìn)行隨機(jī)寫(xiě) 測(cè)試工具的封裝,以下代碼提供如下功能
- 指定隨機(jī)寫(xiě) 請(qǐng)求的個(gè)數(shù)
- 指定 key的范圍,默認(rèn)隨機(jī)
- 指定value的大小
- 指定rocksdb compaction線程數(shù)
- 指定 put的客戶端線程數(shù)
實(shí)現(xiàn)工具如下:
#ifndef __UTIL_H__
#define __UTIL_H__ #include <ctime>
#include <cstdio>
#include <cstdlib>
#include <cassert>
#include <thread>
#include <mutex>#include <sys/time.h>
#include <unistd.h>
#include <signal.h>
#include <gperftools/malloc_extension.h>#include <iostream>
#include <string>
#include <vector>#include "rocksdb/cache.h"
#include "rocksdb/db.h"
#include "rocksdb/slice.h"
#include "rocksdb/options.h"
#include "rocksdb/table.h"
#include "rocksdb/trace_reader_writer.h"
#include "rocksdb/compaction_filter.h"#define LEN 2048
static std::string NEW_VALUE = "NewValue";using namespace std;class ChangeFilter : public rocksdb::CompactionFilter {public:explicit ChangeFilter() {}bool Filter(int /*level*/, const rocksdb::Slice& /*key*/, const rocksdb::Slice& /*value*/,std::string* new_value, bool* value_changed) const override {assert(new_value != nullptr);*new_value = NEW_VALUE;*value_changed = true;return false;}const char* Name() const override { return "ChangeFilter"; }
};/*compaction filter*/
class ChangeFilterFactory : public rocksdb::CompactionFilterFactory {public:explicit ChangeFilterFactory() {}std::unique_ptr<rocksdb::CompactionFilter> CreateCompactionFilter(const rocksdb::CompactionFilter::Context& /*context*/) override {return std::unique_ptr<rocksdb::CompactionFilter>(new ChangeFilter());}const char* Name() const override { return "ChangeFilterFactory"; }
};/*input args*/
static long db_count = 1; // database num
static long test_count; // request num per database
static long key_range; // key range
static long value_size = 100; // value size
static long compaction_num = 32; // background compaction num,if 0, with no compaction
static long thread_num = 8; // client thread numconst size_t long_value_len = 5 * 1024 * 1024;
static string long_value(long_value_len, 'x');mutex g_count_mutex;
static long req_num = 0;static long db_no = -1;static long parse_long(const char *s)
{char *end_ptr = nullptr;long v = strtol(s, &end_ptr, 10);assert(*s != '\0' && *end_ptr == '\0');return v;
}static double now()
{struct timeval t;gettimeofday(&t, NULL);return t.tv_sec + t.tv_usec / 1e6;
}static string long_to_str(long n)
{char s[30];sprintf(s, "%ld", n);return string(s);
}/*
初始化傳入的參數(shù):
1. 請(qǐng)求個(gè)數(shù)
2. key的范圍
3. value的大小(默認(rèn)100B)
4. rocksdb后臺(tái)compaction線程數(shù)
5. 客戶端壓put的線程數(shù)
6. 數(shù)據(jù)庫(kù)的個(gè)數(shù)(指定多少個(gè)db)
*/
static void init(int argc, char *argv[])
{assert(argc == 7);test_count = parse_long(argv[1]);key_range = parse_long(argv[2]);value_size = parse_long(argv[3]);compaction_num = parse_long(argv[4]);thread_num = parse_long(argv[5]);db_count = parse_long(argv[6]);if (key_range == 0){key_range = 1L << 62;}assert(db_count > 0 && db_count <= 20 && test_count > 0 && key_range > 0 && value_size > 0);for (long i = 0; i < db_count; ++ i){pid_t pid = fork();assert(pid >= 0);if (pid == 0){//childsignal(SIGHUP, SIG_IGN);db_no = i;break;}}if (db_no < 0){//parentsleep(1);exit(0);}srand((long)(now() * 1e6) % 100000000);
}/*生成隨機(jī)key*/
static string rand_key()
{char s[30];unsigned long long n = 1;for (int i = 0; i < 4; ++ i){n *= (unsigned long long)rand();}sprintf(s, "%llu", n % (unsigned long long)key_range);string k(s);return k;
}static void set_value()
{assert(long_value.size() == long_value_len);for (size_t i = 0; i < long_value_len; ++ i){long_value[i] = (unsigned char)(rand() % 255 + 1);}}//多線程壓數(shù)據(jù)庫(kù)
template <class DB>
void put_thread(long thread_id, DB *db, string db_full_name, double ts,std::shared_ptr<rocksdb::Cache> cache) {while(1) {const size_t value_slice_len = value_size;char buff[2048]; memset(buff,0,sizeof(char )*2048 + 1);/*生成指定大小的value*/rocksdb::Slice rand_value(long_value.data() + rand() % (long_value_len - value_slice_len), value_slice_len);rocksdb::Status s = db->Put(rocksdb::WriteOptions(), rand_key(), rand_value);g_count_mutex.lock(); req_num ++;g_count_mutex.unlock(); if (!s.ok()){cerr << "Put failed: " << s.ToString() << endl;exit(1);}/*當(dāng)線程編號(hào)為10時(shí)打印統(tǒng)計(jì)的信息*/if (thread_id == 10 && req_num % 10000 == 0){double tm = now() - ts;/*統(tǒng)計(jì)當(dāng)前的IO效率*/printf("thread_id %ld %s: time=%.2f, count=%ld, speed=%.2f\n", \thread_id, db_full_name.c_str(), tm, req_num, req_num / tm);/*打印rocksdb內(nèi)部統(tǒng)計(jì)的 內(nèi)存占用指標(biāo)*/db->GetProperty("rocksdb.block-cache-usage", &out);//rocksdb blockcache 組件占用內(nèi)存情況fprintf(stdout, "rocksdb.block-cache-usage : %s\n", out.c_str());db->GetProperty("rocksdb.estimate-table-readers-mem", &out);fprintf(stdout, "rocksdb.estimate-table-readers-mem : %s\n", out.c_str());db->GetProperty("rocksdb.size-all-mem-tables", &out); //主要是看這個(gè)指標(biāo),代表所有的memtable內(nèi)存占用情況fprintf(stdout, "rocksdb.size-all-mem-tables : %s\n", out.c_str());//tcmalloc statsMallocExtension::instance()->GetStats(buff,2048);fprintf(stdout, "simple_examples heap stats is : %s\n", buff);fflush(stdout);}//總的IO請(qǐng)求達(dá)到了參數(shù)設(shè)置的請(qǐng)求個(gè)數(shù),所有線程停止寫(xiě)入if(req_num >= test_count) {delete db;break;}}
}template <class DB, class OPT>
static void do_run(const string &db_name)
{/*初始化option選項(xiàng)的配置*/OPT options;options.create_if_missing = true;options.stats_dump_period_sec = 30;options.env->SetBackgroundThreads(32);options.OptimizeLevelStyleCompaction();options.allow_concurrent_memtable_write=true ;options.enable_pipelined_write=true ;options.compaction_filter_factory.reset(new ChangeFilterFactory()) ;string db_full_name = db_name + "_" + long_to_str(db_no);printf("%s: db_count=%ld, test_count=%ld, key_range=%ld\n", db_full_name.c_str(), db_count, test_count, key_range);DB *db;if(compaction_num == 0) {options.compaction_style = rocksdb::CompactionStyle::kCompactionStyleNone;} else {options.max_background_compactions = compaction_num; }//cache inistd::shared_ptr<rocksdb::Cache> cache = rocksdb::NewLRUCache(1024L * 1024L * 1024L);rocksdb::BlockBasedTableOptions table_options;table_options.block_cache = cache;options.table_factory.reset(NewBlockBasedTableFactory(table_options));rocksdb::Status status = DB::Open(options, string("./db/") + db_full_name, &db);if (!status.ok()){cerr << "open db failed: " << status.ToString() << endl;exit(1);}double ts = now();set_value();for (long id = 0;id < thread_num; ++id ) {std::thread t(put_thread<DB>, id, db, db_full_name,ts,cache);t.detach();}printf("\n");sleep(40); //等待最后的stat dump輸出
}#endif
引用工具時(shí)只需要調(diào)用以上init和do_run兩個(gè)函數(shù),傳入db名稱即可
#include "util.h"int main(int argc, char *argv[])
{init(argc, argv);do_test<rocksdb::DB, rocksdb::Options>("rocksdb");
}
以上代碼在邏輯中已經(jīng)增加了tcmalloc 的MallocExtension::instance()->GetStats(buff,2048);接口,可以打印tcmalloc的狀態(tài)信息。
使用該接口時(shí)頭文件需要指定#include <gperftools/malloc_extension.h>,編譯選項(xiàng)之中需要加入-ltcmalloc,系統(tǒng)找不到tcmalloc的動(dòng)態(tài)庫(kù),則需要制定動(dòng)態(tài)庫(kù)的加載路徑env LD_PRELOAD="/usr/lib/libtcmalloc.so",關(guān)于gperftools的使用配置詳細(xì)可以參考gperftools
使用工具抓取內(nèi)存分配過(guò)程
-
valgrind + massif
這里很簡(jiǎn)單,使用如下命令讓進(jìn)程啟動(dòng):
valgrind --tools=massif ./test_tools 10000000 0 256 32 100 1
關(guān)于valgrind的詳細(xì)使用可以參考valgrind,這里在運(yùn)行過(guò)程中massif會(huì)做很多次當(dāng)前進(jìn)程占用的物理內(nèi)存快照,并且其中會(huì)有詳細(xì)快照,即進(jìn)程物理內(nèi)存分配過(guò)程中的一個(gè)函數(shù)層級(jí)調(diào)用棧。valigrind默認(rèn)抓取的是堆內(nèi)存,如需要抓取mmap之類的匿名頁(yè)分配的內(nèi)存,需要指定對(duì)應(yīng)的參數(shù)。運(yùn)行一段時(shí)間之后終止進(jìn)程,會(huì)在當(dāng)前目錄下生成一個(gè).out文件,使用ms_print查看文件內(nèi)容
結(jié)果類似如下
這里需要注意massif打印的并不是內(nèi)存沒(méi)有釋放的,只是當(dāng)前時(shí)刻進(jìn)程物理內(nèi)存的一個(gè)分布,但我們?nèi)匀荒軌蚩吹揭恍┯捎诘膬?nèi)存占用信息,一個(gè)是memtable創(chuàng)建的時(shí)候調(diào)用arena分配器分配的內(nèi)存,還有一個(gè)是blockcache 存儲(chǔ)解壓縮數(shù)據(jù)的一個(gè)調(diào)用棧。
為了讓數(shù)據(jù)更加全面準(zhǔn)確,我們也使用gperf工具進(jìn)行進(jìn)程堆內(nèi)存分配的一個(gè)數(shù)據(jù)收集。
-
gperf profiling + pprof數(shù)據(jù)收集
我們使用如下方式啟動(dòng)進(jìn)程
env HEAPPROFILE=./rocksdb_profiling ./test_tools 10000000 0 256 32 100 1,此時(shí)同樣會(huì)每隔一段時(shí)間會(huì)在當(dāng)前文件夾下生成一個(gè)以rocksdb_profiling開(kāi)頭的heap文件
接下來(lái)我們使用工具pprof來(lái)查看內(nèi)存占用情況
pprof --text ./test_tools ./rocksdb_profiling.0001.heap | vim -
打印如下重點(diǎn)關(guān)注第一列和第四列,分別表示該函數(shù)當(dāng)前正在使用的內(nèi)存和累計(jì)分配的內(nèi)存
同樣為了增加對(duì)比性,使用以下命令可視化打印以上占用內(nèi)存較多的函數(shù)的calltrace
pprof --svg ./test_tools ./rocksdb_profiling.0001.heap >> rocksdb_profiling.svg
將生成的svg文件放入瀏覽器如下:
因?yàn)槲覀兇蛴〉臅r(shí)候沒(méi)有過(guò)濾mmap以及sbrk等分配在匿名頁(yè)上的內(nèi)存占用情況,導(dǎo)致顯示的數(shù)據(jù)只有16%的部分是arena分配memtable的占用,其他大都是pthread_create創(chuàng)建線程時(shí)使用mmap分配的內(nèi)存。
可以通過(guò)如下命令過(guò)濾掉匿名頁(yè)的內(nèi)存統(tǒng)計(jì):
pprof --svg --ignore='SbrkSysAllocator::Alloc|MmapSysAllocator::Alloc' ./test_tools ./rocksdb_profiling.0001.heap >> rocksdb_profiling.svg
在運(yùn)行代碼的過(guò)程中我們的二進(jìn)制工具也會(huì)實(shí)時(shí)打印使用內(nèi)部接口抓取到的內(nèi)存占用數(shù)據(jù)以及tcmalloc的接口如下:
rocksdb.block-cache-usage : 0
rocksdb.estimate-table-readers-mem : 0
rocksdb.size-all-mem-tables : 50132416
simple_examples heap stats is : ------------------------------------------------
MALLOC: 50844816 ( 48.5 MiB) Bytes in use by application
MALLOC: + 876544 ( 0.8 MiB) Bytes in page heap freelist
MALLOC: + 341064 ( 0.3 MiB) Bytes in central cache freelist
MALLOC: + 0 ( 0.0 MiB) Bytes in transfer cache freelist
MALLOC: + 366376 ( 0.3 MiB) Bytes in thread cache freelists
MALLOC: + 2621440 ( 2.5 MiB) Bytes in malloc metadata
MALLOC: ------------
MALLOC: = 55050240 ( 52.5 MiB) Actual memory used (physical + swap)
MALLOC: + 0 ( 0.0 MiB) Bytes released to OS (aka unmapped)
MALLOC: ------------
MALLOC: = 55050240 ( 52.5 MiB) Virtual address space used
MALLOC:
MALLOC: 86 Spans in use
MALLOC: 7 Thread heaps in use
MALLOC: 8192 Tcmalloc page size
------------------------------------------------
Call ReleaseFreeMemory() to release freelist memory to the OS (via madvise()).
Bytes released to the OS take up virtual address space but no physical memory.
源碼分析
通過(guò)以上兩個(gè)組合工具已經(jīng)抓取到了rocksdb隨機(jī)寫(xiě)時(shí)對(duì)應(yīng)的內(nèi)存占用數(shù)據(jù)以及內(nèi)存分配過(guò)程,且以上過(guò)程中我們抓數(shù)據(jù)的時(shí)候也都在觀察top本身統(tǒng)計(jì)的無(wú)力內(nèi)存占用大小,經(jīng)過(guò)多方數(shù)據(jù)比對(duì)且數(shù)據(jù)量也能夠達(dá)到業(yè)務(wù)問(wèn)題的數(shù)據(jù)量,并未出現(xiàn)top實(shí)際的物理內(nèi)存超過(guò)rocksdb本身統(tǒng)計(jì)的內(nèi)存占用2-3倍的情況,差異最多只有20%。
帶著疑惑先分析一下兩個(gè)組合工具對(duì)內(nèi)存數(shù)據(jù)的統(tǒng)計(jì),如果在內(nèi)存管理的邏輯上確實(shí)有不合理的地方那跟著內(nèi)存分配的調(diào)用棧一看源碼就知道了。
valgrind和gperf都統(tǒng)計(jì)到了arena的內(nèi)存分配占用較多的情況,我們先看一下這個(gè)邏輯是否合理。
rocksdb的寫(xiě)入流程圖如下,詳細(xì)可以參考rocksdb 寫(xiě)入原理:
一個(gè)put請(qǐng)求會(huì)先寫(xiě)wal,再寫(xiě)memtable,由以上調(diào)用棧我們知道此時(shí)是在寫(xiě)memtable。同時(shí)我們上層是多個(gè)線程在put,rocksdb會(huì)為每個(gè)put綁定一個(gè)writer,并指定一個(gè)主writer 在batch_size的范圍內(nèi)負(fù)責(zé)先寫(xiě)入自己以及從writer的wal,同時(shí)從writer可以直接寫(xiě)memtable.
memtable邏輯
寫(xiě)入會(huì)與自己key-value所綁定的column family對(duì)應(yīng),從而保證cf的邏輯分區(qū)功能。
此時(shí)調(diào)用到了寫(xiě)入對(duì)應(yīng)cf的函數(shù),并將key-value數(shù)據(jù)添加到memtable之中:
Status PutCFImpl(uint32_t column_family_id, const Slice& key,const Slice& value, ValueType value_type) {
......MemTable* mem = cf_mems_->GetMemTable();auto* moptions = mem->GetImmutableMemTableOptions();// inplace_update_support is inconsistent with snapshots, and therefore with// any kind of transactions including the ones that use seq_per_batchassert(!seq_per_batch_ || !moptions->inplace_update_support);if (!moptions->inplace_update_support) {bool mem_res =mem->Add(sequence_, value_type, key, value,concurrent_memtable_writes_, get_post_process_info(mem),hint_per_batch_ ? &GetHintMap()[mem] : nullptr);......
}
之后就是memtale的寫(xiě)入,在剛開(kāi)始的時(shí)候會(huì)根據(jù)傳入key的大小,value的大小分配指定長(zhǎng)度的空間
bool MemTable::Add(SequenceNumber s, ValueType type,const Slice& key, /* user key */const Slice& value, bool allow_concurrent,MemTablePostProcessInfo* post_process_info, void** hint) {// Format of an entry is concatenation of:// key_size : varint32 of internal_key.size()// key bytes : char[internal_key.size()]// value_size : varint32 of value.size()// value bytes : char[value.size()]uint32_t key_size = static_cast<uint32_t>(key.size());uint32_t val_size = static_cast<uint32_t>(value.size());uint32_t internal_key_size = key_size + 8;const uint32_t encoded_len = VarintLength(internal_key_size) +internal_key_size + VarintLength(val_size) +val_size;char* buf = nullptr;std::unique_ptr<MemTableRep>& table =type == kTypeRangeDeletion ? range_del_table_ : table_;KeyHandle handle = table->Allocate(encoded_len, &buf);......
}
最終會(huì)調(diào)用到arena分配器分配指定的內(nèi)存空間供數(shù)據(jù)存儲(chǔ)
inline char* Arena::Allocate(size_t bytes) {// The semantics of what to return are a bit messy if we allow// 0-byte allocations, so we disallow them here (we don't need// them for our internal use).assert(bytes > 0);if (bytes <= alloc_bytes_remaining_) {unaligned_alloc_ptr_ -= bytes;alloc_bytes_remaining_ -= bytes;return unaligned_alloc_ptr_;}return AllocateFallback(bytes, false /* unaligned */);
}
所以以上邏輯本身就是一個(gè)正常的內(nèi)存使用邏輯,key-value寫(xiě)入需要寫(xiě)寫(xiě)入到memtable之中,所以會(huì)分配對(duì)應(yīng)空間來(lái)保存。同時(shí)關(guān)于memtable的釋放,我們并不會(huì)一直占用memtable的空間,而是當(dāng)memtable寫(xiě)入超過(guò)以上流程圖顯示的write_buffer_size的大小之后,會(huì)將當(dāng)前memtable標(biāo)記為只讀的immutable memtable,從而開(kāi)始向底層固化,并且會(huì)創(chuàng)建一個(gè)新的memtable來(lái)繼續(xù)接受key-vale
內(nèi)存中能夠同時(shí)存在的memtable的個(gè)數(shù)取決于參數(shù)max_write_buffer_number,也就是immutable memtable向底層固化結(jié)束之后會(huì)被刪除。
到此arena的分配即為正常的邏輯處理。
table_cache邏輯
但是在valgrind之中仍然有另一個(gè)內(nèi)存占用較大的函數(shù)calltrace
UncompressBlockContentsForCompressionType
仍然先看以上的流程圖,我們能夠看到memtable是一個(gè)rocksdb存在于內(nèi)存的數(shù)據(jù)結(jié)構(gòu),除了該文件之外rocksdb還有一些其他的數(shù)據(jù)結(jié)構(gòu)用來(lái)管理存儲(chǔ)在sst之上的key-value數(shù)據(jù),以及一些常駐于內(nèi)存用于提升索引性能的數(shù)據(jù)結(jié)構(gòu),他們都被封裝在了block cache之中。同時(shí)另一個(gè)cache組件 tablecache是用來(lái)管理rocksdb內(nèi)部產(chǎn)生讀數(shù)據(jù)的一個(gè)存儲(chǔ)組件,比如compaction過(guò)程中需要挑選每一層向下一層寫(xiě)入的文件的時(shí)候會(huì)將改文件的一些元數(shù)據(jù)讀取到table cache之中,如果有過(guò)壓縮,則會(huì)進(jìn)行解壓。
對(duì)應(yīng)的邏輯如下:
當(dāng)上層觸發(fā)讀的時(shí)候,會(huì)下發(fā)一個(gè)key,table_cache就站出來(lái)想要對(duì)當(dāng)前讀的數(shù)據(jù)做一個(gè)緩存來(lái)減少磁盤(pán)IO的次數(shù),此時(shí)讀請(qǐng)求會(huì)先下發(fā)到table_cache之下,拿著請(qǐng)求的key 從tablecache中的data block中索引key對(duì)應(yīng)的value存儲(chǔ)位置,這個(gè)時(shí)候主要是調(diào)用FindTable這個(gè)函數(shù),在該函數(shù)中調(diào)用GetTableReader函數(shù)創(chuàng)建用于緩存key不在的情況的handle信息。
Status TableCache::FindTable(const FileOptions& file_options,const InternalKeyComparator& internal_comparator,const FileDescriptor& fd, Cache::Handle** handle,const SliceTransform* prefix_extractor,const bool no_io, bool record_read_stats,HistogramImpl* file_read_hist, bool skip_filters,int level,bool prefetch_index_and_filter_in_cache) {PERF_TIMER_GUARD_WITH_ENV(find_table_nanos, ioptions_.env);Status s;uint64_t number = fd.GetNumber();Slice key = GetSliceForFileNumber(&number);*handle = cache_->Lookup(key); //先從cache中查找該key是否存在,并返回一個(gè)cache句柄TEST_SYNC_POINT_CALLBACK("TableCache::FindTable:0",const_cast<bool*>(&no_io));/*如果沒(méi)有找到,且判讀此時(shí)沒(méi)有IO,那么直接返回該key不存在。如果此時(shí)有IO,則會(huì)加鎖再嘗試找一次(防止先put后get這樣的情況,put的IO鏈路還未完成)如果還是沒(méi)有找到,則新建一個(gè)用于存放hanlde的緩存table_reader,放到table cache之中,用作當(dāng)前key數(shù)據(jù)的緩存*/if (*handle == nullptr) { if (no_io) { // Don't do IO and return a not-found statusreturn Status::Incomplete("Table not found in table_cache, no_io is set");}MutexLock load_lock(loader_mutex_.get(key));// We check the cache again under loading mutex*handle = cache_->Lookup(key);if (*handle != nullptr) {return s;}//嘗試新建一個(gè)table_reader,用來(lái)存放handle的緩存std::unique_ptr<TableReader> table_reader;s = GetTableReader(file_options, internal_comparator, fd,false /* sequential mode */, record_read_stats,file_read_hist, &table_reader, prefix_extractor,skip_filters, level, prefetch_index_and_filter_in_cache);if (!s.ok()) {assert(table_reader == nullptr);RecordTick(ioptions_.statistics, NO_FILE_ERRORS);// We do not cache error results so that if the error is transient,// or somebody repairs the file, we recover automatically.} else {// 如果創(chuàng)建成功了,就把table_reader添加到cache之中s = cache_->Insert(key, table_reader.get(), 1, &DeleteEntry<TableReader>,handle);if (s.ok()) {// Release ownership of table reader.// 添加成功之后,就把用于緩存hanlde 的table_reader釋放掉,table_reader.release();}}}return s;
}
接下來(lái)看一下GetTableReader 函數(shù),主要是通過(guò)NewTableReader函數(shù)來(lái)進(jìn)行table_reader的創(chuàng)建
Status TableCache::GetTableReader(const FileOptions& file_options,const InternalKeyComparator& internal_comparator, const FileDescriptor& fd,bool sequential_mode, bool record_read_stats, HistogramImpl* file_read_hist,std::unique_ptr<TableReader>* table_reader,const SliceTransform* prefix_extractor, bool skip_filters, int level,bool prefetch_index_and_filter_in_cache) {......s = ioptions_.table_factory->NewTableReader(TableReaderOptions(ioptions_, prefix_extractor, file_options,internal_comparator, skip_filters, immortal_tables_,level, fd.largest_seqno, block_cache_tracer_),std::move(file_reader), fd.GetFileSize(), table_reader,prefetch_index_and_filter_in_cache);...
}
最終的calltrace就如我們之前看到的打印棧一樣,核心還是在沒(méi)有從table_cache之中檢測(cè)到key之后想要將key所代表的hanlde添加到cache之中,這個(gè)過(guò)程在完成之后會(huì)釋放掉中間生成的臨時(shí)數(shù)據(jù)結(jié)構(gòu)table_reader(它是用來(lái)緩存key在table_cache中的數(shù)據(jù)的)。
總結(jié)
綜上的源碼分析,這樣的memtable和table_cache 內(nèi)存分配是完全屬于正常邏輯,且持續(xù)大壓力put的過(guò)程中并未復(fù)現(xiàn)內(nèi)存問(wèn)題。
于是帶著數(shù)據(jù)、分析過(guò)程和源碼 與同事進(jìn)行核對(duì),他也百思不得其解,無(wú)奈之下只好讓他重新pull 最新代碼,再來(lái)一輪測(cè)試。
那么奇跡出現(xiàn)了,他反復(fù)得按照之前的測(cè)試方式,rocksdb內(nèi)存資源依舊穩(wěn)若泰山。。。。。。最終呢,之前測(cè)試的代碼版本是一波異常raft的處理邏輯,正常IO的時(shí)候會(huì)在內(nèi)存緩存大量的臨時(shí)數(shù)據(jù)結(jié)構(gòu)無(wú)法釋放,且不屬于raft log,屬于測(cè)試代碼。今天重新搞了一波最終版本,一切重歸于好。
計(jì)算機(jī)系統(tǒng)已經(jīng)不再是一套簡(jiǎn)單系統(tǒng),一個(gè)微小得改動(dòng)就可能耗費(fèi)幾個(gè)人一天的時(shí)間,而能夠規(guī)避這樣的問(wèn)題最好的辦法就是引入一套完善嚴(yán)謹(jǐn)?shù)囊?guī)則來(lái)約束,縮小復(fù)雜系統(tǒng)的復(fù)雜度。
總結(jié)
以上是生活随笔為你收集整理的Rocksdb 内存“不释放”问题 分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: C++ 中emplace_back和pu
- 下一篇: C++ STL: 基本六大部件概览 及