當(dāng)我們?cè)谧鱿到y(tǒng)開(kāi)發(fā)時(shí),日志系統(tǒng)是繞不開(kāi)的話題。作為日志系統(tǒng)的最終使用者,我們會(huì)接觸不同的日志系統(tǒng),比如 log4j、 logback 和 slf4j 等等,還會(huì)接觸到日志系統(tǒng)的各種概念,比如 Formatter、Appender 和 Priority 等。這些日志系統(tǒng)有什么區(qū)別,這些概念又該怎么理解呢?
今天我們就聊下:我們普通程序員,也就是日志系統(tǒng)最終使用者,可以怎么理解日志系統(tǒng)。
Logging 系統(tǒng)的雛型
讓我們回到計(jì)算機(jī)世界的遠(yuǎn)古時(shí)期或者我們剛剛接觸計(jì)算機(jī)世界的時(shí)期,那個(gè)時(shí)候我們有兩種調(diào)試程序的辦法:1)單步調(diào)試,一步步地跟蹤,查看代碼中變量的值。2) 是 printf 大法 —— 在特定的地方打印日志, 通過(guò)日志的輸出,幫助快速定位。
單步調(diào)試方法費(fèi)時(shí)費(fèi)力,但能準(zhǔn)確定位問(wèn)題。printf 大法簡(jiǎn)單粗暴,需要嘗試,大部分情況能快速找到問(wèn)題。單步調(diào)試和 printf 方法搭配使用,相得益彰。但是單步調(diào)試止步于 gdb 等調(diào)試工具,而 printf 大法最終發(fā)展出了一系列的日志系統(tǒng)。原因就在于單步調(diào)試在程序員調(diào)試才能用,而 printf 大法可以在調(diào)試和生產(chǎn)線上都能用,并且輸出的日志被各方面的人利用和解讀。
什么時(shí)候打印日志是個(gè)問(wèn)題 —— Level
printf 大法是很簡(jiǎn)陋的。在調(diào)試過(guò)程中,有可能日志打到很細(xì)粒度,比如每條數(shù)據(jù)的第三個(gè)字段是什么都打印出來(lái)了,但是真正運(yùn)行又要把這些細(xì)粒度的日志刪除。等到下次調(diào)試,我們又要知道每條數(shù)據(jù)的第三個(gè)字段是什么了。為此,我們希望日志打印是智能:調(diào)試或者線上出問(wèn)題的時(shí)候,各種細(xì)粒度的日志全部打印出來(lái),正常運(yùn)行的時(shí)候輸出一些最簡(jiǎn)單的信息就可以了。
針對(duì)這個(gè)問(wèn)題,日志系統(tǒng)引入日志級(jí)別(Level)的辦法解決。引入日志級(jí)別的概念之后,我們編程時(shí)打印日志,需要指明這條日志的級(jí)別。由于日志級(jí)別是最重要的參數(shù),現(xiàn)在的日志系統(tǒng)都是直接通過(guò)使用不同的函數(shù)來(lái)指明級(jí)別的,包括 logger.TRACE、logger.DEGUB、logger.INFO、logger.WARN、logger.ERROR、logger.FATAL。其中級(jí)別的對(duì)比是 TRACE < DEBUG < INFO <WARN < ERROR < FATAL。同時(shí)系統(tǒng)運(yùn)行行,我們將設(shè)定 log level設(shè)置在某一個(gè)級(jí)別上,那么級(jí)別優(yōu)先級(jí)高的 log 都能打印出來(lái),低的都不能打印出。例如,如果設(shè)置優(yōu)先級(jí)為WARN,那么FATAL、ERROR、WARN 級(jí)別的log能正常輸出,而INFO、DEBUG、TRACE 級(jí)別的log則會(huì)被忽略。
我們編程時(shí) DEBUG 或者 TRACE 級(jí)別打出細(xì)粒度的信息,比如每條數(shù)據(jù)的樣子。當(dāng)我們調(diào)試時(shí)或者線上有問(wèn)題時(shí),我們將程序的當(dāng)前日志級(jí)別設(shè)置成 TRACE 或者 DEBUG,從而將細(xì)粒度的信息打出來(lái)。而正常運(yùn)行時(shí),我們將當(dāng)前日志級(jí)別設(shè)置成 INFO 或者 WARN 級(jí)別,從而忽略細(xì)粒度的信息,降低 IO 操作和提升系統(tǒng)性能。
打印日志到哪里是還是一個(gè)問(wèn)題 —— Appender
有了 Level,我們可以隨心意寫點(diǎn) log 了,只要控制好日志級(jí)別就行。默認(rèn)情況下,我們的日志是打印到 Console 里,我們直接人眼看。隨著時(shí)間的推移,情況變得復(fù)雜起來(lái)。比如我們需要將日志打入文件里,方便以后查看。即使日志打到文件,我們也需要登錄到機(jī)器才能查看,我們需要在發(fā)生錯(cuò)誤時(shí)收到郵件或者短信。為了滿足這些需求,我們?cè)谌罩鞠到y(tǒng)中加入 Appender 部件。Appender 部件負(fù)責(zé)將日志寫的不同的目的。
比如下圖就是 Log4j 的日志配置示例。這個(gè)示例會(huì)打印出所有的信息,每次大小超過(guò)size,則這size大小的日志會(huì)自動(dòng)存入按年份-月份建立的文件夾下面并進(jìn)行壓縮,作為存檔。

