Rocksdb 内存“不释放”问题 分析
文章目錄
- 問題場景描述
- 問題復(fù)現(xiàn)
- 編寫隨機(jī)寫 測試工具
- 使用工具抓取內(nèi)存分配過程
- 源碼分析
- memtable邏輯
- table_cache邏輯
- 總結(jié)
整體的IO場景到底層的源碼分析過程如上導(dǎo)圖,接下來將詳細(xì)闡述具體的過程。
問題場景描述
我們的rocksdb作為單機(jī)存儲引擎,跑在用分布式一致性協(xié)議raft 封裝的一個分布式存儲集群之上。基本的IO架構(gòu)圖如下:
針對該分布式存儲集群,上層使用的是隨機(jī)IO ,即每個raft交給rocksdb的請求所轉(zhuǎn)化的key都是隨機(jī)的。此時,rocksdb底層當(dāng)然調(diào)用的是put的接口來持久化key-value數(shù)據(jù)。
問題現(xiàn)象是(同事給出的,我們只看到一個結(jié)果) 隨著IO的持續(xù)寫入,大概每個節(jié)點rocksdb數(shù)據(jù)的存儲量都達(dá)到20G以上之后,top看到的IO 進(jìn)程物理內(nèi)存資源和實際的抓取的rocksdb tcmalloc分配的堆內(nèi)存大小無法匹配,差距達(dá)到2-3倍。這個時候為了排除raft對內(nèi)存消耗的影響,他將raft的寫log邏輯去掉,IO僅僅經(jīng)過協(xié)議棧到達(dá)底層rocksdb,但是他看到的日志以及內(nèi)存占用仍然還是無法匹配,且內(nèi)存持續(xù)增大無法釋放,是不是rocksdb內(nèi)部的存在內(nèi)存泄露?
問題復(fù)現(xiàn)
業(yè)務(wù)場景 也就是隨機(jī)put,且每次都必先,那么復(fù)現(xiàn)就很簡單了,那單獨的rocksdb來進(jìn)行隨機(jī)寫測試,并抓取內(nèi)存分布情況。
編寫隨機(jī)寫 測試工具
這里說明一下為什么不實用rocksdb原生的db_bench進(jìn)行測試,它功能更多,配置更強(qiáng)。
但是我們想要打印我們自己想看的東西,且排除它自己工具本身接口過多而產(chǎn)生的干擾,所以就直接自己寫一個小工具,方便易用,抓取內(nèi)存信息更為方便。
使用put接口進(jìn)行隨機(jī)寫 測試工具的封裝,以下代碼提供如下功能
- 指定隨機(jī)寫 請求的個數(shù)
- 指定 key的范圍,默認(rèn)隨機(jī)
- 指定value的大小
- 指定rocksdb compaction線程數(shù)
- 指定 put的客戶端線程數(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. 請求個數(shù)
2. key的范圍
3. value的大小(默認(rèn)100B)
4. rocksdb后臺compaction線程數(shù)
5. 客戶端壓put的線程數(shù)
6. 數(shù)據(jù)庫的個數(shù)(指定多少個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ù)庫
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)線程編號為10時打印統(tǒng)計的信息*/if (thread_id == 10 && req_num % 10000 == 0){double tm = now() - ts;/*統(tǒng)計當(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)計的 內(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); //主要是看這個指標(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請求達(dá)到了參數(shù)設(shè)置的請求個數(shù),所有線程停止寫入if(req_num >= test_count) {delete db;break;}}
}template <class DB, class OPT>
static void do_run(const string &db_name)
{/*初始化option選項的配置*/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
引用工具時只需要調(diào)用以上init和do_run兩個函數(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)信息。
使用該接口時頭文件需要指定#include <gperftools/malloc_extension.h>,編譯選項之中需要加入-ltcmalloc,系統(tǒng)找不到tcmalloc的動態(tài)庫,則需要制定動態(tài)庫的加載路徑env LD_PRELOAD="/usr/lib/libtcmalloc.so",關(guān)于gperftools的使用配置詳細(xì)可以參考gperftools
使用工具抓取內(nèi)存分配過程
-
valgrind + massif
這里很簡單,使用如下命令讓進(jìn)程啟動:
valgrind --tools=massif ./test_tools 10000000 0 256 32 100 1
關(guān)于valgrind的詳細(xì)使用可以參考valgrind,這里在運行過程中massif會做很多次當(dāng)前進(jìn)程占用的物理內(nèi)存快照,并且其中會有詳細(xì)快照,即進(jìn)程物理內(nèi)存分配過程中的一個函數(shù)層級調(diào)用棧。valigrind默認(rèn)抓取的是堆內(nèi)存,如需要抓取mmap之類的匿名頁分配的內(nèi)存,需要指定對應(yīng)的參數(shù)。運行一段時間之后終止進(jìn)程,會在當(dāng)前目錄下生成一個.out文件,使用ms_print查看文件內(nèi)容
結(jié)果類似如下
這里需要注意massif打印的并不是內(nèi)存沒有釋放的,只是當(dāng)前時刻進(jìn)程物理內(nèi)存的一個分布,但我們?nèi)匀荒軌蚩吹揭恍┯捎诘膬?nèi)存占用信息,一個是memtable創(chuàng)建的時候調(diào)用arena分配器分配的內(nèi)存,還有一個是blockcache 存儲解壓縮數(shù)據(jù)的一個調(diào)用棧。
為了讓數(shù)據(jù)更加全面準(zhǔn)確,我們也使用gperf工具進(jìn)行進(jìn)程堆內(nèi)存分配的一個數(shù)據(jù)收集。
-
gperf profiling + pprof數(shù)據(jù)收集
我們使用如下方式啟動進(jìn)程
env HEAPPROFILE=./rocksdb_profiling ./test_tools 10000000 0 256 32 100 1,此時同樣會每隔一段時間會在當(dāng)前文件夾下生成一個以rocksdb_profiling開頭的heap文件
接下來我們使用工具pprof來查看內(nèi)存占用情況
pprof --text ./test_tools ./rocksdb_profiling.0001.heap | vim -
打印如下重點關(guān)注第一列和第四列,分別表示該函數(shù)當(dāng)前正在使用的內(nèi)存和累計分配的內(nèi)存
同樣為了增加對比性,使用以下命令可視化打印以上占用內(nèi)存較多的函數(shù)的calltrace
pprof --svg ./test_tools ./rocksdb_profiling.0001.heap >> rocksdb_profiling.svg
將生成的svg文件放入瀏覽器如下:
因為我們打印的時候沒有過濾mmap以及sbrk等分配在匿名頁上的內(nèi)存占用情況,導(dǎo)致顯示的數(shù)據(jù)只有16%的部分是arena分配memtable的占用,其他大都是pthread_create創(chuàng)建線程時使用mmap分配的內(nèi)存。
可以通過如下命令過濾掉匿名頁的內(nèi)存統(tǒng)計:
pprof --svg --ignore='SbrkSysAllocator::Alloc|MmapSysAllocator::Alloc' ./test_tools ./rocksdb_profiling.0001.heap >> rocksdb_profiling.svg
在運行代碼的過程中我們的二進(jìn)制工具也會實時打印使用內(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.
源碼分析
通過以上兩個組合工具已經(jīng)抓取到了rocksdb隨機(jī)寫時對應(yīng)的內(nèi)存占用數(shù)據(jù)以及內(nèi)存分配過程,且以上過程中我們抓數(shù)據(jù)的時候也都在觀察top本身統(tǒng)計的無力內(nèi)存占用大小,經(jīng)過多方數(shù)據(jù)比對且數(shù)據(jù)量也能夠達(dá)到業(yè)務(wù)問題的數(shù)據(jù)量,并未出現(xiàn)top實際的物理內(nèi)存超過rocksdb本身統(tǒng)計的內(nèi)存占用2-3倍的情況,差異最多只有20%。
帶著疑惑先分析一下兩個組合工具對內(nèi)存數(shù)據(jù)的統(tǒng)計,如果在內(nèi)存管理的邏輯上確實有不合理的地方那跟著內(nèi)存分配的調(diào)用棧一看源碼就知道了。
valgrind和gperf都統(tǒng)計到了arena的內(nèi)存分配占用較多的情況,我們先看一下這個邏輯是否合理。
rocksdb的寫入流程圖如下,詳細(xì)可以參考rocksdb 寫入原理:
一個put請求會先寫wal,再寫memtable,由以上調(diào)用棧我們知道此時是在寫memtable。同時我們上層是多個線程在put,rocksdb會為每個put綁定一個writer,并指定一個主writer 在batch_size的范圍內(nèi)負(fù)責(zé)先寫入自己以及從writer的wal,同時從writer可以直接寫memtable.
memtable邏輯
寫入會與自己key-value所綁定的column family對應(yīng),從而保證cf的邏輯分區(qū)功能。
此時調(diào)用到了寫入對應(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的寫入,在剛開始的時候會根據(jù)傳入key的大小,value的大小分配指定長度的空間
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);......
}
最終會調(diào)用到arena分配器分配指定的內(nèi)存空間供數(shù)據(jù)存儲
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 */);
}
所以以上邏輯本身就是一個正常的內(nèi)存使用邏輯,key-value寫入需要寫寫入到memtable之中,所以會分配對應(yīng)空間來保存。同時關(guān)于memtable的釋放,我們并不會一直占用memtable的空間,而是當(dāng)memtable寫入超過以上流程圖顯示的write_buffer_size的大小之后,會將當(dāng)前memtable標(biāo)記為只讀的immutable memtable,從而開始向底層固化,并且會創(chuàng)建一個新的memtable來繼續(xù)接受key-vale
內(nèi)存中能夠同時存在的memtable的個數(shù)取決于參數(shù)max_write_buffer_number,也就是immutable memtable向底層固化結(jié)束之后會被刪除。
到此arena的分配即為正常的邏輯處理。
table_cache邏輯
但是在valgrind之中仍然有另一個內(nèi)存占用較大的函數(shù)calltrace
UncompressBlockContentsForCompressionType
仍然先看以上的流程圖,我們能夠看到memtable是一個rocksdb存在于內(nèi)存的數(shù)據(jù)結(jié)構(gòu),除了該文件之外rocksdb還有一些其他的數(shù)據(jù)結(jié)構(gòu)用來管理存儲在sst之上的key-value數(shù)據(jù),以及一些常駐于內(nèi)存用于提升索引性能的數(shù)據(jù)結(jié)構(gòu),他們都被封裝在了block cache之中。同時另一個cache組件 tablecache是用來管理rocksdb內(nèi)部產(chǎn)生讀數(shù)據(jù)的一個存儲組件,比如compaction過程中需要挑選每一層向下一層寫入的文件的時候會將改文件的一些元數(shù)據(jù)讀取到table cache之中,如果有過壓縮,則會進(jìn)行解壓。
對應(yīng)的邏輯如下:
當(dāng)上層觸發(fā)讀的時候,會下發(fā)一個key,table_cache就站出來想要對當(dāng)前讀的數(shù)據(jù)做一個緩存來減少磁盤IO的次數(shù),此時讀請求會先下發(fā)到table_cache之下,拿著請求的key 從tablecache中的data block中索引key對應(yīng)的value存儲位置,這個時候主要是調(diào)用FindTable這個函數(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是否存在,并返回一個cache句柄TEST_SYNC_POINT_CALLBACK("TableCache::FindTable:0",const_cast<bool*>(&no_io));/*如果沒有找到,且判讀此時沒有IO,那么直接返回該key不存在。如果此時有IO,則會加鎖再嘗試找一次(防止先put后get這樣的情況,put的IO鏈路還未完成)如果還是沒有找到,則新建一個用于存放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;}//嘗試新建一個table_reader,用來存放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;
}
接下來看一下GetTableReader 函數(shù),主要是通過NewTableReader函數(shù)來進(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就如我們之前看到的打印棧一樣,核心還是在沒有從table_cache之中檢測到key之后想要將key所代表的hanlde添加到cache之中,這個過程在完成之后會釋放掉中間生成的臨時數(shù)據(jù)結(jié)構(gòu)table_reader(它是用來緩存key在table_cache中的數(shù)據(jù)的)。
總結(jié)
綜上的源碼分析,這樣的memtable和table_cache 內(nèi)存分配是完全屬于正常邏輯,且持續(xù)大壓力put的過程中并未復(fù)現(xiàn)內(nèi)存問題。
于是帶著數(shù)據(jù)、分析過程和源碼 與同事進(jìn)行核對,他也百思不得其解,無奈之下只好讓他重新pull 最新代碼,再來一輪測試。
那么奇跡出現(xiàn)了,他反復(fù)得按照之前的測試方式,rocksdb內(nèi)存資源依舊穩(wěn)若泰山。。。。。。最終呢,之前測試的代碼版本是一波異常raft的處理邏輯,正常IO的時候會在內(nèi)存緩存大量的臨時數(shù)據(jù)結(jié)構(gòu)無法釋放,且不屬于raft log,屬于測試代碼。今天重新搞了一波最終版本,一切重歸于好。
計算機(jī)系統(tǒng)已經(jīng)不再是一套簡單系統(tǒng),一個微小得改動就可能耗費幾個人一天的時間,而能夠規(guī)避這樣的問題最好的辦法就是引入一套完善嚴(yán)謹(jǐn)?shù)囊?guī)則來約束,縮小復(fù)雜系統(tǒng)的復(fù)雜度。
總結(jié)
以上是生活随笔為你收集整理的Rocksdb 内存“不释放”问题 分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++ 中emplace_back和pu
- 下一篇: 香蕉多少钱一斤啊?