分布式鏈路追蹤技術(shù):編寫(xiě)出“可觀測(cè)”的系統(tǒng)日志
?有在用,但是大多是 logback 和 log4j 2.x。雖然異步日志的效率 logback 和 log4j 2.x 相差無(wú)幾,但 log4j 2.x 仍有些微弱的優(yōu)勢(shì)。
日志接口框架
可以看到,Java 中是存在多種不同日志框架的實(shí)現(xiàn)的,這就會(huì)造成 2 個(gè)問(wèn)題:
多框架協(xié)作:在一個(gè)項(xiàng)目中,不光有你的代碼,還有各種各樣的框架代碼,比如分布式協(xié)調(diào)會(huì)用到 Zookeeper、Curator;RPC 通信會(huì)用到 Dubbo、Thrift。為了方便開(kāi)發(fā),業(yè)務(wù)系統(tǒng)中往往集成了許多第三方框架。我們需要日志來(lái)了解各個(gè)第三方框架之間的協(xié)作狀態(tài),這些第三方框架又依賴(lài)于各個(gè)日志框架進(jìn)行輸出。這時(shí)候如果直接使用像 logback、log4j 這樣的日志框架,豈不是業(yè)務(wù)系統(tǒng)要接入每個(gè)日志框架?
不同框架競(jìng)爭(zhēng):如果要引入多個(gè)日志框架,我們還需要考慮各個(gè)框架的輸出位置。要是多個(gè)日志框架同時(shí)寫(xiě)入一個(gè)日志文件,還會(huì)涉及競(jìng)爭(zhēng)問(wèn)題,導(dǎo)致性能無(wú)法發(fā)揮。
由此就出現(xiàn)了面向接口的日志框架,它提供了統(tǒng)一的 API。開(kāi)發(fā)人員在編寫(xiě)代碼的時(shí)候,直接使用這套面向接口的日志框架,當(dāng)業(yè)務(wù)項(xiàng)目人員在使用時(shí),只需要選擇好實(shí)現(xiàn)框架,就可以統(tǒng)一日志實(shí)現(xiàn)框架。

目前使用最為廣泛的日志接口框架是 SLF4J,出自 logback 的開(kāi)發(fā)者,目前基本已經(jīng)形成規(guī)范。SLF4J 提供了動(dòng)態(tài)占位符的功能,大大提高了程序的性能,無(wú)須開(kāi)發(fā)人員再對(duì)參數(shù)信息進(jìn)行拼接。
比如默認(rèn)情況下程序是 info 級(jí)別的,在原先的代碼方式中想要進(jìn)行日志輸出需要自行拼接字符串:
這會(huì)產(chǎn)生一個(gè)問(wèn)題,系統(tǒng)中如果存在大量類(lèi)似的代碼,同時(shí)系統(tǒng)只輸出 info 及 info 以上級(jí)別的日志,那么,在輸出 debug 日志時(shí)會(huì)產(chǎn)生大量的字符串,而并不會(huì)輸出 debug 日志,最后造成字符串不停地拼接,浪費(fèi)系統(tǒng)性能。此時(shí),SLF4J 就可以使用占位符的功能編寫(xiě)日志,比如像下面這樣:
通過(guò)這樣的形式,SLF4J 就可以根據(jù)日志等級(jí)判斷,只對(duì)符合要求的日志進(jìn)行數(shù)據(jù)拼接和打印。
有些時(shí)候日志輸出需要進(jìn)行數(shù)值計(jì)算,或者 JSON 轉(zhuǎn)換,此時(shí)就需要一定的計(jì)算任務(wù)。但方法調(diào)用如果被當(dāng)作參數(shù)傳遞的話一樣會(huì)被執(zhí)行,所以 Java8 中 SLF4J 還可以通過(guò) Supplier 來(lái)傳遞。如下所示:
可以看到,SLF4J 不僅為我們制定了一套面向接口開(kāi)發(fā)的方式,還為我們明確了如何有效地編寫(xiě)日志。這也是為什么越來(lái)越多人喜歡用 SLF4J。
日志編寫(xiě)方式
在詳細(xì)介紹了我們?cè)陂_(kāi)發(fā)時(shí)需要使用的日志框架后,我將正式展開(kāi)我們的標(biāo)題:“如何編寫(xiě)‘可觀測(cè)’的日志?”我會(huì)從日志編寫(xiě)位置、寫(xiě)入性能、占位符、可讀性、關(guān)鍵信息隱蔽、減少代碼位置信息的輸出、文件分類(lèi)、和日志 review 這 8 個(gè)點(diǎn)來(lái)講解,并將它們分成了 2 個(gè)方向:
日志開(kāi)發(fā)時(shí)(前 5 項(xiàng)):怎么樣寫(xiě)出更有效率的日志?
日志完成后(后 3 項(xiàng)):上線前后有哪些需要注意的?

