看完了微信團隊對Xlog的整體介紹,迫不及待開始了研究,理論部分我是完全參考微信終端跨平臺組件 mars 系列(一) - 高性能日志模塊xlog 這篇文章。
看完這篇理論,文章通過各個點來闡述如何創造這樣的高性能、可擴展的日志系統的想法和設計,于是我結合源碼配合一起梳理一下Xlog的幾個部分,無論以后自己造輪子作為借鑒或者使用這個輪子,都有很多價值。
方案:
使用流式壓縮方式對單行日志進行壓縮,壓縮加密后寫進作為 log 中間 buffer的 mmap 中,當 mmap 中的數據到達一定大小后再寫進磁盤文件中
從github上的官方文檔來看,Xlog的初始化涉及三個語句:
- xlogger_SetLevel(kLevelDebug);
- appender_set_console_log(true);
- appender_open(kAppednerAsync, [logPath UTF8String], "Test");
頭兩句第一個是設置log級別,第二句是控制臺是否打印語句的開關。
最重要的是第三句話 appender_open
。
Appender模塊是是整個日志系統負責寫日志的模塊。這里直接從頂部文章搬運一下結構圖片。這里appender_open是打開log目錄下的日志文件,進行一些初始化操作。
void appender_open(TAppenderMode _mode, const char* _dir, const char* _nameprefix) {
xlogger_SetAppender(&xlogger_appender); //設置xlogger的appender
//創建路徑文件夾
boost::filesystem::create_directories(_dir);
//tickcount_t用于計算每個步驟執行時間。
tickcount_t tick;
tick.gettickcount();
//第一步作用是每次啟動的時候會清理日志,防止占用太多用戶磁盤空間
__del_timeout_file(_dir);
tickcountdiff_t del_timeout_file_time = tickcount_t().gettickcount() - tick;
tick.gettickcount();
//設置mmap文件的路徑
char mmap_file_path[512] = {0};
snprintf(mmap_file_path, sizeof(mmap_file_path), "%s/%s.mmap2", sg_cache_logdir.empty()?_dir:sg_cache_logdir.c_str(), _nameprefix);
//檢查是否mmap文件中有數據,如果沒有直接退出,如果有,清除日志緩存,同時構造LogBuffer對象。
bool use_mmap = false;
if (OpenMmapFile(mmap_file_path, kBufferBlockLength, sg_mmmap_file)) {
sg_log_buff = new LogBuffer(sg_mmmap_file.data(), kBufferBlockLength, true);
use_mmap = true;
} else {
char* buffer = new char[kBufferBlockLength];
sg_log_buff = new LogBuffer(buffer, kBufferBlockLength, true);
use_mmap = false;
}
if (NULL == sg_log_buff->GetData().Ptr()) {
if (use_mmap && sg_mmmap_file.is_open()) CloseMmapFile(sg_mmmap_file);
return;
}
AutoBuffer buffer;
sg_log_buff->Flush(buffer);
...
BOOT_RUN_EXIT(appender_close);
}
日志目錄下的log文件為每天的日志文件,而mmap是日志緩存文件,因此會有如下策略:
這部分可以概括為:
**1. ** 首先日志系統有一個過期的設置,日志log文件過期就自動刪除
**2. ** 每次啟動清空 mmap 的日志緩存
**3. ** 設置日志系統的Appender模塊,應該可以通過自己重寫appender函數,實現自己的一套方案,也即是頂部文章所說的熱插拔。
題外話:
- 剛開始使用時發現main.mm中
setxattr(...);
這個方法,以為是 mars 提供的初始化方法,后來才知道這個方法是系統庫提供,為了防止該路徑下的日志文件被 iCloud 同步。可以參閱官方文檔: https://developer.apple.com/library/content/qa/qa1719/_index.html
關于mmap
上文中關于 appender_open 方法中有這么一句 OpenMmapFile(mmap_file_path, kBufferBlockLength, sg_mmmap_file)
,這個方法的實現就是使用了mmap方法
mmap的介紹
mmap是一種內存映射文件的方法,將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中的映射關系。維持關系之后,進程就可以采用指針的方式讀寫操作這一段內存,而系統會自動回寫臟頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調用read,write等系統調用函數。但內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享。
文件的一般讀寫操作為了提高讀寫效率和保護磁盤,使用了頁緩存機制。這樣造成讀文件時需要先將文件頁從磁盤拷貝到頁緩存中,由于頁緩存處在內核空間,不能被用戶進程直接尋址,所以還需要將頁緩存中數據頁再次拷貝到內存對應的用戶空間中。這樣,通過了兩次數據拷貝過程,才能完成進程對文件內容的獲取任務。寫操作也是一樣,待寫入的buffer在內核空間不能直接訪問,必須要先拷貝至內核空間對應的主存,再寫回磁盤中(延遲寫回),也是需要兩次數據拷貝。
總而言之,常規文件操作需要從磁盤到頁緩存再到用戶主存的兩次數據拷貝。而mmap操控文件,只需要從磁盤到用戶主存的一次數據拷貝過程。說白了,mmap的關鍵點是實現了用戶空間和內核空間的數據直接交互而省去了空間不同數據不通的繁瑣過程。因此mmap效率更高。
而之后我們可以通過使用mmap返回的指針 bufferPtr ,然后使用指針直接寫入數據。
Log接口
首先來看一下 Mars.framework 提供的log接口。
xlogger_IsEnabledFor(_level)
xlogger_AssertP(...)
xlogger_Assert(...)
xlogger_VPrint(...)
xlogger_Print(...)
xlogger_Write(...)
如果你已經使用過 Xlogger
從iOS的案例項目中可以看到LOG_INFO,這本身就是平臺適配層對Mars框架的log接口進行的一層封裝,其實是調用了xlogger_Write
方法,看一下具體實現。
void __xlogger_Write_impl(const XLoggerInfo* _info, const char* _log) {
if (!gs_appender) return;
...
if (NULL == _log) {
if (_info) {
XLoggerInfo* info = (XLoggerInfo*)_info;
info->level = kLevelFatal;
}
gs_appender(_info, "NULL == _log");
} else {
gs_appender(_info, _log);
}
}
而gs_appender就是上面所說appender函數,appender就是負責單行日志的寫入。
static void __appender_async(const XLoggerInfo* _info, const char* _log) {
ScopedLock lock(sg_mutex_buffer_async);
if (NULL == sg_log_buff) return;
char temp[16*1024] = {0}; //tell perry,ray if you want modify size.
PtrBuffer log_buff(temp, 0, sizeof(temp));
log_formater(_info, _log, log_buff);
if (!sg_log_buff->Write(log_buff.Ptr(), (unsigned int)log_buff.Length())) return;
if (sg_log_buff->GetData().Length() >= kBufferBlockLength*1/3 || (NULL!=_info && kLevelFatal == _info->level)) {
sg_cond_buffer_async.notifyAll();
}
}
異步的appender
方法可以歸納為:
- 對日志進行formater
- 寫入日志
- 發送通知。
而同步的Write的方法沒有最后發送通知的部分,在寫入的日志的部分也稍有不同。
然后看一下LogBuffer::Write
方法,根據同步模式和異步模式,Write 方法也分為了同步和異步,這里進一步查看一下異步模式的 Write 方法:
bool LogBuffer::Write(const void* _data, size_t _length) {
size_t before_len = buff_.Length();
size_t write_len = _length;
//1. 先進行流式壓縮
if (is_compress_) {
cstream_.avail_in = (uInt)_length;
cstream_.next_in = (Bytef*)_data;
uInt avail_out = (uInt)(buff_.MaxLength() - buff_.Length());
cstream_.next_out = (Bytef*)buff_.PosPtr();
cstream_.avail_out = avail_out;
if (Z_OK != deflate(&cstream_, Z_SYNC_FLUSH)) {
return false;
}
write_len = avail_out - cstream_.avail_out;
} else {
buff_.Write(_data, _length);
}
char crypt_buffer[4096] = {0};
size_t crypt_buffer_len = sizeof(crypt_buffer);
// 2. 在進行加密
s_log_crypt->CryptAsyncLog((char*)buff_.Ptr() + before_len, write_len, crypt_buffer, crypt_buffer_len);
// 3. 把壓縮加密的日志寫入mmap中
buff_.Write(crypt_buffer, crypt_buffer_len, before_len);
buff_.Length(before_len + crypt_buffer_len, before_len + crypt_buffer_len);
s_log_crypt->UpdateLogLen((char*)buff_.Ptr(), (uint32_t)crypt_buffer_len);
return true;
}
可以看到步驟這個異步LogBuffer中Write方法幾乎是重中之重,涉及到了數據流的壓縮、加密和寫入。
關于流式壓縮
看到了 Write 方法中出現這里的壓縮部分:
if (Z_OK != deflate(&cstream_, Z_SYNC_FLUSH)) {
return false;
}
這里deflate是壓縮數據流的算法. 任何需要流式壓縮的地方都可以用。它是由Huffman 編碼 和 LZ77壓縮 兩個算法組成。
**Huffman **編碼:huffman編碼是一種可變長編碼( VLC:variable length coding))方式,于1952年由huffman提出。依據字符在需要編碼文件中出現的概率提供對字符的唯一編碼,并且保證了可變編碼的平均編碼最短,被稱為最優二叉樹,有時又稱為最佳編碼。
** LZ77**:LZ77壓縮算法靠查找重復的序列. 這里使用術語:”滑動窗口”, 它的意思是:在任何的數據點上, 都記錄了在此之前的字符. 32k的滑動窗口表示壓縮器(解壓器)能記錄前32768個字符. 當下一個需要壓縮的字符序列能夠在滑動窗口中找到, 這個序列會被兩個數字代替: 一個是距離,表示這個序列在窗口中的起始位置離窗口的距離, 一個是長度, 字符串的長度.
舉例 : 將字符串“Blah blah b ”壓縮為“Blah b[D=5,L=5] ”
根據頂部文章的介紹,如之上的代碼所示,的確是單行日志壓縮,作為日志系統,應該避免造成CPU負擔,因此避免了多個日志同時壓縮的方式。
關于加密
這次開源沒有提供具體的加密算法,但是對單行日志進行了編碼。
先來說一下解碼的部分,剛開始使用 Mars 的 Xlogger 組件,看著輸出的xlog格式的文件卻不知道如何解碼,github也沒介紹怎么解碼,后來看了一下源碼,在 mars/log/crypt/ 目錄下有一個
decode_mars_log_file 的python文件,把Xlog格式的加密文件,放到該目錄下,跑一下腳本,就會生成解碼后的log文件。
至于加密的部分,如下圖所示,加密部分的代碼會將其日志變為如下格式,擁有日志頭信息和尾部,然后再變為二進制格式,然后最終寫入到xlog格式的文件中。
下面以同步模式的日志加密為例子:
void LogCrypt::CryptSyncLog(const char* const _log_data, size_t _input_len, char* _output, size_t& _output_len) {
uint16_t seq = __GetSeq(false);
uint32_t len = std::min(_input_len, _output_len - GetHeaderLen() - GetTailerLen());
memcpy(_output + GetHeaderLen(), _log_data, len);
_output[GetHeaderLen() + len] = kMagicEnd;
_output[0] = kMagicSyncStart;
memcpy(_output + 1, &seq, sizeof(seq));
struct timeval tv;
gettimeofday(&tv, 0);
time_t sec = tv.tv_sec;
tm tm_tmp = *localtime((const time_t*)&sec);
char hour = (char)tm_tmp.tm_hour;
memcpy(_output+3, &hour, sizeof(hour));
memcpy(_output+4, &hour, sizeof(hour));
memcpy(_output+5, &len, sizeof(len));
_output_len = GetHeaderLen() + GetTailerLen() + len;
}
于是把xlog對單行日志從壓縮到加密后寫進作為 log 中間 buffer的 mmap 的部分,大致梳理了一下,源碼還有很多細枝末節的地方,主體算是梳理完畢。