瞎逼逼:談談容器日誌採集

故事還得從本週我搞了個 panic 開始說起,在我發佈失敗要排查爲什麼失敗了時,我驚訝的發現我竟然要上容器才能看到 panic 日誌,我工作這麼久還是很少見到這種場面的,經過和基建同學的深入暢談,我要上容器這件事情合不合理拋開不談,但我意識到,雖然大家都有日誌採集,但似乎每家公司的實現卻都略有差異,因此今天就來講講關於日誌採集的一些個人想法。

在過去的文章中,我們提到過好幾次關於系統的穩定性建設,而穩定性建設的第一步,就是要採集數據,關於採集數據的每一個環節,我們都可以花很多篇幅去講解,今天我們要介紹的「日誌(Log)」就是其中的一環。(另外兩個重要的方向是「鏈路追蹤(Trace)」和「度量(Metrics)」)

農耕時代

所謂農耕時代,就是說如果沒有「日誌採集」系統,那麼我們要怎麼看日誌。

上文中我們其實也提到了,那就上機器唄——它的流程大概是:

# 輸入到文件流
./service > stdout.log
# 查看日誌流
tail -f stdout.log
# 搜索
cat stdout.log | grep panic

在自己開發的一些圖一樂小服務中,我也經常簡單粗暴的用這種方式 Debug 和查找結果(比如直接 stdout 的話日誌一口氣輸出太多,那麼暫存到文件後 grep 就方便很多),但對於線上來說顯而易見的就會有很多問題:

  1. 非結構化隨便打的日誌不好檢索

  2. 常駐服務日誌量大了怎麼辦

  3. 我感覺出了點問題,可是我一個服務有三個容器,到底日誌在哪臺容器上

也就是說,在現代化系統中,依靠上機操作的這個想法只能說:尊重、祝福。

升級之路

分析出了要解決的問題之後,接下來我們就可以順着思路設計系統了,對於單一操作系統來說來說,一共有三種方式可以解決:

  1. 寫入 stdout

  2. 寫入文件(磁盤 IO)

  3. 推往採集服務(網絡 IO)

嚴格來說,許多日誌系統都可以監聽不同的數據源,這取決於你自己的配置,它們也不是完全衝突的,比如即使我選擇直接推往採集服務的簡單方式,但爲了避免數據丟失,我仍然可以寫入磁盤作爲一個備份和恢復手段。

接下來疊加的正交問題是:大部分情況下我們都用了 K8S,站在集羣的角度上來說,我們解鎖了新的口徑,如何收集集羣中所有的日誌:

  1. 將日誌直接從應用程序中推送到日誌記錄後端。

  2. 在應用程序的 Pod 中,包含專門記錄日誌的邊車(Sidecar)容器

  3. 使用在每個節點 (Node) 上運行的節點級日誌記錄代理。

由於選擇的多樣性,導致了我前面所說的每家公司的實現差異性。

首先先從最簡單粗暴的設計開始說起,也就是直接將日誌從應用程序推到日誌系統中,K8S 文檔中也有提及,但這完全取決於日誌系統的實現,因此它並沒有展開:

實際上,單看這種簡單的架構,就會存在不少問題,如果沒有一些補償手段,非常容易丟失日誌,而如果容器內異常,也就意味着收集不到對應的日誌,而未發送的日誌也可能會丟失——我明明是希望通過日誌排查爲什麼掛了,但因爲掛了日誌丟失,這就和「可觀測性」離得太遠了。

你也可以使用邊車(Sidecar)容器來掛載日誌系統:

這樣的好處是每個容器都有屬於自己的日誌收集器,獨立啓動,意味着可以獨立配置,相當靈活。

當然,也有一個類似的方法,就是我構建鏡像的時候就把 Agent 一起打進去就行了,但是這樣的話對於 Agent 的單獨管理(重啓、恢復、升級)就必須要將你整個集羣中所有鏡像全部打個包了,Sidecar 的好處就在於它有獨立的生命週期,和主容器互不干擾的同時又共享了網絡和存儲。

但缺點是每個容器都有一個獨立的 agent 相較於其他方法,開銷會成倍的增長,對於大型集羣來說到了一個很難忽視的程度。

改進的一套邊車容器運行方案是,邊車容器只負責從各處搜刮日誌,統一的輸出到 stdout 和 stderr,這樣的話整體的日誌處理還是交給了 k8s,而相比原來使勁往 stdout 和 stderr 輸出內容,邊車容器會讓我們對於日誌流的生產和消費有着更靈活的把控,適合一些你無法把控整個容器內輸出(比如底層有些日誌往文件裏寫),或者需要一個歸檔能力的情況:

此外,因爲它本質上只是一個簡單的日誌流的重定向,因此 sidecar 的整體開銷相比前一個方案要小很多。

當然,這種方案相當於多了一步寫入到容器內的文件,因此相比直接推 stdout 和 stderr,相當於需要額外的存儲空間。因此如果並不需要一些複雜的分類文件記錄,或者本身只寫入到一個固定的文件中,那麼 k8s 仍然推薦直接寫到 stdout。

另一方面,如果你只寫入一個文件,但卻沒有實現日誌輪轉(根據時間切割日誌、自動回收)的話,sidecar 也可以負責做這件事情(但相比之下,或許直接讓 kubelet 去做會更簡單)。

最後我們再來說說「使用在每個節點 (Node) 上運行的節點級日誌記錄代理」的意思,這一套方案很顯然是最環保的,,只需要看 K8S 畫出來的圖就知道:

使用這種方式,你可以擁有:

  1. 最低成本的開銷:宿主機級別的 Agent

  2. kubelet 負責處理日誌輪轉

  3. 完全不被任何多餘進程擠兌資源的純淨容器

