日誌採集中的關鍵技術分析

概述

日誌從最初面向人類演變到如今的面向機器發生了巨大的變化。最初的日誌主要的消費者是軟件工程師,他們經過讀取日誌來排查問題,現在,大量機器日夜處理日誌數據以生成可讀性的報告以此來幫助人類作出決策。在這個轉變的過程當中,日誌採集Agent在其中扮演着重要的角色。node

做爲一個日誌採集的Agent簡單來看其實就是一個將數據從源端投遞到目的端的程序,一般目的端是一個具有數據訂閱功能的集中存儲,這麼作的目的實際上是爲了將日誌分析和日誌存儲解耦,同一份日誌可能會有不一樣的消費者感興趣,獲取到日誌後所處理的方式也會有所不一樣,經過將數據存儲和數據分析進行解耦後,不一樣的消費者能夠訂閱本身感興趣的日誌,選擇對應的分析工具進行分析。像這樣的具有數據訂閱功能的集中存儲業界比較流行的是Kafka,對應到阿里巴巴內部就是DataHub還有阿里雲的LogHub。而數據源端大體能夠分爲三類,一類就是普通的文本文件,另一類則是經過網絡接收到的日誌數據,最後一類則是經過共享內存的方式,本文只會談及第一類。一個日誌採集Agent最爲核心的功能大體就是這個樣子了。在這個基礎上進一步又能夠引入日誌過濾、日誌格式化、路由等功能,看起來就好像是一個生產車間。從日誌投遞的方式來看,日誌採集又能夠分爲推模式和拉模式,本文主要分析的是推模式的日誌採集。後端

推模式是指日誌採集Agent主動從源端取得數據後發送給目的端,而拉模式指的是目的端主動向日誌採集Agent獲取源端的數據

業界現狀

目前業界比較流行的日誌採集主要有Fluentd、Logstash、Flume、scribe等,阿里巴巴內部則是LogAgent、阿里雲則是LogTail,這些產品中Fluentd佔據了絕對的優點併成功入駐CNCF陣營,它提出的統一日誌層(Unified Logging Layer)大大的減小了整個日誌採集和分析的複雜度。Fluentd認爲大多數現存的日誌格式其結構化都很弱,這得益於人類出色的解析日誌數據的能力,由於日誌數據其最初是面向人類的,人類是其主要的日誌數據消費者。爲此Fluentd但願經過統一日誌存儲格式來下降整個日誌採集接入的複雜度,假想下輸入的日誌數據好比有M種格式,日誌採集Agent後端接入了N種存儲,那麼每一種存儲系統須要實現M種日誌格式解析的功能,總的複雜度就是M*N,若是日誌採集Agent統一了日誌格式那麼總的複雜度就變成了M + N。這就是Fluentd的核心思想,另外它的插件機制也是一個值得稱讚的地方。Logstash和Fluentd相似是屬於ELK技術棧,在業界也被普遍使用,關於二者的對比能夠參考這篇文章Fluentd vs. Logstash: A Comparison of Log Collectors緩存

clipboard.png

從頭開始寫一個日誌採集Agent

做爲一個日誌採集Agent在大多數人眼中可能就是一個數據「搬運工」,還會常常抱怨這個「搬運工」用了太多的機器資源,簡單來看就是一個tail -f命令,再貼切不過了,對應到Fluentd裏面就是in_tail插件。筆者做爲一個親身實踐過日誌採集Agent的開發者,但願經過本篇文章來給你們普及下日誌採集Agent開發過程當中的一些技術挑戰。爲了讓整篇文章脈絡是連續的,筆者試圖經過「從頭開始寫一個日誌採集Agent」的主題來說述在整個開發過程當中遇到的問題。安全

如何發現一個文件?

