Logtail技術分享(一) : Polling + Inotify 組合下的日誌保序採集方案

摘要: logtail是阿里雲一款進行日誌實時採集的Agent,當前幾十萬台部署logtail的設備運行在各種不同環境上(集團、螞蟻、阿里雲,還有用戶部署在公網、IOT設備),每天採集數PB的數據,支撐上千種應用的日誌採集。

日誌數據採集

提到數據分析,大部分人首先想到的都是Hadoop,流計算,API等數據加工的方式。如果從整個過程來看,數據分析其實包含了4個過程:採集,存儲,計算和理解四個步驟。

  • 採集:從各種產生數據的源頭,將數據集中到存儲系統。包括硬碟上的歷史數據,用戶網頁的點擊,感測器等等
  • 存儲:以各種適合計算的模式集中式存儲數據,其中既包含大規模的存儲系統(例如數倉),也有例如臨時的存儲(例如Kafka類消息中間件)
  • 計算:形態多種多樣,但大部分計算完成後會將結果再放入存儲
  • 理解:利用機器學習、可視化、通知等手段將結果呈現出來

數據採集是一門很大的範疇,從實時性上和規模上分,一般可以分為3類:

  • 實時採集:例如日誌,database change log等
  • 定時任務:例如每隔5分鐘從FTP或數據源去批量導出數據
  • 線下導數據:例如郵寄硬碟,AWS Snowmobile 卡車等 從數據的價值以及體量上而言,實時數據採集毫無疑問最重要的,而其中最大的部分就是日誌實時採集。

日誌採集Agent做了哪些工作?

日誌採集Agent看起來很簡單:安裝在操作系統中,將實時產生的日誌(文本)數據採集到類似消息中間件(類似Kafka)服務中。很多人可能覺得這是一個tail 命令就能幹的,哪有這麼複雜?

如果我們把其中細節展開就會發現一大堆工作,除了需要解決分散式日誌匯聚的問題,還需要處理各種日誌格式、不同採集目錄、不同運行環境、多租戶資源隔離、資源限制、配置管理、系統監控、容錯、升級等等問題,而日誌採集Agent就是為了解決這些問題應運而生的產物。

試想如果不用Agent,就拿最簡單的收集nginx訪問日誌來講,需要寫一個腳本定期檢測access.log有無更新,把更新的日誌發送到服務端,除此之外還需要將原始訪問日誌解析成key/value欄位、處理日誌輪轉、處理本地/服務端網路異常、處理訪問流量burst時的削峰填谷、處理腳本異常等等,當一個接一個的問題解決完之後,回過頭原來你又造了一遍輪子。

阿里雲日誌服務 的logtail就是一款進行日誌實時採集的Agent,當前幾十萬台部署logtail的設備運行在各種不同環境上(集團、螞蟻、阿里雲,還有用戶部署在公網、IOT設備),每天採集數PB的數據,支撐上千種應用的日誌採集。從剛開始幾個應用、幾千台、每天幾T數據的規模發展到今天,我們踩過很多坑,也從中學到很多,積累了很多寶貴的經驗。

本期主要和大家一起分享logtail設計中對於輪詢和事件模式共存情況下如何解決日誌採集保序、高效、可靠的問題。

為什麼要輪詢+事件

什麼是輪詢什麼是事件

對於日誌採集,大家很容易想到通過定期檢測日誌文件有無更新來進行日誌採集,這種我們一般稱之為輪詢(polling)的方式。輪詢是一種主動探測的收集方式,相對也存在被動監聽的方式,我們一般稱之為事件模式。事件模式依賴於操作系統的事件通知,在linux下2.6.13內核版本引入inotify, 而windows在xp中引入FindFirstChangeNotification,兩者都支持以被動監聽的方式獲取日誌文件的修改事件。

輪詢vs事件

下面來看看輪詢和事件之間的區別,對比如下:

輪詢相對事件的實現複雜度要低很多、原始支持跨平台而且對於系統限制性不高;但輪詢的採集延遲(默認加上輪詢間隔一半的採集延遲)以及資源消耗較高,而且在文件規模較大(十萬級/百萬級)時輪詢一次的時間較長,採集延遲非常高。

