LevelDB源碼分析之十:LOG文件
首先要區(qū)分LOG文件和.log文件。
LOG文件:用來記錄數(shù)據(jù)庫打印的運(yùn)行日志信息,方便bug的查找。
.log文件:在LevelDB中的主要作用是系統(tǒng)故障恢復(fù)時(shí),能夠保證不會丟失數(shù)據(jù)。因?yàn)樵趯⒂涗泴懭雰?nèi)存的Memtable之前,會先寫入.log文件,這樣即使系統(tǒng)發(fā)生故障,Memtable中的數(shù)據(jù)沒有來得及Dump到磁盤的SSTable文件,LevelDB也可以根據(jù).log文件恢復(fù)內(nèi)存的Memtable數(shù)據(jù)結(jié)構(gòu)內(nèi)容,不會造成系統(tǒng)丟失數(shù)據(jù)。
Env.h中定義了操作LOG文件的虛基類Logger,只提供了一個(gè)對外的接口Logv。Logger的Windows版本實(shí)現(xiàn)是WinLogger。
//?win_logger.h class?WinLogger?:?public?Logger?{ ?private: ??FILE*?file_; ?public: ??explicit?WinLogger(FILE*?f)?:?file_(f)?{?assert(file_);?} ??virtual?~WinLogger()?{ ????fclose(file_); ??} ??virtual?void?Logv(const?char*?format,?va_list?ap); };
//?win_logger.cc void?WinLogger::Logv(const?char*?format,?va_list?ap)?{ ??//?獲取當(dāng)前線程ID ??const?uint64_t?thread_id?=?static_cast(::GetCurrentThreadId()); ??//?We?try?twice:?the?first?time?with?a?fixed-size?stack?allocated?buffer, ??//?and?the?second?time?with?a?much?larger?dynamically?allocated?buffer. ??//?嘗試兩次內(nèi)存分配:第一次分配固定大小的棧內(nèi)存,如果不夠,第二次分配更大的堆內(nèi)存 ??char?buffer[500]; ??for?(int?iter?=?0;?iter?<?2;?iter++)?{ ????char*?base; ????int?bufsize; ????if?(iter?==?0)?{ ??????bufsize?=?sizeof(buffer); ??????base?=?buffer; ????}?else?{ ??????bufsize?=?30000; ??????base?=?new?char[bufsize]; ????} ????char*?p?=?base; ????char*?limit?=?base?+?bufsize; ????SYSTEMTIME?st; ????//?GetSystemTime?returns?UTC?time,?we?want?local?time! ????::GetLocalTime(&st); ????p?+=?_snprintf_s(p,?limit?-?p,?_TRUNCATE, ??????"%04d/%02d/%02d-%02d:%02d:%02d.%03d?%llx?", ??????st.wYear, ??????st.wMonth, ??????st.wDay, ??????st.wHour, ??????st.wMinute, ??????st.wSecond, ??????st.wMilliseconds, ??????static_cast(thread_id)); ????//?Print?the?message ????if?(p?<?limit)?{ ??????va_list?backup_ap?=?ap; ???//?limit-p是p可接受的最大字符數(shù) ??????p?+=?vsnprintf(p,?limit?-?p,?format,?backup_ap); ??????va_end(backup_ap); ????} ????//?Truncate?to?available?space?if?necessary ????//?如果第一次分的棧內(nèi)存不夠,會第二次分配更大的堆內(nèi)存。 ????//?如果第二次分的內(nèi)存還不夠,只能對存放內(nèi)容做截?cái)嗵幚砹恕?????//?為什么p==limit也算內(nèi)存不夠呢?因?yàn)樽詈笠娣艙Q行符, ????//?所以有效的內(nèi)容最大長度只能是limit-p-1。 ????if?(p?>=?limit)?{ ??????if?(iter?==?0)?{ ????????continue;?//?Try?again?with?larger?buffer ??????}?else?{ ????????p?=?limit?-?1; ??????} ????} ????//?Add?newline?if?necessary ????//?如果p==base或者有效內(nèi)容的最后一個(gè)字符不是換行符 ????//?則將換行符添加到最后 ????if?(p?==?base?||?p[-1]?!=?'n')?{ ??????*p++?=?'n'; ????} ????assert(p?<=?limit); ????fwrite(base,?1,?p?-?base,?file_); ????//?fwrite只是將寫入內(nèi)容放入緩存中,真正輸出到文件是通過fflush實(shí)現(xiàn)的。 ????fflush(file_); ????//?如果分配了堆內(nèi)存,需要手動釋放。 ????if?(base?!=?buffer)?{ ??????delete[]?base; ????} ????break; ??} }
我用的是Windows版LevelDB,上面這段代碼在Windows 7上用VS2013調(diào)試時(shí)是有bug的。當(dāng)要打印的日志信息長度超過30000時(shí),vsnsprintf會截?cái)嘈畔?,但是vsnsprintf并不會返回被截?cái)嗲暗男畔⒌拈L度,而是返回-1,這樣一來,那一行日志信息只會打印出日期、時(shí)間和線程號,盡管一行日志超過30000字節(jié)的概率非常小。經(jīng)查證,vsnsprintf的返回值和操作系統(tǒng)、編譯器有關(guān)。
經(jīng)測試,Windows10上用VS2015時(shí)vsnsprintf能返回期望結(jié)果——被截?cái)嗲暗男畔⒌拈L度。
個(gè)人覺得這段代碼非常值得學(xué)習(xí)。首先是考慮了內(nèi)存分配的情況。通??吹降娜罩緦?shí)現(xiàn)中,要么是在棧中設(shè)定一個(gè)定長緩沖區(qū),然后設(shè)定一個(gè)日志最大長度,以此來避免內(nèi)存分配;要么是直接分配一大塊內(nèi)存來放日志字符串。這里作者使用了兩級“內(nèi)存分配”,首先在棧中分配500個(gè)字節(jié)的緩沖區(qū),這樣長度小于500字節(jié)的日志就可以避免掉new的開銷。如果發(fā)現(xiàn)放不下,再去new一個(gè)大塊內(nèi)存。當(dāng)然如果單行日志太大,超過了30000字節(jié),那么就直接做截?cái)嗔?。其次就是?nèi)存的防越界處理。
不過這里有個(gè)疑問,為何添加換行符的時(shí)候要判斷p==base,知道的同學(xué)請指點(diǎn)下。
最后在Env.h中封裝了一個(gè)全局的方法Log,方便調(diào)用接口Logv,如下所示。
void?Log(Logger*?info_log,?const?char*?format,?...)?{ ??if?(info_log?!=?NULL)?{ ????va_list?ap; ????va_start(ap,?format); ????info_log->Logv(format,?ap); ????va_end(ap); ??} }
調(diào)用方法:Log(result.info_log, "Ignoring error %s","燦哥哥的博客");