當咱們開始寫日誌採集Agent的時候遇到的第一個問題就是怎麼發現文件,最簡單的方式就是用戶直接把要採集的文件羅列出來放在配置文件中,而後日誌採集Agent會讀取配置文件找到要採集的文件列表,最後打開這些文件進行採集,這恐怕是最爲簡單的了。可是大多數狀況日誌是動態產生的,會在日誌採集的過程當中動態的建立出來, 提早羅列到配置文件中就太麻煩了。正常狀況下用戶只須要配置一個日誌採集的目錄和文件名字匹配的規則就能夠了,好比Nginx的日誌是放在/var/www/log目錄下,日誌文件的名字是access.log、access.log-2018-01-10.....相似於這樣的形式,爲了描述這類文件能夠經過通配符或者正則的表示來匹配這類文件例如: access.log(-[0-9]{4}-[0-9]{2}-[0-9]{2})?有了這樣的描述規則後日志採集Agent就能夠知道哪些文件是須要採集的,哪些文件是不用採集的。接下來會遇到另一個問題就是如何發現新建立的日誌文件?,定時去輪詢下目錄或許是個不錯的方法,可是輪詢的週期太長會致使不夠實時,過短又會耗CPU,你也不但願你的採集Agent被人吐槽佔用太多CPU吧。Linux內核給咱們提供了高效的Inotify的機制,由內核來監測一個目錄下文件的變化,而後經過事件的方式通知用戶。可是別高興的太早,Inotify並無咱們想的那麼好,它存在一些問題,首先並非全部的文件系統都支持Inotify,此外它不支持遞歸的目錄監測,好比咱們對A目錄進行監測,可是若是在A目錄下面建立了B目錄,而後馬上建立C文件,那麼咱們只能獲得B目錄建立的事件,C文件建立的事件就會丟失,最終會致使這個文件沒有被發現和採集。對於已經存在的文件Inotify也無能爲力,Inotify只能實時的發現新建立的文件。Inotify manpage中描述了更多關於Inotify的一些使用上的限制以及bug。若是你要保證不漏採那麼最佳的方案仍是Inotify+輪詢的組合方式。經過較大的輪詢週期來檢測漏掉的文件和歷史文件,經過Inotify來保證新建立的文件在絕大數狀況下能夠實時發現,即便在不支持Inotify的場景下,單獨靠輪詢也能正常工做。到此爲止咱們的日誌採集Agent能夠發現文件了,那麼接下來就須要打開這個文件,而後進行採集了。可是天有不測風雲,在咱們採集的過程當中機器Crash掉了,咱們該如何保證已經採集的數據不要再採集了,可以繼續上次沒有采集到的地方繼續呢?網絡

基於輪詢的方式其優勢就是保證不會漏掉文件,除非文件系統發生了bug,經過增大輪詢的週期能夠避免浪費CPU、可是實時性不夠。Inotify雖然很高效,實時性很好可是不能保證100%不丟事件。所以經過結合輪詢和Inotify後能夠相互取長補短。

點位文件高可用

點位文件? 對就是經過點位文件來記錄文件名和對應的採集位置。那如何保證這個點位文件能夠可靠的寫入呢? 由於可能在文件寫入的那一刻機器Crash了致使點位數據丟掉或者數據錯亂了。要解決這個問題就須要保證文件寫入要麼成功,要麼失敗,絕對不能出現寫了一半的狀況。Linux內核給咱們提供了原子的rename。一個文件能夠原子的rename成另一個文件,利用這個特性能夠保證點位文件的高可用。假設咱們已經存在一份點位文件叫作offset,每一秒咱們去更新這個點位文件,將採集的位置實時的記錄在裏面,整個更新的過程以下:async

  • 將點位數據寫入到磁盤的offset.bak文件中
  • fdatasync確保數據寫入到磁盤
  • 經過rename系統調用將offset.bak改名爲offset

經過這個手段能夠保證在任什麼時候刻點位文件都是正常的,由於每次寫入都會先確保寫入到臨時文件是成功的,而後原子的進行替換。這樣就保證了offset文件老是可用的。在極端場景下會致使1秒內的點位沒有及時更新,日誌採集Agent啓動後會再次採集這1秒內的數據進行重發,這基本上知足需求了。工具