傳統Agent怎麼做

一般Agent(例如logstash、fluentd、filebeats、nxlog等)都採用基於輪詢的方式,相對事件實現較為簡單,而且對於大部分輕量級場景基本適用。但這種方式就會暴露以上對比中出現的採集延遲、資源消耗以及大規模環境支持的問題,部分對於這些條件要求較高的應用只能望而卻步。

logtail的方案是什麼

為了同時兼顧採集效率以及支持各類特殊採集場景,logtail使用了輪詢與事件並存的混合方式(目前只支持linux,windows下方案正在集成中)。一方面借力inotify的低延遲與低性能消耗,另一方面使用輪詢兼容不支持事件的運行環境。然而混合方案相比純粹輪詢/事件的方案都要複雜,這裡主要存在3個問題:

  1. 如何解決高效採集的問題
  2. 如何解決日誌順序保證問題
  3. 如何保證可靠性問題

下面圍繞這些問題對我們的方案進行展開

logtail輪詢+inotify事件實現方式

輪詢+inotify事件混合方案簡介

logtail內部以事件的方式觸發日誌讀取行為,輪詢和inotify作為較為獨立的兩個模塊,對於同一文件/模塊會分別產生獨立的Create/Modify/Delete事件,事件分別存儲於Polling Event Queue和Inotify Event Queue中。

輪詢模塊由DirFilePolling和ModifyPolling兩個線程組成,DirFilePolling負責根據用戶配置定期遍歷文件夾,將符合日誌採集配置的文件加入到modify cache中;ModifyPolling負責定期掃描modify cache中文件狀態,對比上一次狀態(Dev、Inode、Modify Time、Size),若發現更新則生成modify event。

Inotify屬於事件監聽方式,因此不存在獨立線程,該模塊根據用戶配置監聽對應的目錄以及子目錄,當監聽目錄存在變化,內核會將事件push到相應的file descriptor中。

最終由Event Handler線程負責將兩個事件隊列合併(merge)到內部的Event Queue中,並處理相應的Create/Modify/Delete事件,進行實際的日誌讀取。

高效性如何保證

相信讀者在看到混合兩個字時一定想到一個非常明顯的問題:logtail採用了兩種方案,那是不是開銷就是2倍啊?答案當然不是,logtail在混合方案中採取了以下幾個措施來保證兩種方案混合的情況下如何采兩家之長並儘可能去兩家之短:

  1. 事件合併(merge):為減少輪詢產生的事件和inotify產生的事件多次觸發事件處理行為,logtail在事件處理之前將重複的輪詢/inotify事件進行合併,減少無效的事件處理行為;
  2. 輪詢自動降級:如果在系統支持且資源足夠的場景下,inotify無論從延遲和性能消耗都要優於輪詢,因此當某個目錄inotify可以正常工作時,則該目錄的輪詢進行自動降級,輪詢間隔大幅降低到對CPU基本無影響的程度;
  3. 輪詢與inotify cache共享:日誌採集中的很大一部分開銷來源於日誌文件匹配,在集團內外經常會出現一台機器上logtail配置了上百種不同的配置的情況,對於一個文件需要對上百個配置進行逐一判斷是否匹配。logtail內部對於匹配結果維護了一個cache,而且cache對於輪詢和inotify共享,儘可能減少這部分較大的開銷。

日誌收集順序保證

日誌收集順序難點分析