如果有一個方案印證「降本提效」的話,那它肯定當之無愧。

當然,和前面幾個方案對比,Agent 從容器挪到了宿主機(使用 DaemonSet 控制),就意味着可定製性的下降,同時,即使你一個人只報兩三條日誌,如果同一宿主機中有人在瘋狂上報日誌,仍會影響到你自己的日誌採集。

日誌識別

剛剛其實更多的講的是日誌採集 Agent 的部署策略,我們需要從資源消耗、可定製化、可靠性的角度去選擇一個適合我們的部署方案,但是部署完了之後,就面臨了新的問題:剛剛說了半天,好像只知道日誌在什麼位置,但是我怎麼樣讓程序去指定的位置讀。

衆所周知,讀文件也是一門藝術,上面我們提到的無論是 stdout、寫文件、甚至是直接推(需要落盤保證可恢復),最終在一波部署後,都轉換成了——讀文件,只是讀的文件的位置不同,我們現在只有文件夾,不知道文件夾裏那些文件是可用的。

如何識別日誌文件,通常的方式就是:

  1. 用戶可配

  2. 正則匹配

  3. 規則匹配

用戶可配聽上去最簡單,但大部分情況下我們的日誌是存在輪轉的,也就是會根據時間進行 yyyymmdd 的拆分,因此不太可靠。

正則匹配看上去靈活度夠了,但是如果用戶寫的正則表達式特別複雜,遇到正則表達式回溯,那對於整個系統來說將是災難性的(這裏好像欠了一篇講正則表達式的稿子一直沒寫……)。

規則匹配形如:runtime_log-yyyymmdd.log,相對來說開銷會比較小,也可以解決日誌輪轉的問題,但是同時也需要約束用戶的使用,但大部分情況下,應該會使用這種方式來進行靈活度和性能的平衡。

解決了識別文件名的問題,接下來的問題就是,我的文件是不停更新的,怎麼樣知道現在多了新文件需要我去讀取?

最簡單的方式是使用輪詢每次去記錄,看看有沒有變更出來的新文件,但衆所周知,輪詢的效果很差,第一,輪詢的時間怎麼定,雖然我們對日誌要求不高,但當然希望越快能看到越好,如果輪詢定的時間長了,日誌整個消費流程就慢,如果輪詢時間定的短了,那麼資源開銷就變大了。

另一種高級但樸實無華的方案是系統調用,比如 Linux 中我們就可以使用 inotify 監聽文件系統的變化,這樣就把輪詢變成了消費事件,又一次實現了降本提效。(在其他系統中也有類似的系統調用方法)。

現在我們可以完美的感知到了日誌文件的變化,但日誌文件的變化,到我怎麼用這個日誌文件去消費,推到隊列裏,還差了一步:我怎麼知道文件收集到哪裏,消費完沒。

一個簡單而質樸的想法是利用一個文件去記錄我現在處理了哪些文件,分別消費到什麼位置。類似於一種 kv 結構 (filekey-{offset})。這裏爲什麼我們不直接用 filename,而單獨命名了 filekey,是因爲文件名是可變的,舉個簡單的例子,我把現在的文件從 runtime_log.log 歸檔到 runtime_log_20240512.log,那麼是否意味着我需要重新上報?——我的預期肯定是不上報的,但如果我們拿文件名作爲 key,就會存在這樣的問題。

在 Linux 中,同一磁盤 (device) 的一個 inode 會指向唯一一個文件,因此其實我們可以通過 device+inode 作爲 key。

但是問題是,inode 是可以回收再利用的,這樣帶來的問題是,我已經標記這個文件掃描到第 500 行了,但實際上這是三十天前的文件,在 30 天后新的文件分配到了同一個 inode,那麼它的前 500 行就無法正常消費了。

要解決這個問題,我們可以使用算 md5 的方式,比如帶上首行 MD5 來做 key,這樣如果首行日誌不一樣,那就不是同一個文件——但問題是:第一,倒也不是 100% 可能不一樣,第二,首行多長不知道,那麼 MD5 帶來的額外開銷也是不可預期的。

因此這裏我們利用日誌輪轉的特性,既然你在一段時間會歸檔、再過一段時間會刪除,我在你的歸檔 - 刪除的週期一起刪除你的點位信息,那大概率不會有什麼問題。

在監聽文件更新上,理論上我們也可以消費 inotify 的監聽值,當然我也有看到比如「按照文件修改時間排序,輪詢修改時間和點位記錄時間,來決定是否打開文件」。

但是還有一個問題:比如 panic 的場景下,日誌一定會是多行的,那麼我們只能通過引入多行標識符來標識多行的情況,否則默認會是一行一條日誌,諸如 panic 日誌這種情況可能會被打散。

總結

讀到這了,可能有的同學想說:接着往下說呀,但是我們今天的話題聊到採集爲止,到了我們今天結尾的步驟,我們總算可以把日誌幸福的推送進消息隊列了。但接下來我們同樣會面臨一些挑戰:

  1. 生產速度與消費速度

  2. 日誌帶來的消耗監控

  3. 日誌的標準化、格式化

之後的話題,我們下次再說。

參考資料

[1]

日誌架構 - kubernetes: https://kubernetes.io/zh-cn/docs/concepts/cluster-administration/logging/

[2]

vivo: 大數據日誌採集 Agent 設計實踐: https://pdai.tech/md/arch/log/arch-log-example-vivo-logagent.html

[3]

日誌收集 Agent,陰暗潮溼的地底世界: https://zacard.net/2019/06/15/log-agent/

[4]

filebeat 踩坑 inode: https://xiwan.github.io/post/filebeat%E8%B8%A9%E5%9D%91inode/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/N37Va1hhlW3ODBbEamCj6w