1. 程式人生 > >普通程式設計師怎麼理解日誌系統

普通程式設計師怎麼理解日誌系統

 當我們在做系統開發時,日誌系統是繞不開的話題。作為日誌系統的最終使用者,我們會接觸不同的日誌系統,比如 log4j、 logback 和 slf4j 等等,還會接觸到日誌系統的各種概念,比如 Formatter、Appender 和 Priority 等。這些日誌系統有什麼區別,這些概念又該怎麼理解呢?

      
      今天我們就聊下:我們普通程式設計師,也就是日誌系統最終使用者,可以怎麼理解日誌系統。

1. Logging 系統的雛型

      讓我們回到計算機世界的遠古時期或者我們剛剛接觸計算機世界的時期,那個時候我們有兩種除錯程式的辦法:1)單步除錯,一步步地跟蹤,檢視程式碼中變數的值。2) 是 printf 大法 —— 在特定的地方列印日誌, 通過日誌的輸出,幫助快速定位。

      單步除錯方法費時費力,但能準確定位問題。printf 大法簡單粗暴,需要嘗試,大部分情況能快速找到問題。單步除錯和 printf 方法搭配使用,相得益彰。但是單步除錯止步於 gdb 等除錯工具,而 printf 大法最終發展出了一系列的日誌系統。原因就在於單步除錯在程式設計師除錯才能用,而 printf 大法可以在除錯和生產線上都能用,並且輸出的日誌被各方面的人利用和解讀。

2. 什麼時候列印日誌是個問題 —— Level

      printf 大法是很簡陋的。在除錯過程中,有可能日誌打到很細粒度,比如每條資料的第三個欄位是什麼都打印出來了,但是真正執行又要把這些細粒度的日誌刪除。等到下次除錯,我們又要知道每條資料的第三個欄位是什麼了。為此,我們希望日誌列印是智慧:除錯或者線上出問題的時候,各種細粒度的日誌全部打印出來,正常執行的時候輸出一些最簡單的資訊就可以了。

      針對這個問題,日誌系統引入日誌級別 (Level) 的辦法解決。引入日誌級別的概念之後,我們程式設計時列印日誌,需要指明這條日誌的級別。由於日誌級別是最重要的引數,現在的日誌系統都是直接通過使用不同的函式來指明級別的,包括 logger.TRACE, logger.DEGUB, logger.INFO, logger.WARN, logger.ERROR, logger.FATAL。其中級別的對比是 TRACE < DEBUG < INFO <WARN < ERROR < FATAL。同時系統執行行,我們將設定 log level設定在某一個級別上,那麼級別優先順序高的 log 都能打印出來,低的都不能打印出。例如,如果設定優先順序為WARN,那麼FATAL、ERROR、WARN 級別的log能正常輸出,而INFO、DEBUG、TRACE 級別的log則會被忽略。

      我們程式設計時 DEBUG 或者 TRACE 級別打出細粒度的資訊,比如每條資料的樣子。當我們除錯時或者線上有問題時,我們將程式的當前日誌級別設定成 TRACE 或者 DEBUG,從而將細粒度的資訊打出來。而正常執行時,我們將當前日誌級別設定成 INFO 或者 WARN 級別,從而忽略細粒度的資訊,降低 IO 操作和提升系統性能。

      

3. 列印日誌到哪裡是還是一個問題 —— Appender

      有了 Level, 我們可以隨心意寫點 log 了,只要控制好日誌級別就行。預設情況下,我們的日誌是列印到 Console 裡,我們直接人眼看。隨著時間的推移,情況變得複雜起來。比如我們需要將日誌打入檔案裡,方便以後檢視。即使日誌打到檔案,我們也需要登入到機器才能檢視,我們需要在發生錯誤時收到郵件或者簡訊。為了滿足這些需求,我們在日誌系統中加入 Appender 部件。Appender 部件負責將日誌寫的不同的目的。