日誌順序性保證是日誌採集需要提供的基本功能,也是較難實現的一種功能,尤其在以下幾種場景並存的情況下:

  1. 日誌輪轉(rotate):日誌輪轉是指當日誌滿足一定條件(日誌跨天、超過一定條數、超過一定大小)進行重命名/壓縮/刪除後重新創建並寫入的情況,例如Ngnix訪問日誌可設置以20M位單位進行輪轉,當日誌超過20M時,將access.log重命名為access.log.1,之前的access.log.1重命名為access.log.2,以此類推。agent需要保證日誌輪轉時收集順序與日誌產生順序相同;
  2. 不同配置方式:優秀的日誌採集agent並不應該強制限制用戶的配置方式,尤其在指定日誌採集文件名時,有的用戶習慣配置成*.log,有的用戶習慣配置成*.log*,而無論哪種配置agent都應該能夠兼容,不會出現*.log在日誌輪轉情況下少收集或*.log*在日誌輪轉情況下多收集的情況;
  3. 輪詢與inotify並存問題:若系統不支持inotify,則只有輪詢產生的事件,而若inotify正常工作,那麼同一文件的修改會產生兩次事件,而且由於inotify延遲較低,所以事件很可能會先於輪詢的事件被處理。我們需要保證延遲到來的事件不會影響日誌exactly once的讀取;

基於輪轉隊列與文件簽名的日誌採集方法

基本概念

在logtail中,我們設計了一套用於在日誌輪轉、不同用戶配置、輪詢與inotify並存、日誌解析阻塞情況下依然可以保證日誌採集順序的機制。本文將重點該機制的實現方法,在展開之前首先介紹logtail中用到的幾個基本概念:

  • 文件的dev和inode標識

    dev這裡指的是設備編號、 inode是該文件在file system中的唯一標識,通過dev+inode的組合可唯一標識一個文件(這裡需要排除硬連接)。文件的move操作雖然可以改變文件名,但並不涉及文件的刪除創建,dev+inode並不會變化,因此通過dev+inode可以非常方便的判斷一個文件是否發生了輪轉。

  • inode引用計數

    每個文件都對應著一個inode,inode指向文件的meta信息,其中有一個欄位是reference count,默認文件創建時引用計數為1,引用計數為0時文件被文件系統回收。以下情況會改變文件的引用計數:若文件open,則引用計數加1,文件close後減1;硬連接創建引用計數加1;文件/硬鏈接刪除,引用計數減1。因此,雖然文件被刪除,但只要有應用保持該文件的open狀態,則該文件並不會被文件系統回收,應用還可以對該文件進行讀取。
  • 文件簽名(signature)

    dev+inode只能保證同一時刻該文件的唯一性,但並不代表整個life cycle中的唯一性。在文件從文件系統中刪除時,對應的inode也會被回收,內核file system實現中存在分配唯一inode的機制,為了提高inode分配性能,回收的inode會保留在文件系統的cache中,下一次創建文件時,若存在inode cache則直接將該inode賦給新文件。因此純粹通過dev+inode判斷輪轉並不可行(例如日誌文件到達一定size被刪除後,重新創建繼續寫,只要期間沒有其他文件創建,則dev+inode都沒變),logtail中使用日誌文件的前1024位元組的hash作為該文件的簽名(signature),只有當dev+inode+signature一致的情況下才會認為該文件是輪轉的文件。

在logtail的設計中利用了以上幾個概念的功能,下面介紹一下日誌收集順序保證的幾個數據結構:

  • LogFileReader

    LogFileReader存儲了日誌文件讀取的元數據,包括sorcePath、signature、devInode、deleteFlag、filePtr、readOffset、lastUpdateTime、readerQueue(LogFileReaderQueue)。其中sorcePath是reader文件路徑,,signature是文件的簽名,devInode是改文件的dev+inode組合,deleteFlag用於標識該文件是否被刪除,filePtr是文件指針,readOffset代表當前日誌解析進度,lastUpdateTime記錄最後一次進行讀取的時間,readerQueue標識該reader所在的讀取隊列(參見下面介紹)。
  • LogFileReaderQueue

    LogFileReaderQueue中存儲sourcePath相同且未採集完畢的reader列表,reader按照日誌文件創建順序進行排列。
  • NamedLogFileReaderQueueMap

    以sourcePath為key/LogFileReaderQueue為value的map,用於存儲當前正在讀取的所有ReaderQueue
  • DevInodeLogFileReaderMap

    以devInode為key/LogFileReader為value的map,用於存儲當前正在讀取的所有Reader
  • RotatorLogFileReaderMap

    以devInode為key/LogFileReader為value的map,用於存儲處於輪轉狀態且已經讀取完畢的Reader

