最佳日誌實踐(v2.0)
0. 緣起
大約在三年前,我曾經寫過一篇 最佳日誌實踐,還被碼農周刊選為那年的 最受歡迎技術乾貨 之一。當時我任職於網易杭州研究院的存儲平台組,主要做網易對象存儲(NOS)的開發和部分運維工作。由於網易雲音樂,易信等幾個重要產品陸續上線,業務壓力劇增,我們的系統在前前後後大約半年的時間裡,出現了大大小小各種事故。通過不斷總結事故原因、不斷地優化代碼、進化部署架構,才使整個系統逐漸穩定下來。那個時候組裡人常常開玩笑說,我們採用的是TDD的開發模式,只是這個TDD不是測試驅動開發(Test Driven Development),而是悲劇驅動開發(Tragedy Driven Development)。
最佳日誌實踐的第一版便是在那個時候完成的,裡面包含了我們在開發和運維過程中的一些好的實踐。最初起名「最佳日誌實踐」實在有些標題黨,不過由於起名字是一件比寫代碼更困難的事兒,我就繼續沿用這個名字吧。有幾個原因讓我一直想要對那篇文章進行整理和擴充:
- 那篇文章里的一些內容太細節,涉及到了網易對象存儲中的業務邏輯,對讀者不夠友好;
- 那篇文章里一些內容基於Java語言來討論,實際上之後我有很多的精力都在基於Go語言做開發,因此現在更想要討論一些與語言無關的方面;
- 最近幾年又寫了若干個系統,對於日誌這件事情又有了一些心得和體會,也想拿來跟大家分享;
開始正文吧...
1. 什麼是日誌
日誌用來記錄用戶操作、系統運行狀態等,是一個系統的重要組成部分。然而,由於日誌通常不屬於系統的核心功能,所以常常不被團隊成員所重視。對於一些簡單的小程序,可能並不需要在如何記錄日誌的問題上花費太多精力。但是對於作為基礎平台為很多產品提供服務的後端程序,就必須要考慮如何依靠良好的日誌來保證系統可靠的運行了。
好的日誌可以幫助系統的開發和運維人員:
- 了解線上系統的運行狀態
- 快速準確定位線上問題
- 發現系統瓶頸
- 預警系統潛在風險
- 挖掘產品最大價值
- ……
不好的日誌導致:
- 對系統的運行狀態一知半解,甚至一無所知
- 系統出現問題無法定位,或者需要花費巨大的時間和精力
- 無法發現系統瓶頸,不知優化從何做起
- 無法基於日誌對系統運行過程中的錯誤和潛在風險進行監控和報警
- 對挖掘用戶行為和提升產品價值毫無幫助
- ……
2. 日誌的分類
日誌從功能來說,可分為診斷日誌、統計日誌、審計日誌。
診斷日誌, 典型的有:
- 請求入口和出口
- 外部服務調用和返回
- 資源消耗操作: 如讀寫文件等
- 容錯行為: 如雲硬碟的副本修復操作
- 程序異常: 如資料庫無法連接
- 後台操作:定期執行刪除的線程
- 啟動、關閉、配置載入
統計日誌:
- 用戶訪問統計:用戶IP、上傳下載的數據量,請求耗時等
- 計費日誌(如記錄用戶使用的網路資源或磁碟佔用,格式較為嚴格,便於統計)
審計日誌:
- 管理操作
對於簡單的系統,可以將所有的日誌輸出到同一個日誌文件中,並通過不同的關鍵字進行區分。而對於複雜的系統,將不同需求的日誌輸出到不同的日誌文件中是必要的,通過對不同類型的文件採用不同的日誌格式(例如對於計費日誌可以直接輸出為Json格式),可以方便接入其他的子系統。
3. 日誌中記錄什麼
理想的日誌中應該記錄不多不少的信息。
所謂不多,是指不要在日誌中記錄無用的信息。實踐中常見到的無用的日誌有:1)能夠放在一條日誌里的東西,放在多條日誌中輸出;2)預期會發生且能夠被正常處理的異常,列印出一堆無用的堆棧;3)開發人員在開發過程中為了調試方便而加入的「臨時」日誌
所謂不少,是指對於日誌的使用者,能夠從日誌中得到所有需要的信息。在實踐中經常發生日誌不夠的情況,例如:1)請求出錯時不能通過日誌直接來定位問題,而需要開發人員再臨時增加日誌並要求請求的發送者重新發送同樣的請求才能定位問題;2)無法確定服務中的後台任務是否按照期望執行;3)無法確定服務的內存數據結構的狀態;4)無法確定服務的異常處理邏輯(如重試)是否正確執行;5)無法確定服務啟動時配置是否正確載入;6)等等等等
輸出日誌時要考慮日誌的使用者,例如如果日誌主要由系統的運維人員來看,那就不能輸出:
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, ErrorCode:1426 n
至少應該是:
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, ErrorCode:1426, Message: callback request (to http://example.com/callback) failed due to socket timeoutn
這樣運維人員一眼就能清楚問題的原因,而不需要再通過開發來查看ErrorCode對應的具體錯誤。
整理一下通常情況下會遺漏的日誌:
- 系統的配置參數:系統在啟動過程中通常會首先讀啟動參數,可以在系統啟動後將這些參數輸出到日誌中,方便確認系統是按照期望的參數啟動的;
- 後台定期執行的任務:如定期更新緩存的任務,可以記錄任務開始時間,任務結束時間,更新了多少條緩存配置等等,這樣可以掌握定期執行的任務的狀態;
- 異常處理邏輯:如對於分散式存儲系統來說,當系統在一個存儲節點上讀數據失敗時,需要去另一個數據節點上進行重試,可以將讀數據失敗這件事情記錄下來,之後可以通過對日誌的分析確認是否某些節點的磁碟可能存在故障。再比如,如果系統需要請求一個外部資源,可以將請求這個外部資源偶爾失敗又重試成功這件事情記錄下來,具體來說:
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, auth request (to http://auth1.example.com/v2) timeout ... 1 tryn[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, auth request (to http://auth1.example.com/v2) timeout ... 2 tryn[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, auth request (to http://auth1.example.com/v2) successn
要好於
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, auth request (to http://auth1.example.com/v2) successn
因為前者可以讓我們預判被依賴的伺服器服務質量有風險,也許需要進行擴容;
- 日誌中需要記錄關鍵參數,出錯時的關鍵原因等。例如:
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, auth failedn[INFO] RequestID:b1946ac92492d2347c6235b4d2611185, content digest does not matchn[INFO] RequestID:b1946ac92492d2347c6235b4d2611186, request ip not in whitelistn
就不如:
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, auth failed due to token expirationn[INFO] RequestID:b1946ac92492d2347c6235b4d2611185, content digest does not match, expect 7b3f050bfa060b86ba781151c563c953, actual f60645e7107917250a6408f2f302d048n[INFO] RequestID:b1946ac92492d2347c6235b4d2611186, request ip(=202.17.34.1) not in whitelistn
4. 關於日誌級別
我們通常使用的日誌庫,將日誌基本分為以下幾類(從低到高):
TRACE – The TRACE Level designates finer-grained informational events than the DEBUGDEBUG – The DEBUG Level designates fine-grained informational events that are most useful to debug an application.INFO – The INFO level designates informational messages that highlight the progress of the application at coarse-grained level.WARN – The WARN level designates potentially harmful situations.ERROR – The ERROR level designates error events that might still allow the application to continue running.
FATAL – The FATAL level designates very severe error events that will presumably lead the application to abort.開發人員對於何種日誌輸出為何種級別通常有自己的理解,那在實踐中,是否所有的日誌級別都有必要存在,哪些操作需要記入日誌,哪種錯誤應該記為WARN級別,而哪種錯誤又為ERROR級別呢?關於該問題,可以參考StackOverflow上的一個討論。
此處對貼子中的一些觀點,加上我們在平時運維過程中遇到的相關問題進行歸納:
- 一個項目各個日誌級別的定義應該是清楚明確的,需要團隊的每個開發人員共同遵守;
- 即使是TRACE或者DEBUG級別的日誌,也應該有一定的規範,要保證除了開發人員自己以外,包括測試人員和運維人員都可以方便地通過日誌定位問題;
- 對於日誌級別的分類,有以下參考:FATAL — 表示需要立即被處理的系統級錯誤。當該錯誤發生時,表示服務已經出現了某種程度的不可用,系統管理員需要立即介入。這屬於最嚴重的日誌級別,因此該日誌級別必須慎用,如果這種級別的日誌經常出現,則該日誌也失去了意義。通常情況下,一個進程的生命周期中應該只記錄一次FATAL級別的日誌,即該進程遇到無法恢復的錯誤而退出時。當然,如果某個系統的子系統遇到了不可恢復的錯誤,那該子系統的調用方也可以記入FATAL級別日誌,以便通過日誌報警提醒系統管理員修復;ERROR — 該級別的錯誤也需要馬上被處理,但是緊急程度要低於FATAL級別。當ERROR錯誤發生時,已經影響了用戶的正常訪問。從該意義上來說,實際上ERROR錯誤和FATAL錯誤對用戶的影響是相當的。FATAL相當於服務已經掛了,而ERROR相當於好死不如賴活著,然而活著卻無法提供正常的服務,只能不斷地列印ERROR日誌。特別需要注意的是,ERROR和FATAL都屬於伺服器自己的異常,是需要馬上得到人工介入並處理的。而對於用戶自己操作不當,如請求參數錯誤等等,是絕對不應該記為ERROR日誌的;WARN — 該日誌表示系統可能出現問題,也可能沒有,這種情況如網路的波動等。對於那些目前還不是錯誤,然而不及時處理也會變為錯誤的情況,也可以記為WARN日誌,例如一個存儲系統的磁碟使用量超過閥值,或者系統中某個用戶的存儲配額快用完等等。對於WARN級別的日誌,雖然不需要系統管理員馬上處理,也是需要及時查看並處理的。因此此種級別的日誌也不應太多,能不打WARN級別的日誌,就盡量不要打;INFO — 該種日誌記錄系統的正常運行狀態,例如某個子系統的初始化,某個請求的成功執行等等。通過查看INFO級別的日誌,可以很快地對系統中出現的 WARN,ERROR,FATAL錯誤進行定位。INFO日誌不宜過多,通常情況下,INFO級別的日誌應該不大於TRACE日誌的10%;DEBUG or TRACE — 這兩種日誌具體的規範應該由項目組自己定義,該級別日誌的主要作用是對系統每一步的運行狀態進行精確的記錄。通過該種日誌,可以查看某一個操作每一步的執 行過程,可以準確定位是何種操作,何種參數,何種順序導致了某種錯誤的發生。可以保證在不重現錯誤的情況下,也可以通過DEBUG(或TRACE)級別的日誌對問題進行診斷。需要注意的是,DEBUG日誌也需要規範日誌格式,應該保證除了記錄日誌的開發人員自己外,其他的如運維,測試人員等也可以通過 DEBUG(或TRACE)日誌來定位問題;
5. 不斷優化日誌
有一點可以肯定,好的日誌就像好的文章一樣,絕不是一遍就可以寫好的,而需要在實際的運維過程中,結合線上問題的定位,不斷地進行優化。最關鍵的一點是,團隊要重視日誌優化這件事情,不要讓日誌的質量持續降低(當項目變大時,項目的代碼也存在一樣的問題,越寫越亂)。
此處有以下幾個比較好的實踐:
- 在定位問題的過程中完善日誌,如果定位問題花費了很長時間,那就說明系統日誌還存在問題,需要進一步完善和優化;
- 需要思考是否可以通過優化日誌,來提前預判該問題是否可能發生(如某種資源耗盡而導致的錯誤,可以對資源的使用情況進行記錄)
- 定義好整個團隊記錄日誌的規範,保證每個開發記錄的日誌格式統一;特別需要說明的是,對於DEBUG/TRACE級別的日誌,也需要定義好清晰的格式,而不是由開發人員自由發揮;
- 整個團隊(包括開發,運維和測試)定期對記錄的日誌內容進行Review;
- 開發做運維,通過在查問題的過程來優化日誌記錄的方式;
- 運維或測試在日誌中發現的問題,都需要及時向開發人員反映;
6. 關於RequestID
RequestID的作用
一個系統通常通過RequestID來對請求進行唯一的標記,目的是可以通過RequestID將一個請求在系統中的執行過程串聯起來。該RequestID通常會隨著響應返回給調用者,如果調用出現問題,調用者也可以通過提供RequestID幫助服務提供者定位問題。
RequestID的生成:
需要根據實際的使用場景來選擇:
- 對於簡單的系統,可以簡單採用一個隨機數即可,例如
RequestID = md5(time.Now() + random.Int())n
這樣簡單的方式在一定的時間內是不用擔心會衝突的
- 對於複雜的系統,需要在RequestID中編碼更多的內容,例如:可以將處理請求的伺服器IP,接收到請求的時間等信息編碼到RequestID中,這樣通過RequestID可以快速的了解請求屬於哪台機器,然後進一步定位:
./decode.sh 4b2c009a0a7800000142789f42b8ca96n Thu Nov 21 11:06:12 CST 2013n 10.120.202.150n 4b2c009an
- 對於一些特別的系統,RequestID也可以進行針對性的調整,例如在我實現的一個直播服務里,RequestID由兩部分組成,第一部分是一個隨機字元串(通過MD5生成),第二部分是一個不斷在自增的整數:對於直播系統,這樣做的好處是通過RequestID的第一部分,可以快速搜索到一路直播流所有的日誌,而第二部分自增的整數可以幫助快速定位一段時間的日誌。
RequestID串聯起來的日誌系統:
通常一個服務由若干個子系統組成,拿網易對象存儲舉例,它包含了前端負載均衡節點、存儲邏輯伺服器、元數據集群、分散式存儲集群、圖片處理集群、音視頻處理集群、緩存集群等。通常一個請求需要由若干個子系統,甚至所有的子系統的協同處理。這時,如果某個請求出錯,再要定位到具體的出錯原因就比較複雜了,因為常常需要到數十台機器上去定位日誌。
當時的思路在負載均衡節點接收到請求後,就為請求生成一個全局唯一的RequestID,該請求所經過所有子系統系統,均基於該RequestID記錄日誌,這樣通過將所有的日誌收集起來,就可以通過這一個RequestID來得到完整的系統處理日誌了。
然而這並不是一件容易做的事情:所有的系統間調用都需要進行改造,所有的日誌輸出的地方都要統一格式,而我們採用的有些開源組件實際上很難支持這種做法。
不過,有了這樣的認識,我們組在開發新的底層分散式文件系統時,介面傳入的第一個參數就是RequestID了。
7. 動態日誌輸出
上文已經討論過,DEBUG日誌和INFO日誌的一個重要的區別是,INFO日誌用於記錄常規的系統運行狀態,請求的基本的輸入和輸出等,對於定位一般的問題已經足夠了。而DEBUG日誌則詳細的記錄了一個請求的處理過程,甚至是每一個函數的輸入和輸出結果,遇到一些隱藏比較深的問題時,必須要依賴DEBUG日誌。
然而,由於DEBUG級別的日誌數量比INFO級別的數量多很多(通常差一個數量級),如果長期在線上伺服器開啟DEBUG級別的日誌輸出,日誌量太大。再比如,有時候僅僅是由於某一個用戶的訪問模式比較特殊導致了問題,如果將整個服務(特別是一個服務部署了很多台節點時)都臨時調整為DEBUG級別日誌輸出,也非常不方便。
下面介紹一種我採用的方式:
我們的系統採用如下的業務架構(簡化版):
在業務處理層的Proxy中,實現如下邏輯:當接收到的HTTP請求的QueryString中包含"DEBUG=ON"參數時,就將所有的DEBUG級別的日誌也輸出:
在負載均衡層的Openresty中,實現如下介面:管理員可以配置將哪個用戶的哪個桶的哪個對象的哪種操作(註:這是對象存儲中的幾個概念)輸出為DEBUG日誌,Openresty會對每個請求進行過濾,當發現請求和配置的DEBUG日誌輸出條件相匹配時,則在請求的QueryString中新增"DEBUG=ON"參數。
通過這種方式,管理員可以隨時配置哪些請求需要輸出為DEBUG級別的日誌,可以大大提高線上定位問題的效率。
8. 慢操作日誌
服務在接收到一個請求的時候,記錄請求的接收時間(T1),在請求處理完成待發送的時候,會記錄請求發送時間(T2),通常一個請求的日誌都記為INFO級別,然而當出現請求處理時間(T2-T1)超過一定時間(如10s)時,可以將該日誌提升為WARN級別。通過該方法,可以預先發現系統可能存在的一些問題。
同樣的慢操作日誌還可以用來記錄系統一些外部依賴的處理時間,如一個服務可能依賴外部認證伺服器來進行認證授權。通過記錄每次認證請求的時間並將超出預期時間的請求日誌採用WARN級別輸出,可以儘早發現認證伺服器是不是需要擴容等問題。
慢日誌的時間閾值應該是可以動態調整的,這樣在進行系統優化時,可以將該報警時間閾值逐漸調小,不斷地對系統進行優化。
9. 日誌監控
通過對日誌中的關鍵字進行監控,可以及時發現系統故障並報警,這對於保證服務的SLA至關重要。
服務的監控和報警是一個很大的話題,此處只說日誌監控報警需要注意的一些問題:
- 能不報警的就不報警,只有需要運維馬上處理的錯誤才需要發送報警。這樣做的原因是避免長期的報警騷擾讓運維人員對報警不再敏感,最後真的報警來了時,變成了狼來了的傳說;
- 明確報警關鍵字,例如用ERROR作為報警的關鍵字,而不是各種各樣的複雜規則。這樣做的原因是日誌監控本質上是不斷的進行字元串匹配操作,如果規則太多太複雜,就可能對線上服務產生影響;
- 對於一些預警操作,例如某個服務需要重試多次才能成功,或者某個用戶的配額快用完等等,可以通過每天一封報警郵件的方式來反饋;
- 每一次系統出現故障,都需要及時檢查日誌報警是否靈敏,日誌報警的描述是否準確等,不斷優化日誌報警;
10. 其他的注意事項
上線後日誌觀察
每一次上線完成後,除了對系統進行完整的回歸測試外,還需要對日誌進行觀察,特別是當上線新功能以後,可以通過日誌確認新功能是否工作正常。
日誌輸出到不同的文件
在性能測試時遇到的另一個問題是,當並發量很大時,可能會有一些請求處理失敗(如0.5%),為了對這些錯誤進行分析,需要去查這些錯誤請求的日誌。而由於這種情況下日誌量巨大,使得對錯誤日誌的分析變得困難。
這種情況下可以將所有的錯誤日誌同時輸出到一個單獨的文件之中。
日誌文件的大小
日誌文件不宜過大,過大的日誌文件對於日誌監控,問題定位等都會帶來不便。因此需要進行日誌文件的切分,日誌文件應該按天來分割,還是按照小時來分割,應該根據日誌量來決定,原則就是方便開發或運維人員能快速查找日誌。
為了防止日誌文件將整個磁碟空間佔滿,需要定期對日誌文件進行刪除。例如,在收到磁碟報警時,可以將兩個月以前的日誌文件刪除。此處比較好的實踐是:
- 將所有日誌文件收集起來,這樣即使在記錄日誌的機器上刪除,也可以通過收集的日誌對之前的問題進行定位;
- 每天通過定時任務來刪除過期日誌,如每天在凌晨4點刪除60天前的日誌
11. 總結
對文中提出的所有建議總結如下:
- 充分認識到日誌對於一個可靠的後端系統的關鍵作用
- 整個團隊(包括運維人員)需要對日誌級別有明確的規定,什麼日誌輸出為什麼級別,什麼級別的錯誤出現要如何處理等
- 需要定期對日誌內容進行優化更新,目的就是通過日誌快速準確地定位問題
- 要明確不同日誌的用途,對日誌內容進行分類
- 絕不要列印沒有用的日誌,防止無用日誌淹沒重要信息
- 日誌信息要準確全面,努力做到僅憑日誌就可以定位問題
- 日誌的優化是一件需要持續不斷投入精力的事,需要不斷從錯誤中學習
- 根據不同的目的生成RequestID,必要時在RequestID中盡量編碼更多的信息
- 將一個請求的整個處理流程和唯一的RequestID關聯起來
- 支持動態日誌輸出,方便線上問題定位
- 新上線伺服器後一定要對日誌進行觀察,特別地,開發人員可以通過觀察日誌來確認新功能是否工作正常
- 通過日誌級別的提升來發現潛在問題
- 對日誌進行監控報警,比客戶先發現系統問題
- 通過日誌中的關鍵字來確定系統的運行狀態
- 日誌格式要統一規範
- 將錯誤日誌輸出到一個單獨的文件中進行分析
- 要把日誌的大小,如何切分,如何刪除等作為規範建立起來
推薦閱讀:
※成為HBase Committer後
※未來已來,Google Cloud Spanner 展開 NewSQL 時代
※「分散式」「集群」「雲計算」三者是什麼區別呢?
※bifrost : Rust 下的分散式系統框架
※超級乾貨:Word2Vec課堂筆記(內附教學視頻)