日志編寫(xiě)位置
日志編寫(xiě)的位置可以說(shuō)是重中之重,好的日志位置可以幫你解決問(wèn)題,也可以讓你更加了解代碼的運(yùn)行情況。我總結(jié)了幾點(diǎn)比較重要的編寫(xiě)日志的位置,以供參考。
系統(tǒng)/應(yīng)用啟動(dòng)和參數(shù)變更:當(dāng)系統(tǒng)啟動(dòng)時(shí),可以將相關(guān)的參數(shù)信息進(jìn)行打印,以便出現(xiàn)問(wèn)題時(shí),更準(zhǔn)確地查詢(xún)?cè)颍粎?shù)信息可能并不存儲(chǔ)在本地,需要通過(guò)配置中心獲取,而參數(shù)信息有變更時(shí),也需要將變更后的內(nèi)容輸出在日志中。
關(guān)鍵操作節(jié)點(diǎn):最典型的就是在關(guān)鍵位置添加日志,記錄用戶(hù)進(jìn)行的某個(gè)操作。當(dāng)出現(xiàn)問(wèn)題時(shí),你可以通過(guò)這個(gè)位置的日志了解到用戶(hù)的操作。同樣你也可以在系統(tǒng)進(jìn)行某些操作時(shí)添加日志,比如你準(zhǔn)備啟動(dòng)某個(gè)線程池來(lái)進(jìn)行數(shù)據(jù)處理時(shí),可以加上日志便于以后分析問(wèn)題。
大型任務(wù)進(jìn)度上報(bào):當(dāng)系統(tǒng)在處理某個(gè)比較大型的任務(wù)時(shí),可以在這個(gè)過(guò)程中增加相關(guān)的日志來(lái)表明任務(wù)處理的進(jìn)度,防止因?yàn)殚L(zhǎng)時(shí)間沒(méi)有處理而無(wú)法得知程序執(zhí)行的狀態(tài),比如在文件下載時(shí),可以按照百分比來(lái)定時(shí)/定次地上報(bào)數(shù)據(jù)。
異常:當(dāng)程序出現(xiàn)異常時(shí),我們通常是通過(guò) try-catch 來(lái)記錄當(dāng)時(shí)的情況,然后以日志的形式表現(xiàn)出來(lái)。如果是通過(guò) try-catch 處理,你需要注意日志編寫(xiě)的位置。如果你需要將日志在本層拋出,則不需要進(jìn)行日志記錄,否則會(huì)出現(xiàn)日志重復(fù)的問(wèn)題。如果你除了異常以外還需要記錄其他的內(nèi)容,則可以通過(guò)定制異常信息來(lái)實(shí)現(xiàn)。
寫(xiě)入性能
日志的寫(xiě)入性能則會(huì)受到如下 5 個(gè)因素的影響:
日志編寫(xiě)位置:日志編寫(xiě)的位置在程序中十分重要,如果在 for 循環(huán)中編寫(xiě),因?yàn)檫@個(gè)循環(huán)會(huì)持續(xù)很多次,那么就會(huì)產(chǎn)生大量的日志記錄。此時(shí)可以考慮一下,這個(gè)日志的記錄是否有必要。
日志數(shù)量:如果你大量地編寫(xiě)日志,那么日志的質(zhì)量一定會(huì)降低。同時(shí),大量的日志會(huì)讓你很難去查看問(wèn)題,反而成了一種負(fù)擔(dān)。在高訪問(wèn)量時(shí),過(guò)多的日志也會(huì)影響程序的執(zhí)行效率。
日志編寫(xiě)等級(jí):我在上一節(jié)中講過(guò),日志等級(jí)很容易被濫用,不正確的日志等級(jí)會(huì)導(dǎo)致我們查詢(xún)問(wèn)題的時(shí)間增加。
日志輸出級(jí)別:這里指的是對(duì)于配置日志輸出級(jí)別的選擇。在線上環(huán)境,不建議使用 debug 級(jí)別,因?yàn)榫€上一直有請(qǐng)求,debug 級(jí)別會(huì)輸出大量的基礎(chǔ)和請(qǐng)求信息,極其浪費(fèi)資源,因此建議開(kāi)啟 info 或者以上。
無(wú)用輸出參數(shù):不對(duì)大字段、無(wú)用字段輸出,因?yàn)檫@很影響程序執(zhí)行效率和日志的內(nèi)容。我曾遇到一個(gè)案例,A 同學(xué)在線上打印了一個(gè)完整的 HTML 內(nèi)容,導(dǎo)致當(dāng)日的部分日志內(nèi)容錯(cuò)亂,部分日志無(wú)法檢索,原因就在于 HTML 存在多行內(nèi)容導(dǎo)致無(wú)法解析的問(wèn)題。當(dāng)開(kāi)發(fā)人員到線上服務(wù)器上查看時(shí),日志文件的大小已經(jīng)擴(kuò)大了 3 倍。
好的日志一定是便于你去排查問(wèn)題的。在編寫(xiě)日志時(shí)你一定要思考這個(gè)日志可以幫你做什么。
占位符
在介紹日志接口框架時(shí)我提到過(guò),在日志編寫(xiě)時(shí)盡可能地選擇基于占位符的編寫(xiě)方式。這里我會(huì)告訴你為什么要用占位符:
- 節(jié)約性能。在生成較高級(jí)別的日志時(shí),低級(jí)別的日志會(huì)不停疊加字符串而占用過(guò)多的內(nèi)存、CPU 資源,導(dǎo)致性能浪費(fèi)。
 - 便于編寫(xiě)。先確認(rèn)日志所想要表達(dá)的內(nèi)容,再確認(rèn)你所需要編寫(xiě)的參數(shù),這樣在寫(xiě)日志時(shí),目的也會(huì)更加明確。
 - 便于查看。在代碼 review 時(shí),更方便查看日志想表達(dá)的意思,而不會(huì)被日志的參數(shù)打亂。
 