事件處理流程

logtail基於以上的數據結構實現了日誌數據順序讀取,具體處理流程如下:

CreateEvent處理方式

  1. 對於日誌的Create Event,首先從當前的devInodeReaderMap中查找是否存在該dev+inode的Reader(因為在輪詢和Inotify共存的情況下,可能會出現在處理Create Event時Reader已經被創建的情況),若不存在則創建Reader。
  2. Reader通過dev+inode和sourcePath創建,創建Reader後需加入到devInodeReaderMap以及其sourcePath對應的ReaderQueue尾部

DeleteEvent處理方式

  1. 對於日誌文件的Delete Event,若該Reader所在隊列長度大於1(當前解析進度落後,文件雖被刪除但日誌未採集完成),則忽略此Delete事件;若Reader所在隊列長度為1,設置該Reader的deleteFlag,若一定時間內該Reader沒有處理過Modify事件且日誌解析完畢則刪除該Reader

ModifyEvent處理方式

  1. 首先根據dev+inode查找devInodeReaderMap,找到該Reader所在的ReaderQueue,獲取ReaderQueue的隊列首部的Reader進行日誌讀取操作;
  2. 日誌讀取時首先檢查signature是否改變,若改變則認為日誌被truncate寫,從文件頭開始讀取;若signature未改變,則從readOffset處開始讀取並更新readOffset
  3. 若該日誌文件讀取完畢(readOffset==fileSize)且ReaderQueue的size > 1,則從ReaderQueue中移除該Reader並加入到rotatorReadrMap中(日誌已經發生了輪轉,且輪轉後的文件已經讀取完畢,所以可以從ReaderQueue中移除),此時繼續把Modify Event push到Event隊列中,觸發隊列後續文件的讀取,進入下一循環;若日誌文件讀取完畢且ReaderQueue的size==1(size為1說明該文件並沒有輪轉,極有可能後續還有寫入,所以不能從ReaderQueue中移除),則完成次輪Modify Event處理,進入下一循環
  4. 若日誌文件沒有讀取完成,則把Modify Event push到Event隊列中,進入下一循環(避免所有時間都被同一文件佔用,保證日誌文件讀取公平性)
  • RotatorLogFileReaderMap主要用於解決輪詢事件延遲問題:當inotify事件處理完成、日誌讀取完畢、ReaderQueue size > 1同時發生,若直接刪除該Reader,則輪詢的事件到達時,將會查找不到Reader並創建一個新的Reader重新進行日誌讀取。因此我們在Reader讀取完畢時將其放入到RotatorLogFileReaderMap保存,若事件查找不到Reader時會檢測RotatorLogFileReaderMap,若存在則跳過此次事件處理,避免多重事件造成日誌重複採集的情況。

日誌採集可靠性保證

考慮到性能、資源、性價比等問題,logtail在設計之初並不保證exact once或者at least once,但這並不代表logtail不可靠,有很多用戶基於logtail採集的access日誌用來計費。下面主要介紹可靠性中較難解決的三個場景:

  1. 日誌解析阻塞:由於各種原因(網路阻塞、日誌burst寫入、流量控制、CPU/磁碟負載)等問題可能造成日誌解析進度落後於日誌產生速度,而在此時若發生日誌輪轉,logtail需在有限資源佔用情況下儘可能保證輪轉後的日誌文件不丟失
  2. 採集配置更新/進程升級:配置更新或進行升級時需要中斷採集並重新初始化採集上下文,logtail需要保證在配置更新/進程升級時即使日誌發生輪轉也不會丟失日誌
  3. 進程crash、宕機等異常情況:在進程crash或宕機時,logtail需儘可能保證日誌重複採集數儘可能的少丟失日誌

日誌採集阻塞處理

