納秒級高性能日誌系統 · ATC 2018

2021-02-19 真沒什麼邏輯

『看看論文』是一系列分析計算機和軟體工程領域論文的文章,我們在這個系列的每一篇文章中都會閱讀一篇來自 OSDI、SOSP 等頂會中的論文,這裡不會事無巨細地介紹所有的細節,而是會篩選論文中的關鍵內容,如果你對相關的論文非常感興趣,可以直接點擊連結閱讀原文。

本文要介紹的是 2018 年 ATC 期刊中的論文 —— NanoLog: A Nanosecond Scale Logging System[^1],該論文實現的 NanoLog 是高性能的日誌系統,與 C++ 社區中的其他日誌系統,例如:spdlog、glog 和 Boost Log 相比,它的性能可以高出 1 ~ 2 個數量級,我們在這篇文章中來簡要分析 NanoLog 的設計與實現原理。

日誌是系統可觀測性的重要一環,相信很多工程師都有線上出問題臨時加日誌查問題的經歷,作者剛剛又重新經歷了這一過程,稍有經驗的開發者都會在系統中加入很多日誌方便生產環境的問題排查,更有經驗的開發者會謹慎地在系統中(尤其是低延遲的實時系統)添加日誌,因為列印日誌這件看起來簡單的事情實際上會帶來很大的額外開銷。

列印日誌是一件簡單的事情,幾乎所有的工程師在踏上編程之路的第一天起就學會了如何使用 printf 等函數向標準輸出列印字符串,在 99% 的程序中使用這種做法都不會帶來性能問題,只是在生產環境中我們會使用結構化的、容易解析的格式替代 printf,例如:在日誌中加入時間戳、文件名和行數等信息。

圖 1 - 常見的日誌系統

但是剩下 1% 的程序要求超低延遲的系統,它們對響應時間的要求可能是微秒甚至納秒級別的。在這種場景下,如果仍然需要日誌系統提供可觀測性,我們需要深入研究向標準輸出或者文件寫日誌的細節,例如:使用緩衝區、異步寫入文件以及減少反射等動態特性的使用,除此之外,我們還要保證日誌輸出的順序、避免消息丟失或者截斷。

spdlog、glog 和 Boost Log 已經能夠滿足絕大多數應用程式的需求,但是對於這些延遲機器敏感的應用程式,列印日誌需要的幾微秒可能會顯著地增加請求的處理時間影響程序的性能,而今天分享的 NanoLog 是一個可以在納秒尺度列印日誌的系統。

架構設計

NanoLog 能在毫秒尺度列印日誌是因為它能夠在編譯期間提取靜態日誌消息並在離線階段處理日誌的格式化等問題。其核心優化都建立在以下兩個條件上:

開發者可直接閱讀的格式化日誌不一定要在應用程式運行時直接生成,我們可以在運行期間記錄日誌的動態參數並在運行後按需生成;日誌消息中的絕大部分信息都是靜態冗餘的,只有少數參數會發生變化,我們可以在編譯期間獲取日誌的靜態內容並僅在後處理器中列印一次;

正是因為大多數日誌都遵循上述特性,所以 NanoLog 可以在它們的基礎上實現納秒尺度的日誌列印。NanoLog 的不同設計方式決定了它與傳統日誌模塊會在架構上有很大的差異,它由以下三個組件構成:

圖 2 - NanoLog 系統架構
預處理器:在編譯期間從原始碼提取日誌消息、將原始日誌語句替換成優化過的代碼並為每條日誌消息生成壓縮、字典輔助函數;運行時庫:在緩衝區中緩存多個線程列印的日誌並使用預處理階段生成的輔助函數輸出壓縮後的二進位日誌;解碼器:利用預處理階段生成的包含靜態信息的字典解碼日誌,獲取可以被人類閱讀的日誌;

傳統的日誌系統都僅包含運行時,我們只需要在開發時引入頭文件、連結靜態庫或者動態庫並在代碼中指定的位置輸出日誌,日誌系統會程序運行期間將日誌列印到標準輸出或者指定文件。

但是 NanoLog 打破了上述這種傳統的設計,為了減少運行時的開銷,我們將一部分操作遷移到了編譯期間和運行後,這也遵循工作質量守恆定律:工作不會憑空消失,它只會從運行時轉移到程序生命周期的其他階段,NanoLog 就使用了下面的操作降低了日誌系統的開銷:

在編譯時重寫了日誌語句移除靜態信息並將昂貴的格式化操作推遲到代碼運行後的階段,這能夠在運行時減少大量的計算和 I/O 帶寬需求;為每個日誌消息編譯了特定代碼,能夠高效地處理動態參數,避免在運行時解析日誌消息並編碼參數類型;使用輕量級的壓縮算法以及不保序的日誌減少運行時的 I/O 和處理時間;使用後處理組合壓縮的日誌數據和編譯時提取的靜態信息生成可讀的日誌;實現原理

這一節我們將簡要分析 NanoLog 系統中三大組件,即預處理、運行時和後處理的具體實現原理。

預處理

NanoLog 使用 Python 來實現預處理器,程序的入口在 processor/parser 中,它會掃描用戶的源文件、生成元數據文件和修改後的原始碼,接下來會將這些修改後的代碼編譯到 .so 或者 .a 文件中,而不是編譯初始的代碼。除此之外,預處理器還會讀取生成的所有元數據文件、生成 C++ 原始碼並編譯到 NanoLog 的運行時庫和最後的用戶程序中:

圖 3 - 預處理器

它會為原始碼中的每個 NANO_LOG 生成兩條語句,即 record 和 compress,其中前者記錄日誌中的動態參數,後者壓縮記錄的數據減少程序的 I/O 時間。下面是預處理器生成的 record 函數:

inline void __syang0__fl__E32374s3237424642lf__s46cc__100__(NanoLog::LogLevel level, const char* fmtStr , const char* arg0, int arg1, int arg2, double arg3) {
    extern const uint32_t __fmtId__E32374s3237424642lf__s46cc__100__;

    if (level > NanoLog::getLogLevel())
        return;

    uint64_t timestamp = PerfUtils::Cycles::rdtsc();
    size_t str0Len = 1 + strlen(arg0);;
    size_t allocSize = sizeof(arg1) + sizeof(arg2) + sizeof(arg3) +  str0Len +  sizeof(NanoLogInternal::Log::UncompressedEntry);
    NanoLogInternal::Log::UncompressedEntry *re = reinterpret_cast<NanoLogInternal::Log::UncompressedEntry*>(NanoLogInternal::RuntimeLogger::reserveAlloc(allocSize));

    re->fmtId = __fmtId__E32374s3237424642lf__s46cc__100__;
    re->timestamp = timestamp;
    re->entrySize = static_cast<uint32_t>(allocSize);

    char *buffer = re->argData;

    // Record the non-string arguments
    NanoLogInternal::Log::recordPrimitive(buffer, arg1);
 NanoLogInternal::Log::recordPrimitive(buffer, arg2);
 NanoLogInternal::Log::recordPrimitive(buffer, arg3);

    // Record the strings (if any) at the end of the entry
    memcpy(buffer, arg0, str0Len); buffer += str0Len;*(reinterpret_cast<std::remove_const<typename std::remove_pointer<decltype(arg0)>::type>::type*>(buffer) - 1) = L'\0';

    // Make the entry visible
    NanoLogInternal::RuntimeLogger::finishAlloc(allocSize);
}

每個函數都包含特定的 fmtId,日誌的壓縮和解壓也都會用到這裡生成的標識符,上述函數還會為參數分配內存空間並按照順序調用 recordPrimitive 將所有參數記錄到緩衝區中。壓縮使用的 compress 也遵循類似的邏輯:

inline ssize_t
compressArgs__E32374s3237424642lf__s46cc__100__(NanoLogInternal::Log::UncompressedEntry *re, char* out) {
    char *originalOutPtr = out;

    // Allocate nibbles
    BufferUtils::TwoNibbles *nib = reinterpret_cast<BufferUtils::TwoNibbles*>(out);
    out += 2;

    char *args = re->argData;

    // Read back all the primitives
    int arg1; std::memcpy(&arg1, args, sizeof(int)); args +=sizeof(int);
 int arg2; std::memcpy(&arg2, args, sizeof(int)); args +=sizeof(int);
 double arg3; std::memcpy(&arg3, args, sizeof(double)); args +=sizeof(double);

    // Pack all the primitives
    nib[0].first = 0x0f & static_cast<uint8_t>(BufferUtils::pack(&out, arg1));
 nib[0].second = 0x0f & static_cast<uint8_t>(BufferUtils::pack(&out, arg2));
 nib[1].first = 0x0f & static_cast<uint8_t>(BufferUtils::pack(&out, arg3));

    if (true) {
        // memcpy all the strings without compression
        size_t stringBytes = re->entrySize - (sizeof(arg1) + sizeof(arg2) + sizeof(arg3) +  0)
                                            - sizeof(NanoLogInternal::Log::UncompressedEntry);
        if (stringBytes > 0) {
            memcpy(out, args, stringBytes);
            out += stringBytes;
        }
    }

    return out - originalOutPtr;
}