日志系統(tǒng)可以自定多種 Appender,人們基于這套邏輯發(fā)明了一套日志收集和實(shí)時(shí)檢索的系統(tǒng),也稱之為日志系統(tǒng)。在這里為了區(qū)別,我們將日志收集和檢索系統(tǒng)稱之為日志收集檢索系統(tǒng)。日志收集檢索系統(tǒng)一般有 Kafka 流、實(shí)時(shí)處理組件 Spark Streaming 或者 Storm、實(shí)時(shí)檢索系統(tǒng) ElasticSearch 組成。后臺(tái)系統(tǒng)通過(guò)日志系統(tǒng)的 Appender 組件將日志打到 Kafka 流,然后 Spark Streaming 或者 Storm 處理流數(shù)據(jù)并將之寫入 ElasticSearch 檢索系統(tǒng)。這樣開(kāi)發(fā)和運(yùn)維就可以在 ElasticSearch 提供的 Web 查看可視化的日志了。日志收集檢索系統(tǒng)的主要好處是,1) 日志集中管理,不需要登錄到不同機(jī)器;2) 提供可視化的日志查看;3) 提供基于日志的監(jiān)控、監(jiān)測(cè)服務(wù)等。
目前最有名的日志收集檢索系統(tǒng)應(yīng)該是上圖的 Splunk 了。
日志什么樣也是個(gè)問(wèn)題 —— Formatter
有了日志的 Level 和 Appender,我們還需要解決日志樣式的問(wèn)題。一般情況,我們希望的日志格式包括:Level,函數(shù)名,文件名和打日志的代碼行數(shù)。這可以通過(guò)日志系統(tǒng)的 Formatter 組件來(lái)實(shí)現(xiàn)的。下圖是一個(gè) 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):
- # 如果你添加了多個(gè)handler,你會(huì)發(fā)現(xiàn)我們的定制消息被重復(fù)了多次,
- # 我們?cè)趓ecord里設(shè)置一個(gè)marker來(lái)避免
- 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 概念之后,整個(gè)日志系統(tǒng)的架子算是搭起來(lái)了,如下圖所示。作為一個(gè)普通程序員,可以安心使用這個(gè)日志系統(tǒng)了。
高效地打印日志是另外一個(gè)問(wèn)題 —— Efficient
但是作為一個(gè)有追求的普通程序,我們想知道大規(guī)模系統(tǒng)的極限環(huán)境中,日志系統(tǒng)能不能撐得住。答案嘛,是按照上面設(shè)計(jì)的日志系統(tǒng)是撐不住的。因?yàn)榇笠?guī)模系統(tǒng)的極限環(huán)境,實(shí)時(shí)要求高,不能忍受寫文件或者寫網(wǎng)絡(luò)的延遲。怎么辦呢?有請(qǐng)對(duì)付 IO 延遲利器 —— Buffer。基于 Buffer,并考慮到 Buffer 所帶來(lái)的線程同步的問(wèn)題, 人們?cè)O(shè)計(jì)了下面的方案。在這個(gè)方案中,每個(gè)線程生成一個(gè) Buffer,然后寫線程輪詢從 Buffer 讀入信息,并寫目的地。在這套方案中,寫日志并不會(huì)導(dǎo)致服務(wù)延遲。
除了架構(gòu)上的設(shè)計(jì),還有一些小 trick 提高性能。比如我們?cè)?log4j 官方 API 查到丑陋的 INFO 函數(shù)們。Java 語(yǔ)言不支持不定長(zhǎng)參數(shù)的情況下,log4j 強(qiáng)行搞一個(gè)支持不定長(zhǎng)的 INFO 函數(shù),就只能靠著寫不同的函數(shù)重載,最終也只支持 9 個(gè)參數(shù)。
但這些丑陋的 INFO 是有意義的。這些丑陋的 INFO 是為了能夠?qū)崿F(xiàn)下表中不定長(zhǎng)參數(shù)的方式。這種不定長(zhǎng)參數(shù)方式相比字符串拼接方式的區(qū)別在哪里呢? 當(dāng)前級(jí)別是 ERROR 時(shí),INFO 級(jí)別的信息是不用輸出的,字符串拼接方式還是要拼字符串,而不定長(zhǎng)參數(shù)方式就可以不用拼接字符串了。
總結(jié)