可是點位文件中記錄了文件名和對應的採集位置這會帶來另一個問題,若是在進程Crash的過程當中,文件被重命名了該怎麼辦? 那啓動後豈不是找不到對應的採集位置了。在日誌的這個場景下文件名其實很是不可靠,文件的重命名、刪除、軟鏈等都會致使相同的文件名在不一樣時刻其實指向的是不一樣的文件,並且將整個文件路徑在內存中保存實際上是很是耗費內存的。Linux內核提供了inode能夠做爲文件的標識信息,並且保證同一時刻Inode是不會重複的,這樣就能夠解決上面的問題,在點位文件中記錄文件的inode和採集的位置便可。日誌採集Agent啓動後經過文件發現找到要採集的文件,經過獲取Inode而後從點位文件中查找對應的採集位置,最後接着後面繼續採集便可。那麼即便文件重命名了可是它的Inode不會變化,因此仍是能夠從點位文件中找到對應的採集位置。可是Inode有沒有限制呢? 固然有,天下沒有免費的午飯,不一樣的文件系統Inode會重複,一個機器能夠安裝多個文件系統,因此咱們還須要經過dev(設備號)來進一步區分,因此點位文件中須要記錄的就是dev、inode、offset三元組。到此爲止咱們的採集Agent能夠正常的採集日誌了,即便Crash了再次啓動後仍然能夠繼續進行採集。可是忽然有一天咱們發現有兩個文件竟然是同一個Inode,Linux內核不是保證同一時刻不會重複的嗎?難道是內核的bug?注意我用的是「同一時刻」,內核只能保證在同一時刻不會重複,這究竟是什麼意思呢? 這即是日誌採集Agent中會遇到的一個比較大的技術挑戰,如何準確的標識一個文件。post

如何識別一個文件?

如何標識一個文件算是日誌採集Agent中一個比較有挑戰的技術問題了,咱們先是經過文件名來識別,後來發現文件名並不可靠,並且還耗費資源,後來咱們換成了dev+Inode,可是發現Inode只能保證同一時刻Inode不重複,那這句話究竟是什麼意思呢? 想象一下在T1時刻有一個文件Inode是1咱們發現了並開始採集,一段時間後這個文件被刪除了,Linux內核就會將這個Inode釋放掉,新建立一個文件後Linux內核會將剛釋放的Inode又分配給這個新文件。那麼這個新文件被發現後會從點位文件中查詢上次採集到哪了,結果就會找到以前的那個文件記錄的點位了,致使新文件是從一個錯誤的位置進行採集。若是能給每個文件打上一個惟一標識或許就能夠解決這個問題,幸虧Linux內核給文件系統提供了擴展屬性xattr,咱們能夠給每個文件生成惟一標識記錄在點位文件中,若是文件被刪除了,而後建立一個新的文件即便Inode相同,可是文件標識不同,日誌採集Agent就能夠識別出來這是兩個文件了。可是問題來了,並非全部的文件系統都支持xattr擴展屬性。因此擴展屬性只是解了部分問題。或許咱們能夠經過文件的內容來解決這個問題,能夠讀取文件的前N個字節做爲文件標識。這也不失爲一種解決方案,可是這個N到底取多大呢? 越大相同的機率越小,形成沒法識別的機率就越小。要真正作到100%識別出來的通用解決方案還有待調研,姑且認爲這裏解了80%的問題吧。接下來就能夠安心的進行日誌採集了,日誌採集其實就是讀文件了,讀文件的過程須要注意的就是儘量的順序讀,充份利用Linux系統緩存,必要的時候能夠用posix_fadvise在採集完日誌文件後清除頁緩存,主動釋放系統資源。那麼何時纔算採集完一個文件呢? 採集到末尾返回EOF的時候就算採集完了。但是一會日誌文件又會有新內容產生,如何才知道有新數據了,而後繼續採集呢?阿里雲

clipboard.png

如何知道文件內容更新了?