日誌的記錄和壓縮函數都是 Python 的預處理器分析原始碼生成的,工程師在開始時使用的 NANO_LOG 會被預處理器展開成新的代碼,該預處理器與 C++ 的預處理器有非常相似的功能,只是這裡需要展開的代碼過於複雜,我們很難在 C++ 中使用預處理器實現。

運行時

應用程式靜態連結的 NanoLog 運行時會通過線程上的緩衝區解耦記錄動態參數的低延時 record 操作和磁碟 I/O 等高延遲操作。線程上的緩衝區會存儲 record 方法調用的結果,這些數據對後臺的壓縮線程也是可見的:

圖 4 - 運行時

線程上用於暫存數據的緩衝區是程序提升性能的關鍵,我們要儘可能滿足避免鎖競爭和緩存一致性帶來的額外開銷,暫存緩衝區使用環形隊列和單生產者的消費者模型降低數據的同步開銷。

圖 5 - 環形隊列

環形隊列是一個連續的、使用固定大小緩存的數據結構,它的頭尾是相連的,非常適合緩存數據流,Linux 內核就使用這種數據結構作為 Socket 的讀寫緩衝區[^2],而音視頻也會使用環形隊列暫存剛剛收到還沒有被解碼的數據。

struct __kfifo {
 unsigned int in;
 unsigned int out;
 unsigned int mask;
 unsigned int esize;
 void  *data;
};

運行時不僅通過無鎖環形隊列提升性能,還需要解決環形隊列中的日誌消費問題。為了提高後臺線程的處理能力,運行時會將日誌的組裝推遲到後處理並壓縮日誌數據減少 I/O 高延遲帶來的影響。

後處理

後處理器的實現相對比較簡單,因為每條日誌都包含特定的標識符,後處理器會根據該標識符在壓縮後的日誌頭中找到編譯時生成的相關信息,並根據這些信息展開和解壓日誌。

需要注意的是,因為在運行期間每個線程都有自己的暫存緩衝區存儲日誌,所以 NanoLog 最終列印的日誌不能嚴格保證時間順序,它只能保證日誌輸出的順序在大體上是有序的。

總結

NanoLog 是設計非常有趣、值得學習的日誌系統,但是它並不適合所有的項目,它將原本運行時需要完成的工作轉移到了編譯期間和後處理階段,減輕了程序運行時的負擔,但是其輸出的二進位日誌是無法直接閱讀的,這也增加了開發者處理日誌的工作量。

雖然論文中提到,日誌分析引擎基本都會收集、解析並分析工程師可以直接閱讀的日誌,而大多數的時間都花費在了日誌的讀取和解析上,使用二進位的日誌不僅能夠減少讀取和解析的時間,還能減少昂貴的 I/O 和使用的帶寬,但是這點在我們的系統中是否是優點就見仁見智了。

日誌使用二進位的輸出格式確實能夠降低多方面的成本,但是它不僅需要日誌收集和解析系統的支持,還犧牲了開發者本地調試的體驗,而如果沒有自動收集並解壓日誌的模塊,手動解壓線上日誌排查問題是非常糟糕的體驗,不過在極端的性能場景下我們可能也沒有太多的選擇,哪怕犧牲體驗可能也要上。

