用一支菸的時間,寫一個日誌採集工具

南小哥:良辰小哥,我希望能採集aix機器上的日誌數據。

我:噢,這個不是很簡單嗎?

南小哥:emmmm,一臺機器大概有上萬份不同種類的日誌吧,而且都是業務相關,日誌之間是有關聯關係的。

我:emmmm這個看起來也不是很難。

南小哥:還有就是機器比較老,估計只能跑java5……

我放下了手中的鍵盤,陷入沉思,作爲一個優秀的程序猿最重要的就是——氣勢、泰山崩於前的鎮定。

我用最快的速度掃描了一遍現行的filebeat、fluented、flume、fluentd、scribe等解決方案,發現,沒有機會,難道就這樣放棄了嗎?

不,沒有什麼可以難倒我良辰!

我從口袋裏掏出一根菸,在煙霧繚繞中我緩緩看向南小哥說:這個不是問題,我就用一支菸的時間來寫個日誌採集工具吧。

如何唯一標識一個文件

從需求角度來講,用戶往往需要採集指定目錄指定格式下的文件,常用方式是配置路徑通配符,例如這樣: /path//.log,意思就是收集/path/a/web.log、/path/b/web_2018-12-11.log等日誌。但是,用文件名來標識文件是不準確的,移動一個文件mv a.log b.log,雖然名稱是變成了b.log,但實際日誌依舊是a.log的日誌,把b.log當做新文件處理就會出現採集到重複日誌的問題。
在這裏插入圖片描述
那換成inode作爲文件唯一標識怎麼樣?答案是,會好點。爲啥是好一點而已呢,原因有2個,一是不同文件系統間的不同文件inode可以一樣,二是同個文件系統下,不同時刻存在的文件inode可以一樣。這2個問題都可以導致讀取日誌時的偏移量錯亂問題,最終導致數據丟失或者數據重複。

那換成內容前綴的方式唯一標識怎麼樣?內容前綴指的是使用文件前N個字節作校驗。很顯然,這也是一個緩解的操作,因爲不同日誌依舊有出現相同校驗碼的情況。這裏的校驗碼算法可以使用crc32或者adler32,後者相比前者可靠性弱一點,性能則是前者的2~3倍。

目前最靠譜的方式是通過inode+內容前綴的方式唯一標識文件,雖然沒辦法完全解決唯一標識文件的問題,但鑑於同個文件系統不同時刻產生的文件剛好內容前綴有相同的情況概率比較低,所以該方案已經符合業務使用了。

如何監聽文件變化

當新增、刪除、修改文件時,如果獲知快速獲知變化?一種方式是使用linux的inotify機制,文件發生變化會立馬通知服務,這優點是實時性高,但缺點是相同通知過於頻繁反而提高了開銷,另外不是所有系統都含有inotify機制。另一種方式是用輪詢,輪詢的實時性會差點,因爲考慮性能,一般輪訓間隔在1s~5s,但好在簡單且通用。

如何合併日誌

有時用戶有合併日誌的需求,比較常見的就是異常堆棧信息,一行日誌被換行符分割成了多行。因爲日誌都是有規範的,會按時間、日誌等級、方法名、代碼行號等信息順序打印,那這裏一個簡單的處理方式就是使用正則匹配的方式來解決,按照匹配行作split劃分日誌。

但一些偏業務的日誌,就不能簡單地通過正則匹配了,這類日誌的內容是存在着關聯關係的,像訂單信息,日誌需要通過訂單id進行關聯的,但由於多線程並行寫日誌等原因,邏輯上存在關聯的日誌在物理上未必連續,所以要求合併邏輯具有"跳"行關聯的能力,這時候可以利用可命名的正則捕獲功能來處理,把捕獲的字段作爲上下文在上下游日誌傳遞,把匹配的日誌存到緩存統一輸出即可。

如何實現高吞吐

批量化處理,一是日誌讀取批量化,雖然讀取日誌已經是順序讀了,但如果在讀取時通過預讀取提前把待讀取的日誌都讀取出來放進buffer,這方式可以進一步提升性能。二是發送批量化,發送批量化的好處主要體現在2方面,一方面是提高壓縮比,像日誌這類存在大量重複內容的數據,數據越多壓縮比越高,另一方面,降低了請求頭部的大小佔比,減少帶寬的浪費。

異步化處理,文件監聽、日誌處理、日誌發送3個模塊解耦並異步化,數據及通知通過隊列傳遞。

非阻塞發送,發送端要處理的常規操作包括參數校驗、序列化、壓縮、協議包裝、ack、重試、負載均衡、心跳、校驗和、失敗回調、數據收發、連接管理。如果這系列操作都是由一個nio線程處理掉,發送效率肯定很低,但考慮到通常數據接收端數量不會太多(小於1000),所以這裏使用reactor的多線程模型完全足夠了,netty支持reactor多線程模型的,所以可以直接基於netty開發。這裏只需要注意io線程除了連接管理,其餘事情都交由工作線程處理就行。

在這裏插入圖片描述

如何實現資源控制

採集工具往往需要和待採集日誌的系統放在同一個機器上,不少系統還對性能敏感的,這就要求採集工具必須有控制資源使用的能力。

如何控制句柄使用?句柄使用往往是被嚴格受限的,但如果機器需要監聽上萬上十萬的文件時,如果使用句柄?這時候就需要採取惰性持有策略,即文件生成的時候不會持有句柄,只有嘗試讀取時再持有句柄,由於同時讀取的文件往往不多,從而只會佔用少量文件句柄。

另外,存在句柄引用的文件即便被刪掉,空間是不會被釋放掉的,導致長時間持有句柄是不是會有磁盤被打爆的風險?這就需要加上相應的定時釋放句柄的機制,被刪除的文件會加上一個時鐘,時鐘倒計時爲0時把句柄釋放掉。

如何控制內存使用?無論是日誌的合併還是批量化操作,都需要使用較大的緩存,一旦緩存過大,就有oom的風險,所以需要機制控制內存佔用。這裏可以簡單實現一個內存分配器,分配器內部維護一個計數器,用於記錄當前分配內存大小。線程分配關鍵內存或者釋放內存都需要請求內存分配器,內存超限則掛起請求的線程,並用等待/通知機制讓線程協同起來。把分配內存的大戶控制住,就可以控制住整體內存大小了。

如何控制流量及cpu? 流量過大不僅佔用多高帶寬,而且流量與cpu佔用也呈正比關係,所以控制流量的同時也就實現了cpu的控制。在固定窗口、滑動窗口、令牌桶、漏桶幾個流控算法中,令牌桶和漏桶算法都可以令流速較爲平滑,而且guava實現了令牌桶算法。這裏直接使用guava的RateLimiter即可。

在這裏插入圖片描述

最後

我看了一眼手上燒完的煙,時間剛剛好。

在實際應用中,仍舊會遇到各種各樣的問題,一方面是來自於業務的擴展性需要,另一方面是隨着集羣的擴大,在數據熱點、實時性、彈性負載均衡方面會遇到諸多複雜的挑戰。本文僅提供開發採集工具常見問題的解決思路,更深入的細節需要讀者實際去探索了。

文章作者——良辰(袋鼠雲日誌團隊、後端開發工程師)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章