背景
眾所周知,日誌是記錄應用程式執行狀態的一種重要工具,在業務服務中,日誌更是十分重要。通常情況下,日誌主要是記錄關鍵執行點、程式執行錯誤時的現場資訊等。系統出現故障時,運維人員一般先檢視錯誤日誌,定位故障原因。當業務流量小、邏輯複雜度低時,應用出現故障時錯誤日誌一般較少,運維人員一般能夠根據錯誤日誌迅速定位到問題。但是,隨著業務邏輯的迭代,系統接入的依賴服務不斷增多,引入的元件不斷增多,當系統出現故障時(如Bug被觸發、依賴服務超時等等),錯誤日誌的量級會急劇增加。極端情況下甚至出現“瘋狂報錯”的現象,這時候錯誤日誌的內容會存在相互掩埋、相互影響的問題,運維人員面對報錯一時難以理清邏輯,有時甚至顧此失彼,沒能第一時間解決最核心的問題。
錯誤日誌是系統報警的一種,實際生產中,運維人員能夠收到的報警資訊多種多樣。如果在報警流出現的時候,透過處理程式,將報警進行聚類,整理出一段時間內的報警摘要,那麼運維人員就可以在摘要資訊的幫助下,先對當前的故障有一個大致的輪廓,再結合技術知識與業務知識定位故障的根本原因。
圍繞上面描述的問題,以及對於報警聚類處理的分析假設,本文主要做了以下事情:
選定聚類演算法,簡單描述了演算法的基本原理,並給出了針對報警日誌聚類的一種具體的實現方案。
在分散式業務服務的系統下構造了三種不同實驗場景,驗證了演算法的效果,並且對演算法的不足進行分析闡述。
目標
對一段時間內的報警進行聚類處理,將具有相同根因的報警歸納為能夠涵蓋報警內容的泛化報警(Generalized Alarms),最終形成僅有幾條泛化報警的報警摘要。如下圖1所示意。
設計
如圖2所示,異常報警根因分析的設計大致分為四個部分:收集報警資訊、提取報警資訊的關鍵特徵、聚類處理、展示報警摘要。
聚類演算法採用論文“Clustering Intrusion Detection Alarms to Support Root Cause Analysis” [KLAUS JULISCH, 2002]中描述的根因分析演算法。該演算法基於一個假設:將報警日誌叢集經過泛化,得到的泛化報警能夠表示報警叢集的主要特徵。以下面的例子來說明,有如下的幾條報警日誌:
我們可以將這幾條報警抽象為:“全部伺服器 網路呼叫 故障”,該泛化報警包含的範圍較廣;也可以抽象為:“server_room_a伺服器 網路呼叫 產品資訊獲取失敗”和“server_room_b伺服器 RPC 獲取產品型別資訊失敗”,此時包含的範圍較小。當然也可以用其他層次的抽象來表達這個報警叢集。
我們可以觀察到,抽象層次越高,細節越少,但是它能包含的範圍就越大;反之,抽象層次越低,則可能無用資訊越多,包含的範圍就越小。
這種抽象的層次關係可以用一些有向無環圖(DAG)來表達,如圖3所示:
屬性(Attribute):構成報警日誌的某一類資訊,如機器、環境、時間等,文中用Ai表示。
值域(Domain):屬性Ai的域(即取值範圍),文中用Dom(Ai)表示。
泛化層次結構(Generalization Hierarchy):對於每個Ai都有一個對應的泛化層次結構,文中用Gi表示。
不相似度(Dissimilarity):定義為d(a1, a2)。它接受兩個報警a1、a2作為輸入,並返回一個數值量,表示這兩個報警不相似的程度。與相似度相反,當d(a1, a2)較小時,表示報警a1和報警a2相似。為了計算不相似度,需要使用者定義泛化層次結構。
為了計算d(a1, a2),我們先定義兩個屬性的不相似度。令x1、x2為某個屬性Ai的兩個不同的值,那麼x1、x2的不相似度為:在泛化層次結構Gi中,透過一個公共點父節點p連線x1、x2的最短路徑長度。即d(x1, x2) := min{d(x1, p) + d(x2, p) | p ∈ Gi, x1 ⊴ p, x2 ⊴ p}。例如在圖3的泛化層次結構中,d("Thrift", "Pigeon") = d("RPC", "Thrift") + d("RPC", "Pigeon") = 1 + 1 = 2。
對於兩個報警a1、a2,其計算方式為:
例如:a1 = ("server_room_b-biz_tag-offline02", "Thrift"), a2 = ("server_room_a-biz_tag-online01", "Pigeon"), 則d(a1, a2) = d("server_room_b-biz_tag-offline02", "server_room_a-biz_tag-online01") + d(("Thrift", "Pigeon") = d("server_room_b-biz_tag-offline02", "伺服器") + d("server_room_a-biz_tag-online01", "伺服器") + d("RPC", "Thrift") + d("RPC", "Pigeon") = 2 + 2 + 1 + 1 = 6。
我們用C表示報警集合,g是C的一個泛化表示,即滿足∀ a ∈ C, a ⊴ g。以報警集合{"dx-trip-package-api02 Thrift get deal list error.", "dx-trip-package-api01 Thrift get deal list error."}為例,“dx伺服器 thrift呼叫 獲取產品資訊失敗”是一個泛化表示,“伺服器 網路呼叫 獲取產品資訊失敗”也是一個泛化表示。對於某個報警聚類來說,我們希望獲得既能夠涵蓋它的集合又有最具象化的表達的泛化表示。為了解決這個問題,定義以下兩個指標:
基於以上的概念,將報警日誌聚類問題定義為:定義L為一個日誌集合,min_size為一個預設的常量,Gi(i = 1, 2, 3……n) 為屬性Ai的泛化層次結構,目標是找到一個L的子集C,滿足 |C| >= min_size,且H(C)值最小。min_size是用來控制抽象程度的,極端情況下如果min_size與L集合的大小一樣,那麼我們只能使用終極抽象了,而如果min_size = 1,則每個報警日誌是它自己的抽象。找到一個聚類之後,我們可以去除這些元素,然後在L剩下的集合裡找其他的聚類。
不幸的是,這是個NP完全問題,因此論文提出了一種啟發式演算法,該演算法滿足|C| >= min_size,使H(C)值儘量小。
演算法描述
演算法假設所有的泛化層次結構Gi都是樹,這樣每個報警叢集都有一個唯一的、最頂層的泛化結果。
將L定義為一個原始的報警日誌集合,演算法選擇一個屬性Ai,將L中所有報警的Ai值替換為Gi中Ai的父值,透過這一操作不斷對報警進行泛化。
持續步驟2的操作,直到找到一個覆蓋報警數量大於min_size的泛化報警為止。
輸出步驟3中找到的報警。
演算法虛擬碼如下所示:
其中第7行的啟發演算法為:
這裡的邏輯是:如果有一個報警a滿足 a[count]>= min_size,那麼對於所有屬性Ai , 均能滿足Fi >= fi(a[Ai]) >= min_size。反過來說,如果有一個屬性Ai的Fi值小於min_size,那麼a[count]就不可能大於min_size。所以選擇Fi值最小的屬性Ai進行泛化,有助於儘快達到聚類的條件。
此外,關於min_size的選擇,如果選擇了一個過大的min_size,那麼會迫使演算法合併具有不同根源的報警。另一方面,如果過小,那麼聚類可能會提前結束,具有相同根源的報警可能會出現在不同的聚類中。
因此,設定一個初始值,可以記作ms0。定義一個較小的值 ℇ(0 < ℇ < 1),當min_size取值為ms0、ms0 * (1 - ℇ)、ms0 * (1 + ℇ)時的聚類結果相同時,我們就說此時聚類是ℇ-魯棒的。如果不相同,則使ms1 = ms0 * (1 - ℇ),重複這個測試,直到找到一個魯棒的最小值。
需要注意的是,ℇ-魯棒性與特定的報警日誌相關。因此,給定的最小值,可能相對於一個報警日誌來說是魯棒的,而對於另一個報警日誌來說是不魯棒的。
實現
1. 提取報警特徵
根據線上問題排查的經驗,運維人員通常關注的指標包括時間、機器(機房、環境)、異常來源、報警日誌文字提示、故障所在位置(程式碼行數、介面、類)、Case相關的特殊ID(訂單號、產品編號、使用者ID等等)等。
但是,我們的實際應用場景都是線上準實時場景,時間間隔比較短,因此我們不需要關注時間。同時,Case相關的特殊ID不符合我們希望獲得一個抽象描述的要求,因此也無需關注此項指標。
綜上,我們選擇的特徵包括:機房、環境、異常來源、報警日誌文字關鍵內容、故障所在位置(介面、類)共5個。
2. 演算法實現
(1) 提取關鍵特徵
我們的資料來源是日誌中心已經格式化過的報警日誌資訊,這些資訊主要包含:報警日誌產生的時間、服務標記、在程式碼中的位置、日誌內容等。
故障所在位置
優先查詢是否有異常堆疊,如存在則查詢第一個原生程式碼的位置;如果不存在,則取日誌列印位置。
異常來源
獲得故障所在位置後,優先使用此資訊確定異常報警的來源(需要預先定義詞典支援);如不能獲取,則在日誌內容中根據關鍵字匹配(需要預先定義詞典支援)。
報警日誌文字關鍵內容
優先查詢是否有異常堆疊,如存在,則查詢最後一個異常(通常為真正的故障原因);如不能獲取,則在日誌中查詢是否存在“code=……,message=……” 這樣形式的錯誤提示;如不能獲取,則取日誌內容的第一行內容(以換行符為界),並去除其中可能存在的Case相關的提示資訊
提取“機房和環境”這兩個指標比較簡單,在此不做贅述。
(2) 聚類演算法
演算法的執行,我們以圖4來表示。
考慮到日誌資料中可能包含種類極多,且根據小規模資料實驗表明,min_size = 1/5 * 報警日誌數量時,演算法已經有較好的表現,再高會增加過度聚合的風險,因此我們取min_size = 1/5 * 報警日誌數量,ℇ參考論文中的實驗,取0.05。
(4) 聚類停止條件
考慮到部分場景下,報警日誌可能較少,因此min_size的值也較少,此時聚類已無太大意義,因此設定聚類停止條件為:聚類結果的報警摘要數量小於等於20或已經存在某個類別的count值達到min_size的閾值,即停止聚類。
3. 泛化層次結構
泛化層次結構,用於記錄屬性的泛化關係,是泛化時向上抽象的依據,需要預先定義。
根據實驗所用專案的實際使用環境,我們定義的泛化層次結構如下:
實驗
以下三個實驗均使用C端API系統。
1. 單依賴故障
實驗材料來自於線上某業務系統真實故障時所產生的大量報警日誌。
環境:線上
故障原因:產品中心線上單機故障
報警日誌數量:939條
部分原始報警日誌如圖9所示,初次觀察時,很難理出頭緒。
經過聚類後的報警摘要如表1所示:
我們可以看到前三條報警摘要的Count遠超其他報警摘要,並且它們指明瞭故障主要發生在產品中心的介面。
2. 無相關的多依賴同時故障
實驗材料為利用故障注入工具,在Staging環境模擬運營置頂服務和A/B測試服務同時產生故障的場景。
環境:Staging(使用線上錄製流量和壓測平臺模擬線上正常流量環境)
模擬故障原因:置頂與A/B測試介面大量超時
報警日誌數量:527條
部分原始報警日誌如圖10所示:
經過聚類後的報警摘要如表2所示:
從上表可以看到,前兩條報警摘要符合本次試驗的預期,定位到了故障發生的原因。說明在多故障的情況下,演算法也有較好的效果。
3. 中介軟體與相關依賴同時故障
實驗材料為利用故障注入工具,在Staging環境模擬產品中心服務和快取服務同時產生超時故障的場景。
環境:Staging(使用線上錄製流量和壓測平臺模擬線上正常流量環境)
模擬故障原因:產品中心所有介面超時,所有快取服務超時
報警日誌數量:2165
部分原始報警日誌如圖11所示:
經過聚類後的報警摘要如表3所示:
從上表可以看到,快取(Squirrel和Cellar雙快取)超時最多,產品中心的超時相對較少,這是因為我們系統針對產品中心的部分介面做了兜底處理,當超時發生時後先查快取,如果快取查不到會穿透呼叫一個離線資訊快取系統,因此產品中心超時總體較少。
綜合上述三個實驗得出結果,演算法對於報警日誌的泛化是具有一定效果。在所進行實驗的三個場景中,均能夠定位到關鍵問題。但是依然存在一些不足,報警摘要中,有的經過泛化的資訊過於籠統(比如Other Exception)。
經過分析,我們發現主要的原因有:其一,對於錯誤資訊中關鍵欄位的提取,在一定程度上決定了向上泛化的準確度。其二,系統本身日誌設計存在一定的侷限性。
同時,在利用這個泛化後的報警摘要進行分析時,需要使用者具備相應領域的知識。
未來規劃
本文所關注的工作,主要在於驗證聚類演算法效果,還有一些方向可以繼續完善和最佳化:
日誌內容的深度分析。本文僅對報警日誌做了簡單的關鍵字提取和人工標記,未涉及太多文字分析的內容。我們可以透過使用文字分類、文字特徵向量相似度等,提高日誌內容分析的準確度,提升泛化效果。
多種聚類演算法綜合使用。本文僅探討了處理系統錯誤日誌時表現較好的聚類演算法,針對系統中多種不同型別的報警,未來也可以配合其他聚類演算法(如K-Means)共同對報警進行處理,最佳化聚合效果。
自適應報警閾值。除了對報警聚類,我們還可以透過對監控指標的時序分析,動態管理報警閾值,提高告警的質量和及時性,減少誤報和漏告數量。
參考資料
Julisch, Klaus. "Clustering intrusion detection alarms to support root cause analysis." ACM transactions on information and system security (TISSEC) 6.4 (2003): 443-471.
https://en.wikipedia.org/wiki/Cluster_analysis
作者簡介
劉瑒,美團點評後端工程師。2017 年加入美團點評,負責美團點評境內度假的業務開發。
千釗,美團點評後端工程師。2017 年加入美團點評,負責美團點評境內度假的業務開發。