日誌採集中的關鍵技術分析
概述
日誌從最初面向人類演變到現在的面向機器發生了巨大的變化。最初的日誌主要的消費者是軟體工程師,他們通過讀取日誌來排查問題,如今,大量機器日夜處理日誌資料以生成可讀性的報告以此來幫助人類做出決策。在這個轉變的過程中,日誌採集Agent在其中扮演著重要的角色。
作為一個日誌採集的Agent簡單來看其實就是一個將資料從源端投遞到目的端的程式,通常目的端是一個具備資料訂閱功能的集中儲存,這麼做的目的其實是為了將日誌分析和日誌儲存解耦,同一份日誌可能會有不同的消費者感興趣,獲取到日誌後所處理的方式也會有所不同,通過將資料儲存和資料分析進行解耦後,不同的消費者可以訂閱自己感興趣的日誌,選擇對應的分析工具進行分析。像這樣的具備資料訂閱功能的集中儲存業界比較流行的是DataHub
還有阿里雲的LogHub
。而資料來源端大致可以分為三類,一類就是普通的文字檔案,另外一類則是通過網路接收到的日誌資料,最後一類則是通過共享記憶體的方式,本文只會談及第一類。一個日誌採集Agent最為核心的功能大致就是這個樣子了。在這個基礎上進一步又可以引入日誌過濾、日誌格式化、路由等功能,看起來就好像是一個生產車間。從日誌投遞的方式來看,日誌採集又可以分為推模式和拉模式,本文主要分析的是推模式的日誌採集。
推模式是指日誌採集Agent主動從源端取得資料後傳送給目的端,而拉模式指的是目的端主動向日誌採集Agent獲取源端的資料
業界現狀
目前業界比較流行的日誌採集主要有Fluentd
Logstash
、Flume
、scribe
等,阿里巴巴內部則是LogAgent
、阿里雲則是LogTail
,這些產品中Fluentd
佔據了絕對的優勢併成功入駐CNCF陣營,它提出的統一日誌層(Unified Logging Layer)大大的減少了整個日誌採集和分析的複雜度。Fluentd
認為大多數現存的日誌格式其結構化都很弱,這得益於人類出色的解析日誌資料的能力,因為日誌資料其最初是面向人類的,人類是其主要的日誌資料消費者。為此Fluentd
希望通過統一日誌儲存格式來降低整個日誌採集接入的複雜度,假想下輸入的日誌資料比如有M種格式,日誌採集Agent後端接入了N種儲存,那麼每一種儲存系統需要實現M種日誌格式解析的功能,總的複雜度就是M*N
M + N
。這就是Fluentd
的核心思想,另外它的外掛機制也是一個值得稱讚的地方。Logstash
和Fluentd
類似是屬於ELK技術棧,在業界也被廣泛使用,關於兩者的對比可以參考這篇文章Fluentd vs. Logstash: A Comparison of Log Collectors從頭開始寫一個日誌採集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
,每一秒我們去更新這個點位檔案,將採集的位置實時的記錄在裡面,整個更新的過程如下:
- 將點位資料寫入到磁碟的
offset.bak
檔案中 - 通過
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中會遇到的一個比較大的技術挑戰,如何準確的標識一個檔案。
如何識別一個檔案?
如何標識一個檔案算是日誌採集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的時候就算採集完了。可是一會日誌檔案又會有新內容產生,如何才知道有新資料了,然後繼續採集呢?
如何知道檔案內容更新了?
Inotify
可以解決這個問題、通過Inotify
監控一個檔案,那麼只要這個檔案有新增資料就會觸發事件,得到事件後就可以繼續採集了。但是這個方案存在一個問題就是在大量檔案寫入的場景會導致事件佇列溢位,比如使用者連續寫入日誌N次就會產生N個事件,其實對於日誌採集Agent只要知道內容就更新就可以了,至於更新幾次這個反而不重要, 因為每次採集其實都是持續讀檔案,直到EOF,只要使用者是連續寫日誌,那麼就會一直採集下去。另外Intofy
能監控的檔案數量也是有上限的。所以這裡最簡單通用的方案就是輪詢去查詢要採集檔案的stat資訊,發現檔案內容有更新就採集,採集完成後再觸發下一次的輪詢,既簡單又通用。通過這些手段日誌採集Agent終於可以不中斷的持續採集日誌了,既然是日誌總會有被刪除的一刻,如果在我們採集的過程中被刪除了會如何? 大可放心,Linux中的檔案是有引用計數的,已經開啟的檔案即使被刪除也只是引用計數減1,只要有程序引用就可以繼續讀內容的,所以日誌採集Agent可以安心的繼續把日誌讀完,然後釋放檔案的fd,讓系統真正的刪除檔案。但是如何知道採集完了呢? 廢話,上面不是說了採集到檔案末尾就是採集完了啊,可是如果此刻還有另外一個程序也打開了這個檔案,在你採集完所有內容後又追加了一段內容進去,而你此時已經釋放了fd了,在檔案系統上這個檔案已經不在了,再也沒辦法通過檔案發現找到這個檔案,開啟並讀取資料了,這該怎麼辦?
如何安全的釋放檔案控制代碼?
Fluentd
的處理方式就是將這部分的責任推給使用者,讓使用者配置一個時間,檔案刪除後如果在指定的時間範圍內沒有資料新增就釋放fd,其實這就是間接的甩鍋行為了。這個時間配置的太小會造成丟資料的概率增大,這個時間配置的太大會導致fd和磁碟空間一直被佔用造成短時間自由浪費的假象。這個問題的本質上其實就是我們不知道還有誰在引用這個檔案,如果還有人在引用這個檔案就可能會寫入資料,此時即使你釋放了fd資源仍然是佔用的,還不如不釋放,如果沒有任何人在引用這個檔案了,那其實就可以立刻釋放fd了。如何知道誰在引用這個檔案呢? 想必大家都用過lsof -f
列出系統中程序開啟的檔案列表,這個工具通過掃描每一個程序的/proc/PID/fd/
目錄下的所有檔案描述符,通過readlink
就可以檢視這個描述符對應的檔案路徑,例如下面這個例子:
tianqian-[email protected]:~$ sudo ls -al /proc/22686/fd
total 0
dr-x------ 2 tianqian-zyf tianqian-zyf 0 May 27 12:25 .
dr-xr-xr-x 9 tianqian-zyf tianqian-zyf 0 May 27 12:25 ..
lrwx------ 1 tianqian-zyf tianqian-zyf 64 May 27 12:25 0 -> /dev/pts/19
lrwx------ 1 tianqian-zyf tianqian-zyf 64 May 27 12:25 1 -> /dev/pts/19
lrwx------ 1 tianqian-zyf tianqian-zyf 64 May 27 12:25 2 -> /dev/pts/19
lrwx------ 1 tianqian-zyf tianqian-zyf 64 May 27 12:25 4 -> /home/tianqian-zyf/.post.lua.swp
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來完美解決這個問題吧。
總結
到此為此,一個基於檔案的採集Agen涉及到的核心技術點都已經介紹完畢了,這其中涉及到很多檔案系統、Linux相關的知識,只有掌握好這些知識才能更好的駕馭日誌採集。想要編寫一個可靠的日誌採集Agent確保資料不丟失,這其中的複雜度和挑戰不容忽視。希望通過本文能讓讀者對日誌採集有一個較為全面的認知。