日誌收集Agent,陰暗潮溼的地底世界

本文介紹作者在參與嚴選技術工作組的日誌平臺項目時,在日誌收集Agent這塊遇到的一些問題,深入到每個底層細節和大家談談。

日誌Agent對於使用日誌平臺的用戶來說,是一個黑盒。對於用戶來說,Agent有些不好的地方:

  • Agent私底下偷偷摸摸都做了些什麼事情呢?(陰暗的)

  • Agent的設計實現其實有一些dirty、creepy的地方(潮溼的)

所以我覺得日誌收集Agent對大家來說是一個陰暗潮溼的地底世界,就讓我舉起火把,照亮所有dark、dirty、creepy的地方。

背景提要

日誌收集我們知道是在宿主服務器通過一個Agent來收集日誌數據,並且將收集到的數據源源不斷的發送到日誌平臺的下游鏈路消費。

正是因爲日誌收集Agent是整個日誌平臺的唯一數據來源,所以日誌收集的地位非常重要。一旦日誌收集Agent出現問題,輕則影響後續鏈路的報警和查詢,重則影響宿主服務器,反客爲主,影響更爲重要的應用系統。

所以,先來看看我們選型Agent的時候有些什麼陰暗的地方。

日誌收集Agent方案

由於日誌收集Agent的特殊性,我們對於Agent的要求優先級由高到低如下:

低耗>穩定>高效>輕量

以此原則,我們逐步推動Agent的演進。

日誌收集Agent初期方案

其實日誌平臺一開始並沒有對比太多其他的Agent方案,直接就是使用的Flume作爲Agent。

why?

基於MVP(minimum viable product)原則,日誌平臺第一版的選項更看重能快速上線、快速試錯、快速驗證。而且嚴選這邊其實之前就存在一套日誌收集系統是用的Flume,我們決定先複用Flume,再對其定製。然後其實還有一個更重要的原因:爲了兼容一些歷史問題(例如收集的日誌要寫到北京Kafka的情況),我一開始不得不沿用Flume作爲Agent的方案。

then?

以爲我們Agent就此受困於歷史的漩渦中束手束腳,止步不前?不存在的,我們同步調研了Filebeat的方案。來看看我們對Filebeat的調研工作。

Filebeat作爲日誌收集Agent

先來看下2者之間的對比:


Filebeat
Flume
語言
Go
Java
包大小
<10m
>68m
額外依賴
根據source與sink的不同可能需要額外的依賴包
配置複雜度 較高
性能
資源佔用

擴展性
可靠性 高(at-least-once) 高(at-least-once)
限流 自帶,背壓敏感協議 自定義開發擴展的一個Interceptor
負載均衡 內置
內置
輸入源 內置了幾個 支持多樣的輸入源,方便的自定義擴展輸入源
輸出源 內置了幾個 內置比較豐富,方便的擴展


權衡優劣後,我更傾向於選擇Filebeat作爲日誌收集的Agent,原因如下:

  • 我們對於Agent的需求是低耗、穩定、高效、輕量。擴展性顯得並不那麼重要,功能豐富與穩定性,我更傾向於後者

  • 對於輸入源,我們的場景也正好只是基於文件的日誌數據收集,Filebeat已經滿足我們的需求場景

  • 對於輸出源,Filebeat需要定製開發,支持http/grpc,有一定開發成本,但是完全可以接受

  • 目前flume-agent的方案,日誌切分是在flink任務中,導致後續架構鏈路冗長。使用Filebeat完全可以把切分的工作放在Agent端來簡化架構鏈路,這對於後續日誌平臺的運維也大有裨益

同時,我們做了Filebeat的壓測,壓測數據如下:

其結果讓我們震驚,在內存佔用很低的情況下(3%以下),最高CPU佔用只有70%,Flume(平均145%)的一半不到。這使我們以後的Agent方案逐漸向Filebeat傾斜。

好了,是時候來點乾貨了,我們來看看日誌收集都有哪些問題?哪些creepy的設計?

如何發現日誌文件

