從Mars的Xlog組件學習日志系統

看完了微信團隊對Xlog的整體介紹,迫不及待開始了研究,理論部分我是完全參考微信終端跨平臺組件 mars 系列(一) - 高性能日志模塊xlog 這篇文章。
看完這篇理論,文章通過各個點來闡述如何創造這樣的高性能、可擴展的日志系統的想法和設計,于是我結合源碼配合一起梳理一下Xlog的幾個部分,無論以后自己造輪子作為借鑒或者使用這個輪子,都有很多價值。

方案:
使用流式壓縮方式對單行日志進行壓縮,壓縮加密后寫進作為 log 中間 buffer的 mmap 中,當 mmap 中的數據到達一定大小后再寫進磁盤文件中

從github上的官方文檔來看,Xlog的初始化涉及三個語句:

  1. xlogger_SetLevel(kLevelDebug);
  2. appender_set_console_log(true);
  3. appender_open(kAppednerAsync, [logPath UTF8String], "Test");

頭兩句第一個是設置log級別,第二句是控制臺是否打印語句的開關。
最重要的是第三句話 appender_open
Appender模塊是是整個日志系統負責寫日志的模塊。這里直接從頂部文章搬運一下結構圖片。這里appender_open是打開log目錄下的日志文件,進行一些初始化操作。

E6D06D6B-F811-404D-8B98-634FA8B06C19.png
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函數,實現自己的一套方案,也即是頂部文章所說的熱插拔。

題外話:

  1. 剛開始使用時發現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等系統調用函數。但內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享。

A2179609-D37C-45D7-A181-1C3E225A69BB.png

文件的一般讀寫操作為了提高讀寫效率和保護磁盤,使用了頁緩存機制。這樣造成讀文件時需要先將文件頁從磁盤拷貝到頁緩存中,由于頁緩存處在內核空間,不能被用戶進程直接尋址,所以還需要將頁緩存中數據頁再次拷貝到內存對應的用戶空間中。這樣,通過了兩次數據拷貝過程,才能完成進程對文件內容的獲取任務。寫操作也是一樣,待寫入的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方法可以歸納為:

  1. 對日志進行formater
  2. 寫入日志
  3. 發送通知。

而同步的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壓縮 兩個算法組成。

  1. **Huffman **編碼:huffman編碼是一種可變長編碼( VLC:variable length coding))方式,于1952年由huffman提出。依據字符在需要編碼文件中出現的概率提供對字符的唯一編碼,并且保證了可變編碼的平均編碼最短,被稱為最優二叉樹,有時又稱為最佳編碼。

  2. ** 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格式的文件中。


6E0EE9E2-7896-449F-AFD9-60341BC892FD.png

下面以同步模式的日志加密為例子:

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 的部分,大致梳理了一下,源碼還有很多細枝末節的地方,主體算是梳理完畢。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容

  • 在應用程序中添加日志記錄總的來說基于三個目的:監視代碼中變量的變化情況,周期性的記錄到文件中供其他應用進行統計分析...
    時待吾閱讀 5,072評論 1 13
  • 在應用程序中添加日志記錄總的來說基于三個目的:監視代碼中變量的變化情況,周期性的記錄到文件中供其他應用進行統計分析...
    時待吾閱讀 5,010評論 0 6
  • 單機存儲引擎就是哈希表、B樹等數據結構在機械磁盤、SSD等持久化介質上的實現。單機存儲系統是單機存儲引擎的一種封裝...
    olostin閱讀 2,526評論 0 5
  • log4j 1.1 簡介 Log4j是一個由Java編寫可靠、靈活的日志框架,是Apache旗下的一個開源項目;現...
    賈博巖閱讀 7,938評論 1 32
  • 喣風十里,柳堤一路,湘南蘇仙桃源處。古樟千年再逢春,車馬相繼鄉人駐。 炮竹沖天,福聯盈戶,紅泥小爐親人簇。 噓寒問...
    從八到九閱讀 162評論 0 0