可讀性
日志的可讀性也是日志編寫(xiě)的關(guān)鍵之一。一個(gè)好的日志就像一篇好的文章,能讓你很快了解到這個(gè)日志中的關(guān)鍵信息。我在工作中發(fā)現(xiàn)很多人寫(xiě)的日志都是無(wú)意義的,根本無(wú)法幫你定位問(wèn)題的根源,比如像下面的這段代碼:
這段代碼乍一看似乎沒(méi)什么問(wèn)題,但是運(yùn)行后系統(tǒng)會(huì)大量地輸出“數(shù)據(jù)保存成功!”的消息,這個(gè)輸出和沒(méi)有是一樣的,起不到任何的作用。
我總結(jié)了幾點(diǎn)在日志中容易遺漏的信息:
- 會(huì)話標(biāo)識(shí):當(dāng)前操作的用戶(hù)和與當(dāng)前請(qǐng)求相關(guān)的信息,類(lèi)似 Session。當(dāng)出現(xiàn)問(wèn)題/查看行為時(shí),可以根據(jù)這個(gè)值來(lái)快速識(shí)別到相關(guān)的日志。
 - 請(qǐng)求標(biāo)識(shí):每個(gè)請(qǐng)求都擁有一個(gè)唯一的標(biāo)識(shí),這樣在查看問(wèn)題時(shí),我們只需要查看這一個(gè)請(qǐng)求中的所有日志即可。一般我們會(huì)配合鏈路追蹤系統(tǒng)一同使用,因?yàn)楹笳呖梢詫?shí)現(xiàn)跨應(yīng)用的日志追蹤,從而幫助我們過(guò)濾掉不相關(guān)的信息。
 - 參數(shù)信息:在日志中增加參數(shù)信息能幫你了解到,是什么情況下產(chǎn)生的問(wèn)題,這樣你也很容易復(fù)現(xiàn)問(wèn)題,以及辨別錯(cuò)誤的原因。
 - 發(fā)生數(shù)據(jù)的結(jié)果:和參數(shù)信息相互對(duì)應(yīng),一個(gè)是執(zhí)行前,一個(gè)是執(zhí)行后。發(fā)生數(shù)據(jù)的結(jié)果可以幫你了解程序執(zhí)行的結(jié)果,出錯(cuò)時(shí)也是很重要的參考條件。
 