Agent如何發現哪些日誌文件是要被收集的呢?主要有如下幾種方式:

  • 用戶配置

    • 優點:簡單高效。用戶直接把需要採集的日誌文件給配進來,可以說是最簡單高效的辦法。

    • 缺點:日誌文件很可能是會按天滾動(rotate)的,提前配置肯定覆蓋不了後面新創建的日誌文件。

  • 正則匹配(例如:access_log.\d{4}-\d{2}-\d{2}.\d{2})

    • 優點:靈活。能夠很好的覆蓋到各種滾動新建的日誌文件

    • 缺點:正則由用戶輸入,效率堪憂,一旦發生大量回溯,很可能CPU 100%。這也是我們最不希望發生在Agent端的問題

  • 佔位符匹配(例如:access_log.yyyy-MM-dd.log)

    • 優點:靈活高效。即能很好的匹配新創建的日誌文件,Agent端的匹配效率也非常高

    • 缺點:對於一些奇葩命名滾動規則的日誌文件不太好適應

日誌平臺使用的是佔位符匹配的方式,但是後端其實是兼容正則匹配的,這是出於兼容歷史的原因,後面將逐步去掉正則的匹配的方式。

解決了如何發現文件後,緊接着就會遇到另一個問題:

如何發現新創建的文件

直覺做法肯定是輪詢目錄中的日誌文件,顯然這不是個完美的方案。因爲輪詢的週期太長會導致不夠實時,太短又會耗CPU。

這真是一個艱難的trade-off

我們來對比下Flume(以下所說的Flume都是我們基於Flume改造定製的yanxuan-flume)和Filebeat的做法:

yanxuan-flume:輪詢

Flume目前是每隔500ms去輪詢查找是否有新的日誌文件,基本上就我們前面提到的”直覺做法”。實現簡單,但是我們很難衡量這個500ms是否是一個合適合理的值。

Filebeat:OS內核指令+輪詢

Filebeat的方案就完善優雅很多。依賴OS內核提供的高效指令,分別是:

  • Linux:inotify

  • macOS:fsevents

  • Windows:ReadDirectoryChangesW

來通知是否有新文件,並且輔助一個週期相對較長的輪詢來避免內核指令的bug(具體參考其man page),取長補短,低耗與高效兼得

又多了一個使用Filebeat的理由!

好了,現在我們已經清楚如何發現文件了,那麼問題又來了,我們如何知道這個文件是否已經收集過了?如果沒有收集完,應該從什麼位置開始接着收集?

如何標識一個日誌文件收集的位置

一般是用一個文件(這裏我們稱之爲點位文件)來記錄收集的文件名(包含文件路徑)與收集位置(偏移量)的對應關係,key就是文件名稱,value就是偏移量。記錄到文件的好處是,在機器宕掉後修復,我們還能從文件中恢復出上次採集的位置來繼續收集。

那麼,點位文件存在什麼問題呢?點位文件使用日誌文件名稱作爲key,但是一個日誌文件的名稱是有可能被更改的,當文件被改名後,由於點位文件中查詢不到對應的採集位置,Agent會認爲是一個全新的日誌文件而重頭重新收集。所以用文件名稱不能識別一個文件。那麼問題又來了:

如何識別一個文件

如何識別一個文件,最簡單的就是根據文件路徑+文件名稱。但是我們上面說了,文件很可能被改名。每個文件其實都有個inode屬性(可以使用命令stat test.log查看),這個inode由OS保證同一個device下inode唯一。所以自然而然的我們就會想到用device+inode來唯一確定一個文件。然而inode是會重新分配的,即當我們刪除一個文件後,其inode是會被重複利用,分配給新創建的文件。

舉個常見例子:假如日誌文件配置爲保留30天,那30天以前的日誌文件是會被自動刪除的。當刪除30天前的日誌文件,其inode正好分配給當天新創建的日誌文件,那當天的日誌是不會被收集的,因爲在點位文件中記錄了其採集偏移量。

我們來看看Flume和Filebeat是怎麼做的:

yanxuan-flume:device+inode+首行內容MD5

  • 優點:無需用戶干預,能保證唯一識別一個文件。

  • 缺點:需要打開文件讀取文件內容,而且首行內容MD5還是太暴力了,因爲首行很可能是一個超長日誌,再加上MD5,不僅耗CPU,而且判斷效率有點低。

可以考慮讀取首行N個字節的內容md5,但是N到底取多大呢? 越大相同的概率越小,效率越低。反過來,N越小重複的概率越大,效率越高。這又是一個艱難的trade-off啊!

Filebeat:device+inode

  • 優點:判斷效率高。無需打開文件讀取內容即可判斷

  • 缺點:可能會誤判