正常情況下,日誌採集進度和日誌產生進度一致,此時ReaderQueue中只有一個Reader處於採集狀態。如上圖所示,正在被採集的access.log由於磁碟上存在、應用和logtail正在打開,所以引用計數為3,其他輪轉的日誌文件引用計數為1。

而當應用日誌burst寫入、網路暫時性阻塞、服務端Quota不足、CPU/磁碟負載較高等情況發生,日誌採集進度可能落後於日誌產生進度,此時我們希望logtail能夠在一定的資源限制下儘可能保留住這些日誌,等待網路恢復或系統負載下降時將這些日誌採集到伺服器,並且保證日誌採集順序不會因為採集阻塞而混亂。

如上圖所示,logtail內部通過保持輪轉日誌file descriptor的打開狀態來防止日誌採集阻塞時未採集完成的日誌文件被file system回收(在ReaderQueue中的file descriptor一直保持打開狀態,保證文件引用計數至少為1)。通過ReaderQueue的順序讀取保證日誌採集順序與日誌產生順序一致。

  1. 當ReaderQueue的size大於1時說明日誌解析出現阻塞,此時logtail會將該ReaderQueue中所有Reader的file descriptor保持打開狀態,這樣即使在日誌文件輪轉後被刪除或被壓縮(本質還是被刪除)時logtail依然能夠採集到該日誌。
  2. 當日誌輪轉時(dev+inode變化,文件名未變),logtail會根據新的dev+inode創建Reader,並加入其文件名對應的ReaderQueue尾部,ReaderQueue保持順序讀取,以此保證日誌文件解析順序。
  • 若日誌採集進度一直低於日誌產生進度,則很有可能出現ReaderQueue會無限增長的情況,因此logtail內部對於ReaderQueue設置了上限,當size超過上限時禁止後續Reader的創建

配置更新/升級過程處理

logtail配置採用中心化的管理方式,用戶只需在管理頁面配置,保存後會自動將配置更新到遠程的logtail節點。此外logtail具備自動升級的功能,當推出新版本時,logtail會自動從伺服器下載最新版本並升級到該版本。

  • 為保證配置更新/升級過程中日誌數據不丟失,在logtail升級過程中,會將當前所有Reader的狀態保存到內存/本地的checkpoint文件中;當新配置應用/新版本啟動後,會載入上一次保存的checkpoint,並通過checkpoint恢復Reader的狀態。
  • 然而在老版本checkpoint保存完畢到新版本Reader創建完成的時間段內,很有可能出現日誌輪轉的情況,因此新版本在載入checkpoint時,會檢查對應checkpoint的文件名、dev+inode有無變化
  1. 若文件名與dev+inode未變且signature未變,則直接根據該checkpoint創建Reader
  2. 若文件名與dev+inode變化則從當前目錄查找對應的dev+inode,若查找到則對比signature是否變化;若signature未變則認為是文件輪轉,根據新文件名創建Reader;若signature變化則認為是該文件被刪除後重新創建,忽略該checkpoint。

進程crash、宕機等異常情況處理

  1. 進程異常crash:logtail運行時會產生兩個進程,分別是守護進程和工作進程,當工作進程異常crash時(概率極低)守護進程會立即重新拉起工作進程
  2. 進程重新啟動時狀態恢復:logtail除配置更新/進程升級會保存checkpoint外,還會定期將採集進度dump到本地,進程重新啟動的過程與版本升級的過程相似:除了恢復正常日誌文件狀態外,還會查找輪轉後的日誌,儘可能降低日誌丟失風險

作者:元乙

原文鏈接:Logtail技術分享(一) : Polling + Inotify 組合下的日誌保序採集方案

更多技術乾貨敬請關注云棲社區知乎機構號:阿里云云棲社區 - 知乎

推薦閱讀:

基於傳輸層TCP、UDP協議的自定義應用層協議如何實現?
Akka HTTP 文檔 (非官方漢化)- 導讀
談談 HTTPS
如何使用 Charles 抓包並分析 Http 報文
【RPU-A】官網 HTTP 指南基於新 HttpClient 重構

TAG:日志 | HTTP | 日志分析 |