Inotify能夠解決這個問題、經過Inotify監控一個文件,那麼只要這個文件有新增數據就會觸發事件,獲得事件後就能夠繼續採集了。可是這個方案存在一個問題就是在大量文件寫入的場景會致使事件隊列溢出,好比用戶連續寫入日誌N次就會產生N個事件,其實對於日誌採集Agent只要知道內容就更新就能夠了,至於更新幾回這個反而不重要, 由於每次採集其實都是持續讀文件,直到EOF,只要用戶是連續寫日誌,那麼就會一直採集下去。另外Intofy能監控的文件數量也是有上限的。因此這裏最簡單通用的方案就是輪詢去查詢要採集文件的stat信息,發現文件內容有更新就採集,採集完成後再觸發下一次的輪詢,既簡單又通用。經過這些手段日誌採集Agent終於能夠不中斷的持續採集日誌了,既然是日誌總會有被刪除的一刻,若是在咱們採集的過程當中被刪除了會如何? 大可放心,Linux中的文件是有引用計數的,已經打開的文件即便被刪除也只是引用計數減1,只要有進程引用就能夠繼續讀內容的,因此日誌採集Agent能夠安心的繼續把日誌讀完,而後釋放文件的fd,讓系統真正的刪除文件。可是如何知道採集完了呢? 廢話,上面不是說了採集到文件末尾就是採集完了啊,但是若是此刻還有另一個進程也打開了這個文件,在你採集完全部內容後又追加了一段內容進去,而你此時已經釋放了fd了,在文件系統上這個文件已經不在了,再也沒辦法經過文件發現找到這個文件,打開並讀取數據了,這該怎麼辦?lua

如何安全的釋放文件句柄?

Fluentd的處理方式就是將這部分的責任推給用戶,讓用戶配置一個時間,文件刪除後若是在指定的時間範圍內沒有數據新增就釋放fd,其實這就是間接的甩鍋行爲了。這個時間配置的過小會形成丟數據的機率增大,這個時間配置的太大會致使fd和磁盤空間一直被佔用形成短期自由浪費的假象。這個問題的本質上其實就是咱們不知道還有誰在引用這個文件,若是還有人在引用這個文件就可能會寫入數據,此時即便你釋放了fd資源仍然是佔用的,還不如不釋放,若是沒有任何人在引用這個文件了,那其實就能夠馬上釋放fd了。如何知道誰在引用這個文件呢? 想必你們都用過 lsof -f列出系統中進程打開的文件列表,這個工具經過掃描每個進程的/proc/PID/fd/目錄下的全部文件描述符,經過readlink就能夠查看這個描述符對應的文件路徑,例以下面這個例子:

clipboard.png

22686這個進程就打開了一個文件,fd是4,對應的文件路徑是/home/tianqian-zyf/.post.lua.swp。經過這個方法能夠查詢到文件的引用計數,若是引用計數是1,也就是隻有當前進程引用,那麼基本上能夠作到安全的釋放fd,不會形成數據丟失,可是帶來的問題就是開銷有點大,須要遍歷全部的進程查看它們的打開文件表逐一的比較,複雜度是O(n),若是能作到O(1)這個問題纔算完美解決。經過搜索相關的資料我發現這個在用戶態來作幾乎是沒有辦法作到的,Linux內核沒有暴露相關的API。只能經過Kernel的方式來解決,好比添加一個API經過fd來獲取文件的引用計數。這在內核中仍是比較容易作到的,每個進程都保存了打開的文件,在內核中就是struct file結構,經過這個結構就能夠找到這個文件對應的struct inode對象,這個對象內部就維護了引用計數值。期待後續Linux內核可以提供相關的API來完美解決這個問題吧。

clipboard.png

總結

到此爲此,一個基於文件的採集Agen涉及到的核心技術點都已經介紹完畢了,這其中涉及到不少文件系統、Linux相關的知識,只有掌握好這些知識才能更好的駕馭日誌採集。想要編寫一個可靠的日誌採集Agent確保數據不丟失,這其中的複雜度和挑戰不容忽視。但願經過本文能讓讀者對日誌採集有一個較爲全面的認知。

本文做者:中間件小哥
閱讀原文本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索