相關焦點

  • go-carbon 1.2.3 版本發布了,新增毫秒、微妙、納秒級時間戳輸出
    github.com/golang-module/carbon")// 使用gitee庫go get -u gitee.com/go-package/carbonimport ( "gitee.com/go-package/carbon")更新日誌
  • Linux系統logger日誌命令詳解
    -p priority_level    指定輸入消息的優先級,優先級可以是數字或者指定為 「facility.level」 的格式。比如:"-p local3.info " local3 這個設備的消息級別為info。默認級別是 「user.notice」。-s    將消息記錄到標準錯誤以及系統日誌。-t tag    指定標記,標記記錄中的每一行。
  • 日誌易饒琛琳:極客邦科技CNUTCon日誌處理技術前瞻
    日誌易饒琛琳:極客邦科技CNUTCon日誌處理技術前瞻 2018 年 11 月 16 日,極客邦科技舉辦的CNUTCon 全球運維技術大會將於上海如期舉行。
  • Linux-系統日誌簡介
    前言日誌在排查文件的時候至關重要,在Linux上一般跟系統相關的日誌默認都會放到/var/log下面。1、/var/log/boot.log一般包含系統啟動時的日誌,包括自啟動的服務。3、/var/log/croncron計劃任務的日誌,每當cron任務被執行的時候都會在這個文件裡面記錄。4、/var/log/dmesg包含內核緩衝信息(kernel ring buffer)。在系統啟動時,會在屏幕上顯示許多與硬體有關的信息。
  • 提供高性能交易系統服務,「艾科朗克」完成5000萬A輪融資
    依託自主研發的FPGA硬體加速技術,公司為證券、期貨、基金公司提供國際領先的微秒級(百萬分之一秒)量化交易整體解決方案。「艾科朗克」產品體系覆蓋四大期貨交易所、兩大證券交易所,幫助實現極速交易並大幅提升盈利空間。據介紹,相較國外成熟市場,中國量化交易市場規模還存在很大成長空間,高性能交易系統是行業玩家的必爭之地。
  • 微信支付萬億日誌在Hermes中的實踐
    一、業務規模 目前微信支付日誌單日最大入庫總量已達到萬億級,單日入庫存儲量達 PB 級,而在春節等重大節假日預計整個日入庫規模會有進一步的增長。
  • 1號店11.11:從應用架構落地點談高可用高並發高性能
    1.背景 1.1三高是電商核心交易系統的基礎電商核心交易系統有很多特點,如分布式、高可擴展等,在眾多特性中,高可用、高並發、高性能是基礎。大到技術峰會、論壇、研討會,小到一場面試,高可用、高並發、高性能始終是焦點,是技術大牛、技術追隨者永遠津津樂道的話題,成為他們畢生的追求。
  • 釐米級定位、300萬年誤差一秒 北鬥系統這些「絕活」很強大
    最後一顆北鬥三號衛星目前正在發射架上等待發射,而它的兄弟們,已經在太空中為我們服務多時,那麼北鬥系統究竟有多準呢?定位準增強後定位達釐米級導航衛星能力強不強,首先要看定位準不準這個基本功。北鬥系統的水平和高程定位精度實測均優於5米,通過遍布全國2600個地基增強站組成的地基增強系統,可以提供米級、分米級、釐米級等增強定位精度服務。
  • 釐米級定位、300萬年誤差一秒……北鬥系統這些「絕活」很強大
    央視網消息:最後一顆北鬥三號衛星目前正在發射架上等待發射,而它的兄弟們,已經在太空中為我們服務多時,那麼北鬥系統究竟有多準呢?  定位準  增強後定位達釐米級北鬥系統的水平和高程定位精度實測均優於5米,通過遍布全國2600個地基增強站組成的地基增強系統,可以提供米級、分米級、釐米級等增強定位精度服務。拿汽車導航舉例,原來能定位到車在哪條路,現在能知道車在主路還是輔路,以後北鬥可以精確定位到哪條車道。  北鬥高精度定位服務提供商 陳金培:這個基站基本上覆蓋了中國的大部分地方,除了少部分無人區以外。
  • Linux 日誌文件系統原來是這樣工作的
    日誌文件系統(Journal File System)就是為解決上述問題而誕生的。它的原理是在進行寫操作之前,把即將進行的各個步驟(稱為transaction)事先記錄下來,保存在文件系統上單獨開闢的一塊空間上,這就是所謂的日誌(journal),也被稱為write-ahead logging,日誌保存成功之後才進行真正的寫操作、把文件系統的元數據和用戶數據寫進硬碟(稱為checkpoint),這樣萬一寫操作的過程中掉電,下次掛載文件系統之前把保存好的日誌重新執行一遍就行了
  • 國家重點研發計劃「高性能計算」重點專項 「E級計算機關鍵技術...
    由江南計算技術研究所牽頭的國家重點研發計劃「高性能計算」重點專項「E級計算機關鍵技術驗證系統」項目檢查會於2018年5月30日在無錫召開。錢德沛教授等總體專家組專家、項目和課題負責人,以及專項辦等有關人員共計40餘人參加了會議。
  • X-Paxos —— 阿里巴巴的高性能分布式強一致Paxos獨立基礎庫
    【IT168 評論】Paxos(分布式一致性算法)作為分布式系統的基石,一直都是計算機系統工程領域的熱門話題。Paxos號稱是最難理解的算法,其實現真的這麼困難麼?「X-Paxos」是阿里巴巴資料庫團隊面向高性能、全球部署以及阿里業務特徵等需求,實現的一個高性能分布式強一致的Paxos獨立基礎庫。X-Paxos具體又有哪些優勢,能給現有的系統帶來什麼用的收益呢?
  • 英特爾引領百億億級高性能計算創新為石油勘探創造加速度
    這一過程要涉及到TB級、甚至是PB級海量數據的快速處理,只有高性能計算技術才能擔此重任。那麼百億億級高性能計算技術又能為石油勘探帶來哪些新的助力呢?簡而言之就是更高的相關數據處理效率與更出色的分析或模擬精度。
  • TPS公司納秒脈衝等離子體點火技術完成多缸發動機點火試驗 可提高...
    (圖源:TPS官網)蓋世汽車訊 據外媒報導,TPS公司開發納秒脈衝等離子體點火系統,以提高燃油效率和減少溫室氣體排放。該公司已在上路車輛的天然氣發動機上演示了納秒脈衝等離子點火技術,首次完成多缸測試。
  • 高性能交易系統服務商艾科朗克獲5000萬A輪融資,灃揚資本領投
    依託自主研發的FPGA硬體加速技術,艾科朗克為證券、期貨、基金公司提供國際領先的微秒級(百萬分之一秒)量化交易整體解決方案。其產品體系覆蓋四大期貨交易所、兩大證券交易所,為極速交易保駕護航並大幅提升盈利空間的同時,在穩定性、安全性、易用性上注重細節和用戶體驗,市場份額遙遙領先。相較國外成熟市場,中國量化交易市場規模有巨大成長空間,高性能交易系統是行業玩家的必爭之地。
  • 納秒雷射、皮秒雷射、飛秒雷射,你分得清嗎?
    大家對雷射加工並不陌生,但你對經常能聽到的納秒雷射、皮秒雷射、飛秒雷射等,你是否能分得清呢?0.001秒=10-3秒1μs (微秒)=0.000001=10-6秒1ns (納秒為了連接電子元件,一般需要在板上鑽高達數十萬個μm級的小孔。因此保證基底的穩定性不會受到鑽孔過程時熱輸入的影響就變得十分重要,皮秒雷射正是這個應用的理想工具。皮秒雷射能以衝擊鑽探的方式完成孔的加工,並保證孔的均勻性。除了電路板,皮秒雷射還可以對塑料薄膜、半導體、金屬膜和藍寶石等材料進行高質量鑽孔。
  • C++ 20 來了,系統級軟體開發的春天來了!
    為此【全球C++及系統軟體技術大會】秉承「全球專家、連接智慧」的理念,特邀全球C++和系統級軟體技術領域的大師、專家、學者,匯聚一堂,大會圍繞C++ 20新標準、架構與設計、工程與效能、安全與可信計算、系統級軟體及其他程式語言,深度探討系統級軟體技術領域的最佳工程實踐和前沿方法。此次大會必將成為系統軟體領域規模最大、陣容最強、乾貨最多的高規格技術盛會!
  • win10:怎麼才能看系統操作日誌呢?看看win10日誌獲取方法吧
    如何在自己電腦的Win10系統上進行獲取所報錯的系統日誌?當計算機出現Windows故障或者嚴重的藍屏時,許多用戶都會在Windows系統上檢查相應的系統日誌文件,以了解詳細的原因。那麼怎麼查看系統日誌?跟隨系統總裁小編來看看吧。
  • AliSQL X-Cluster基於X-Paxos的高性能強一致MySQL資料庫
    X-Paxos是阿里巴巴自主研發的一致性協議庫,目標是填補市面上高性能、易接入的一致性協議庫的空白。而市面上已開源的一致性協議實現,包括etcd以及其他廠商等都存在或性能不夠,或功能上無法滿足複雜的現實應用場景需求的問題。
  • HBR-C 《管理日誌 2018》預售開始了!
    2017年,我們摘編12位管理學大師的名言,匯集成一本《管理日誌》。