LevelDB源碼分析之十:LOG文件
首先要區(qū)分LOG文件和.log文件。
LOG文件:用來(lái)記錄數(shù)據(jù)庫(kù)打印的運(yùn)行日志信息,方便bug的查找。
.log文件:在LevelDB中的主要作用是系統(tǒng)故障恢復(fù)時(shí),能夠保證不會(huì)丟失數(shù)據(jù)。因?yàn)樵趯⒂涗泴?xiě)入內(nèi)存的Memtable之前,會(huì)先寫(xiě)入.log文件,這樣即使系統(tǒng)發(fā)生故障,Memtable中的數(shù)據(jù)沒(méi)有來(lái)得及Dump到磁盤(pán)的SSTable文件,LevelDB也可以根據(jù).log文件恢復(fù)內(nèi)存的Memtable數(shù)據(jù)結(jié)構(gòu)內(nèi)容,不會(huì)造成系統(tǒng)丟失數(shù)據(jù)。
Env.h中定義了操作LOG文件的虛基類(lèi)Logger,只提供了一個(gè)對(duì)外的接口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)前線(xiàn)程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)存不夠,會(huì)第二次分配更大的堆內(nèi)存。 ????//?如果第二次分的內(nèi)存還不夠,只能對(duì)存放內(nèi)容做截?cái)嗵幚砹恕?????//?為什么p==limit也算內(nèi)存不夠呢?因?yàn)樽詈笠娣艙Q行符, ????//?所以有效的內(nèi)容最大長(zhǎng)度只能是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只是將寫(xiě)入內(nèi)容放入緩存中,真正輸出到文件是通過(guò)fflush實(shí)現(xiàn)的。 ????fflush(file_); ????//?如果分配了堆內(nèi)存,需要手動(dòng)釋放。 ????if?(base?!=?buffer)?{ ??????delete[]?base; ????} ????break; ??} }
我用的是Windows版LevelDB,上面這段代碼在Windows 7上用VS2013調(diào)試時(shí)是有bug的。當(dāng)要打印的日志信息長(zhǎng)度超過(guò)30000時(shí),vsnsprintf會(huì)截?cái)嘈畔?,但是vsnsprintf并不會(huì)返回被截?cái)嗲暗男畔⒌拈L(zhǎng)度,而是返回-1,這樣一來(lái),那一行日志信息只會(huì)打印出日期、時(shí)間和線(xiàn)程號(hào),盡管一行日志超過(guò)30000字節(jié)的概率非常小。經(jīng)查證,vsnsprintf的返回值和操作系統(tǒng)、編譯器有關(guān)。
經(jīng)測(cè)試,Windows10上用VS2015時(shí)vsnsprintf能返回期望結(jié)果——被截?cái)嗲暗男畔⒌拈L(zhǎng)度。
個(gè)人覺(jué)得這段代碼非常值得學(xué)習(xí)。首先是考慮了內(nèi)存分配的情況。通??吹降娜罩緦?shí)現(xiàn)中,要么是在棧中設(shè)定一個(gè)定長(zhǎng)緩沖區(qū),然后設(shè)定一個(gè)日志最大長(zhǎng)度,以此來(lái)避免內(nèi)存分配;要么是直接分配一大塊內(nèi)存來(lái)放日志字符串。這里作者使用了兩級(jí)“內(nèi)存分配”,首先在棧中分配500個(gè)字節(jié)的緩沖區(qū),這樣長(zhǎng)度小于500字節(jié)的日志就可以避免掉new的開(kāi)銷(xiāo)。如果發(fā)現(xiàn)放不下,再去new一個(gè)大塊內(nèi)存。當(dāng)然如果單行日志太大,超過(guò)了30000字節(jié),那么就直接做截?cái)嗔?。其次就是?nèi)存的防越界處理。
不過(guò)這里有個(gè)疑問(wèn),為何添加換行符的時(shí)候要判斷p==base,知道的同學(xué)請(qǐng)指點(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","燦哥哥的博客");