比如下圖就是 Log4j 的日誌配置示例。這個示例會打印出所有的資訊,每次大小超過size,則這size大小的日誌會自動存入按年份-月份建立的資料夾下面並進行壓縮,作為存檔。

      日誌系統可以自定多種 Appender,人們基於這套邏輯發明了一套日誌收集和實時檢索的系統,也稱之為日誌系統。在這裡為了區別,我們將日誌收集和檢索系統稱之為日誌收集檢索系統。日誌收集檢索系統一般有 kafka 流、實時處理元件 Spark Streaming 或者 Storm、實時檢索系統 ElasticSearch 組成。後臺系統通過日誌系統的 Appender 元件將日誌打到 kafka 流,然後 Spark Streaming 或者 Storm 處理流資料並將之寫入 ElasticSearch 檢索系統。這樣開發和運維就可以在 ElasticSearch 提供的 Web 檢視視覺化的日誌了。日誌收集檢索系統的主要好處是,1) 日誌集中管理,不需要登入到不同機器;2) 提供視覺化的日誌檢視;3) 提供基於日誌的監控、監測服務等。

目前最有名的日誌收集檢索系統應該是上圖的 Splunk 了。

4. 日誌什麼樣也是個問題 —— Formatter

      有了日誌的 Level 和 Appender, 我們還需要解決日誌樣式的問題。一般情況,我們希望的日誌格式包括:Level, 函式名,檔名和打日誌的程式碼行數。這可以通過日誌系統的 Formatter 元件來實現的。下圖是一個 Python 自定義的 Formatter。

import logging
 
def AltCustomFormatter(logging.Formatter):
    def __init__(self, fmt=None, datefmt=None):
        super(AltCustomFormatter, self).__init__(fmt, datefmt)
 
    def format(self, record):
        # 如果你添加了多個handler,你會發現我們的定製訊息被重複了多次,
        # 我們在record裡設定一個marker來避免
        if record.levelno > logging.INFO and not hasattr(record, 'is_custom'):
            record.msg = "[%s, %s, %s] %s" % (record.filename, record.lineno, record.funcName, record.msg)
            record.is_custom = True
 
        return super(AltCustomFormatter, self).format(record)

      在引入 Level, Formater 和 Appender 概念之後,整個日誌系統的架子算是搭起來了,如下圖所示。作為一個普通程式設計師,可以安心使用這個日誌系統了。

5. 高效地列印日誌是另外一個問題 —— Efficient

      但是作為一個有追求的普通程式,我們想知道大規模系統的極限環境中,日誌系統能不能撐得住。答案嘛,是按照上面設計的日誌系統是撐不住的。因為大規模系統的極限環境,實時要求高,不能忍受寫檔案或者寫網路的延遲。怎麼辦呢?有請對付 IO 延遲利器 —— Buffer。基於 Buffer, 並考慮到 Buffer 所帶來的執行緒同步的問題, 人們設計了下面的方案。在這個方案中,每個執行緒生成一個 Buffer, 然後寫執行緒輪詢從 Buffer 讀入資訊,並寫目的地。在這套方案中,寫日誌並不會導致服務延遲。

      除了架構上的設計,還有一些小 trick 提高效能。比如我們在 log4j 官方 API 查到醜陋的 INFO 函式們。1.5 之前的 Java 不支援不定長引數的情況下,log4j 強行搞一個支援不定長的 INFO 函式,就只能靠著寫不同的函式過載,最終也只支援 9 個引數。

      

但這些醜陋的 INFO 是有意義的。這些醜陋的 INFO 是為了能夠實現下表中不定長引數的方式。這種不定長引數方式相比字串拼接方式的區別在哪裡呢? 當前級別是 ERROR 時, INFO 級別的資訊是不用輸出的,字串拼接方式還是要拼字串,而不定長引數方式就可以不用拼接字串了。

日誌字串處理方式 示例 特點
不定長引數 logger.INFO("Encounter %s, but %s required", "current_value", "required_value") 日誌不輸出,就不用字串拼接。執行效率高。
字串拼接 logger.INFO("Encounter %s, but %s required".format("current_value", "required_value")) 日誌不輸出,也要拼接字串。

6. 總結

      上面就是日誌系統基本概念的介紹了。從遠古時期的 printf 大法到現代化的日誌系統,為了讓我們普通程式設計師也能直觀地瞭解系統執行狀態,大神們引入了 Level, Appender, Formatter 等日誌系統核心概念,開發了 log4j, logback 和 tinylog 等著名日誌系統。

歡迎工作一到十年的Java工程師朋友們加入Java進階高階架構:828545509

本群提供免費的學習指導 架構資料 以及免費的解答

不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導