如何寫一個日誌採集工具
如何寫一個日誌採集工具
背景
一系列分散式架構、容器技術的發展促進了軟體開發、交付、qa的效率,在架構的演化上,主要可以看到2類變化,一個變化是系統層面,從單體系統向微服務的方向演進,一個是資源方面,逐步細分,從以前的物理伺服器到現在的容器演進。在架構不斷演進當中,日誌統一管理變得越來越重要也越來越複雜。從採集端來看,就包括了文字日誌、網路收發日誌、容器日誌、記憶體共享日誌等日誌的採集,如何實時、高吞吐、低消耗並且無公害地採集日誌,是一個成功日誌採集工具所必須解決的問題。下面筆者將以開發當中會遇到的問題的角度跟大家分享如何開發日誌採集工具。
如何唯一標識一個檔案
從需求角度來講,使用者往往需要採集指定目錄指定格式下的檔案,常用方式是配置路徑萬用字元,例如這樣: /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即可。

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