關(guān)鍵信息隱蔽
對(duì)于關(guān)鍵的信息不顯示或者進(jìn)行掩碼顯示,以免信息被盜取后出現(xiàn)數(shù)據(jù)內(nèi)容泄漏。推特在 2018 年曾將用戶(hù)打印在日志中,這一行為泄露了 3.3 億人。
減少代碼位置信息的輸出
如果不是必要,盡量不要在日志格式中輸出當(dāng)前日志所在的代碼行和方法名稱(chēng)信息。如果你看過(guò) logback,log4j 的源碼你就知道,這都是通過(guò)獲取當(dāng)前線程堆??煺招畔?lái)進(jìn)行實(shí)現(xiàn)的,這種實(shí)現(xiàn)方式會(huì)極大地影響程序執(zhí)行的效率。
在 log4j 的文檔中有這樣一段話:“使用同步方式進(jìn)行獲取位置信息會(huì)慢 1.3 到 5 倍,如果是使用異步日志,因?yàn)闀?huì)涉及跨線程獲取位置信息,會(huì)慢 30 到 100 倍。原文:https://logging.apache.org/log4j/2.0/manual/async.html#Location。
所以,關(guān)閉代碼位置信息的輸出可以節(jié)省系統(tǒng)資源的使用,提升性能。
文件分類(lèi)
文件分類(lèi)可以幫助你提高檢索日志信息時(shí)的效率。將不同的業(yè)務(wù)邏輯按照不同的日志文件來(lái)分類(lèi),可以保證你看到的信息都是和這個(gè)功能相關(guān)的,不會(huì)被其他的日志干擾。這也是在大型系統(tǒng)中經(jīng)常會(huì)使用到的功能。
比如拉勾的單點(diǎn)登錄系統(tǒng),就會(huì)將用戶(hù)的極驗(yàn)驗(yàn)證功能和登錄驗(yàn)證功能拆成兩個(gè)單獨(dú)的日志文件,當(dāng)出現(xiàn)問(wèn)題時(shí),可以根據(jù)相關(guān)功能的日志來(lái)快速篩查問(wèn)題,減少了篩選所需的時(shí)間。
日志 review
每一次功能上線后,除了要對(duì)業(yè)務(wù)功能進(jìn)行回歸,還要對(duì)日志進(jìn)行觀察,確認(rèn)日志內(nèi)容的輸出情況,比如日志內(nèi)容是否符合預(yù)期,會(huì)不會(huì)有不合適的地方?
好的日志不是一次就能寫(xiě)好的,一定是要和代碼一樣不停地迭代,才能寫(xiě)出更方便處理問(wèn)題,也更具有可讀性的日志。
日志管理
就像店家在賣(mài)出商品后還要負(fù)責(zé)其售后,編寫(xiě)完日志,對(duì)于它的管理也是十分重要的。好的日志管理方式可以提高閱讀日志的效率,而這需要開(kāi)發(fā)人員和運(yùn)維人員共同協(xié)作。
日志格式
日志的格式布局會(huì)影響運(yùn)維人員將這些日志內(nèi)容收集與管理的效率。如果編寫(xiě)者和管理者能夠通過(guò)協(xié)商,規(guī)定出一套完整的日志格式,這樣就能在排查問(wèn)題時(shí)事半功倍。
我會(huì)簡(jiǎn)單介紹幾點(diǎn)在日志編寫(xiě)時(shí)需要注意的事項(xiàng):
- 系統(tǒng)之間格式保持一致:每個(gè)應(yīng)用在日志格式上盡量保持統(tǒng)一,這樣,運(yùn)維人員在進(jìn)行日志收集時(shí),就可以采用統(tǒng)一的日志模板來(lái)收集和格式化內(nèi)容,減少雙方的溝通成本。
 - 不編寫(xiě)多行的日志內(nèi)容:除了異常堆棧信息,不對(duì)日志內(nèi)容進(jìn)行多行的輸出,多行的內(nèi)容十分不便于數(shù)據(jù)解析,可能會(huì)出現(xiàn)生成多條日志的情況。
 - 不要使用日志中的常見(jiàn)內(nèi)容來(lái)分割:如果采用日志中常見(jiàn)的內(nèi)容來(lái)分割,會(huì)在格式解析時(shí)出現(xiàn)問(wèn)題,比如用戶(hù) ID 中的空格就是比較常見(jiàn)的內(nèi)容。
 
日志歸檔
日志歸檔是一件很重要的事情。如果你將日志內(nèi)容全部寫(xiě)到一個(gè)文件中,這個(gè)日志文件會(huì)變得越來(lái)越煩冗,不利于日志的收集和查看。
一般情況下,我們會(huì)對(duì)日志按照日期來(lái)歸檔,每天生成一個(gè)日志文件,這樣在日志備份和恢復(fù)時(shí),可以按照日期來(lái)進(jìn)行。如果感覺(jué)天級(jí)別的日志仍然太大了,可以考慮按照小時(shí)細(xì)分。
結(jié)語(yǔ)
今天我們了解了日志編寫(xiě)的工具、日志編寫(xiě)需要注意的 8 個(gè)事項(xiàng)以及日志管理的方式,有哪些是你原來(lái)犯過(guò)的錯(cuò)誤,又有哪些是你原來(lái)沒(méi)有想到的呢?歡迎你在留言區(qū)分享與討論。希望你在日后的日志編寫(xiě)中可以注意到這些問(wèn)題。?















 
 
 














 
 
 
 