Filebeat提供了一個配置選項來決定何時刪除點位文件中的記錄:clean_inactive:72h表示清除72h不活躍的文件對應的點位文件中的記錄。基本上我們的文件都是每天(24h)滾動(rotated)的,那前一天的日誌文件是不會寫入的,所以設置clean_inactive:72h是合理的。

那爲什麼不在日誌文件被刪除後直接刪除點位文件中對應的記錄呢?因爲假如我們的日誌文件在一個共享的存儲分區中,當這個分區消失了一會(接觸不良等情況)又重新出現後,裏面的所有日誌文件都會重頭開始重新收集,因爲他們的收集狀態已經從點位文件中刪除了。

我覺得這是一個合理的“甩鍋”給使用者的配置選項。

解決了如何標識文件,如何標識採集狀態,那如何判斷一個日誌文件採集完了呢?採集到末尾返回EOF的時候就算採集完了,可是當採集速度大於日誌生產速度的時候,很可能我們採集到末尾返回EOF後,又有新的內容寫入。所以,問題就變成:

如何知道文件內容更新

最簡單通用的方案就是輪詢要採集的文件,發現文件內容有更新就採集,採集完成後再觸發下一次的輪詢,既簡單又通用。

那具體是輪詢什麼呢?

  • yanxuan-flume:按照文件的修改時間排序,輪詢文件內容,嘗試收集,如果返回是EOF則繼續下一份文件

  • Filebeat:按照文件的修改時間排序,輪詢文件的stat狀態,修改時間大於點位文件中記錄的時間,則打開文件收集,返回EOF則繼續下一份文件

相比Flume,Filebeat又做了一個小優化,每次不會直接就打開文件,而是先比較文件的修改時間再決定是否打開文件進行收集。

不得不感嘆,魔鬼在細節!低耗和高效如何兼得,Filebeat處處都是細節

好了,知道該什麼時候收集了,那我們具體收集的時候會遇到什麼問題呢?

如何收集多行日誌

目前的Agent默認都是單行收集的,即遇到換行符就認爲是一條全新的日誌。可是很多情況下,我們的一條日誌是多行的,比如異常堆棧、格式化後的SQL&Json等。

那如何判斷那幾行是屬於同一條日誌呢?

  • yanxuan-flume:Flume原生是不支持的,我們自己寫了個插件,通過配置一條日誌的開頭字符S來判斷。假如一行日誌的開頭不是S,則認爲是和上一行屬於同一條日誌。

  • Filebeat:支持Flume類似的方式,同時提供了配置項negate:true 或 false;默認是false,匹配開頭字符S的行合併到上一行;true,不匹配S的行合併到上一行。能夠覆蓋更多的多行日誌場景。

當然還有其他相關配置來兜底合併行可能帶來的問題,例如一次最多合併幾行和合並行的超時時間來防止可能的內存溢出與卡死。

萬無一失了嗎?想想多行日誌的最後一行按照以上的邏輯可以正常收集嗎?例如下圖所示:

如何處理多行日誌的最後一行

當多行日誌收集遇到最後一行怎麼收集呢?還是來比較下Flume和Filebeat的做法:

  • yanxuan-flume:遇到EOF即認爲是這條多行日誌收集完了。這有個問題就是,很可能這條多行日誌還沒有寫完,就被收集發送出去了。而且當收集速度大於日誌寫入速度的時候或者異步打印日誌的時候,又很容易發生這種情況

  • Filebeat:遇到EOF會回退之前讀取的內容,然後一直持有這個文件句柄(直到超時),直到新一行日誌寫入,根據新一行日誌的行首字符匹配來判斷是否當前行的日誌結束。所以Filebeat的存在的問題是很可能最後一行永遠不會被收集

目前業界貌似沒有太好的辦法來完美解決這個問題。個人覺得基於Filebeat的多行合併的超時時間配置選項能夠很大程度緩解這個問題,因爲多行日誌往往也是一次性寫入的,超過一定時間寫入的往往都是一條全新的日誌。

原文鏈接:https://zacard.net/2019/06/15/log-agent/

Kubernetes管理員認證(CKA)培訓

本次CKA培訓在北京開班,基於最新考綱,通過線下授課、考題解讀、模擬演練等方式,幫助學員快速掌握Kubernetes的理論知識和專業技能,並針對考試做特別強化訓練,讓學員能從容面對CKA認證考試,使學員既能掌握Kubernetes相關知識,又能通過CKA認證考試,學員可多次參加培訓,直到通過認證。點擊下方圖片或者閱讀原文鏈接查看詳情。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章