概述
日志從最初面向人類演變到現在的面向機器發生了巨大的變化。最初的日志主要的消費者是軟體工程師,他們通過讀取日志來排查問題,如今,大量機器日夜處理日志資料以生成可讀性的報告以此來幫助人類做出決策。在這個轉變的過程中,日志采集Agent在其中扮演着重要的角色。
作為一個日志采集的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
從頭開始寫一個日志采集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
- 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中會遇到的一個比較大的技術挑戰,如何準确的辨別一個檔案。
如何識别一個檔案?
如何辨別一個檔案算是日志采集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-zyf@ubuntu:~$ 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確定資料不丢失,這其中的複雜度和挑戰不容忽視。希望通過本文能讓讀者對日志采集有一個較為全面的認知。