根因分析初探:一种报警聚类算法在业务系统的落地实施
背景
眾所周知,日志是記錄應(yīng)用程序運行狀態(tài)的一種重要工具,在業(yè)務(wù)服務(wù)中,日志更是十分重要。通常情況下,日志主要是記錄關(guān)鍵執(zhí)行點、程序執(zhí)行錯誤時的現(xiàn)場信息等。系統(tǒng)出現(xiàn)故障時,運維人員一般先查看錯誤日志,定位故障原因。當業(yè)務(wù)流量小、邏輯復雜度低時,應(yīng)用出現(xiàn)故障時錯誤日志一般較少,運維人員一般能夠根據(jù)錯誤日志迅速定位到問題。但是,隨著業(yè)務(wù)邏輯的迭代,系統(tǒng)接入的依賴服務(wù)不斷增多,引入的組件不斷增多,當系統(tǒng)出現(xiàn)故障時(如Bug被觸發(fā)、依賴服務(wù)超時等等),錯誤日志的量級會急劇增加。極端情況下甚至出現(xiàn)“瘋狂報錯”的現(xiàn)象,這時候錯誤日志的內(nèi)容會存在相互掩埋、相互影響的問題,運維人員面對報錯一時難以理清邏輯,有時甚至顧此失彼,沒能第一時間解決最核心的問題。
錯誤日志是系統(tǒng)報警的一種,實際生產(chǎn)中,運維人員能夠收到的報警信息多種多樣。如果在報警流出現(xiàn)的時候,通過處理程序,將報警進行聚類,整理出一段時間內(nèi)的報警摘要,那么運維人員就可以在摘要信息的幫助下,先對當前的故障有一個大致的輪廓,再結(jié)合技術(shù)知識與業(yè)務(wù)知識定位故障的根本原因。
圍繞上面描述的問題,以及對于報警聚類處理的分析假設(shè),本文主要做了以下事情:
目標
對一段時間內(nèi)的報警進行聚類處理,將具有相同根因的報警歸納為能夠涵蓋報警內(nèi)容的泛化報警(Generalized Alarms),最終形成僅有幾條泛化報警的報警摘要。如下圖1所示意。
我們希望這些泛化報警既要具有很強的概括性,同時盡可能地保留細節(jié)。這樣運維人員在收到報警時,便能快速定位到故障的大致方向,從而提高故障排查的效率。
設(shè)計
如圖2所示,異常報警根因分析的設(shè)計大致分為四個部分:收集報警信息、提取報警信息的關(guān)鍵特征、聚類處理、展示報警摘要。
算法選擇
聚類算法采用論文“Clustering Intrusion Detection Alarms to Support Root Cause Analysis [KLAUS JULISCH, 2002]”中描述的根因分析算法。該算法基于一個假設(shè):將報警日志集群經(jīng)過泛化,得到的泛化報警能夠表示報警集群的主要特征。以下面的例子來說明,有如下的幾條報警日志:
server_room_a-biz_tag-online02 Thrift get deal ProductType deal error. server_room_b-biz_tag-offline01 Pigeon query deal info error. server_room_a-biz_tag-offline01 Http query deal info error. server_room_a-biz_tag-online01 Thrift query deal info error. server_room_b-biz_tag-offline02 Thrift get deal ProductType deal error.我們可以將這幾條報警抽象為:“全部服務(wù)器 網(wǎng)絡(luò)調(diào)用 故障”,該泛化報警包含的范圍較廣;也可以抽象為:“server_room_a服務(wù)器 網(wǎng)絡(luò)調(diào)用 產(chǎn)品信息獲取失敗”和“server_room_b服務(wù)器 RPC 獲取產(chǎn)品類型信息失敗”,此時包含的范圍較小。當然也可以用其他層次的抽象來表達這個報警集群。
我們可以觀察到,抽象層次越高,細節(jié)越少,但是它能包含的范圍就越大;反之,抽象層次越低,則可能無用信息越多,包含的范圍就越小。
這種抽象的層次關(guān)系可以用一些有向無環(huán)圖(DAG)來表達,如圖3所示:
為了確定報警聚類泛化的程度,我們需要先了解一些定義:
- 屬性(Attribute):構(gòu)成報警日志的某一類信息,如機器、環(huán)境、時間等,文中用Ai表示。
- 值域(Domain):屬性Ai的域(即取值范圍),文中用Dom(Ai)表示。
- 泛化層次結(jié)構(gòu)(Generalization Hierarchy):對于每個Ai都有一個對應(yīng)的泛化層次結(jié)構(gòu),文中用Gi表示。
- 不相似度(Dissimilarity):定義為d(a1, a2)。它接受兩個報警a1、a2作為輸入,并返回一個數(shù)值量,表示這兩個報警不相似的程度。與相似度相反,當d(a1, a2)較小時,表示報警a1和報警a2相似。為了計算不相似度,需要用戶定義泛化層次結(jié)構(gòu)。
為了計算d(a1, a2),我們先定義兩個屬性的不相似度。令x1、x2為某個屬性Ai的兩個不同的值,那么x1、x2的不相似度為:在泛化層次結(jié)構(gòu)Gi中,通過一個公共點父節(jié)點p連接x1、x2的最短路徑長度。即d(x1, x2) := min{d(x1, p) + d(x2, p) | p ∈ Gi, x1 ? p, x2 ? p}。例如在圖3的泛化層次結(jié)構(gòu)中,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”, “服務(wù)器”) + d(“server_room_a-biz_tag-online01”, “服務(wù)器”) + 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服務(wù)器 thrift調(diào)用 獲取產(chǎn)品信息失敗”是一個泛化表示,“服務(wù)器 網(wǎng)絡(luò)調(diào)用 獲取產(chǎn)品信息失敗”也是一個泛化表示。對于某個報警聚類來說,我們希望獲得既能夠涵蓋它的集合又有最具象化的表達的泛化表示。為了解決這個問題,定義以下兩個指標:
H?值最小時對應(yīng)的g,就是我們要找的最適合的泛化表示,我們稱g為C的“覆蓋”(Cover)。
基于以上的概念,將報警日志聚類問題定義為:定義L為一個日志集合,min_size為一個預設(shè)的常量,Gi(i = 1, 2, 3……n) 為屬性Ai的泛化層次結(jié)構(gòu),目標是找到一個L的子集C,滿足 |C| >= min_size,且H?值最小。min_size是用來控制抽象程度的,極端情況下如果min_size與L集合的大小一樣,那么我們只能使用終極抽象了,而如果min_size = 1,則每個報警日志是它自己的抽象。找到一個聚類之后,我們可以去除這些元素,然后在L剩下的集合里找其他的聚類。
不幸的是,這是個NP完全問題,因此論文提出了一種啟發(fā)式算法,該算法滿足|C| >= min_size,使H?值盡量小。
算法描述
算法偽代碼如下所示:
輸入:報警日志集合L,min_size,每個屬性的泛化層次結(jié)構(gòu)G1,......,Gn 輸出:所有符合條件的泛化報警 T := L; // 將報警日志集合保存至表T for all alarms a in T doa[count] := 1; // "count"屬性用于記錄a當前覆蓋的報警數(shù)量 while ?a ∈ T : a[count] < min_size do {使用啟發(fā)算法選擇一個屬性Ai;for all alarms a in T doa[Ai] := parent of a[Ai] in Gi;while identical alarms a, a' exist doSet a[count] := a[count] + a'[count];delete a' from T; }其中第7行的啟發(fā)算法為:
首先計算Ai對應(yīng)的Fi fi(v) := SELECT sum(count) FROM T WHERE Ai = v // 統(tǒng)計在Ai屬性上值為v的報警的數(shù)量 Fi := max{fi(v) | v ∈ Dom(Ai)} 選擇Fi值最小的屬性Ai這里的邏輯是:如果有一個報警a滿足 a[count]>= min_size,那么對于所有屬性Ai , 均能滿足Fi >= fi(a[Ai]) >= min_size。反過來說,如果有一個屬性Ai的Fi值小于min_size,那么a[count]就不可能大于min_size。所以選擇Fi值最小的屬性Ai進行泛化,有助于盡快達到聚類的條件。
此外,關(guān)于min_size的選擇,如果選擇了一個過大的min_size,那么會迫使算法合并具有不同根源的報警。另一方面,如果過小,那么聚類可能會提前結(jié)束,具有相同根源的報警可能會出現(xiàn)在不同的聚類中。
因此,設(shè)置一個初始值,可以記作ms0。定義一個較小的值 ?(0 < ? < 1),當min_size取值為ms0、ms0 * (1 - ?)、ms0 * (1 + ?)時的聚類結(jié)果相同時,我們就說此時聚類是?-魯棒的。如果不相同,則使ms1 = ms0 * (1 - ?),重復這個測試,直到找到一個魯棒的最小值。
需要注意的是,?-魯棒性與特定的報警日志相關(guān)。因此,給定的最小值,可能相對于一個報警日志來說是魯棒的,而對于另一個報警日志來說是不魯棒的。
實現(xiàn)
1. 提取報警特征
根據(jù)線上問題排查的經(jīng)驗,運維人員通常關(guān)注的指標包括時間、機器(機房、環(huán)境)、異常來源、報警日志文本提示、故障所在位置(代碼行數(shù)、接口、類)、Case相關(guān)的特殊ID(訂單號、產(chǎn)品編號、用戶ID等等)等。
但是,我們的實際應(yīng)用場景都是線上準實時場景,時間間隔比較短,因此我們不需要關(guān)注時間。同時,Case相關(guān)的特殊ID不符合我們希望獲得一個抽象描述的要求,因此也無需關(guān)注此項指標。
綜上,我們選擇的特征包括:機房、環(huán)境、異常來源、報警日志文本關(guān)鍵內(nèi)容、故障所在位置(接口、類)共5個。
2. 算法實現(xiàn)
(1) 提取關(guān)鍵特征
我們的數(shù)據(jù)來源是日志中心已經(jīng)格式化過的報警日志信息,這些信息主要包含:報警日志產(chǎn)生的時間、服務(wù)標記、在代碼中的位置、日志內(nèi)容等。
- 故障所在位置
優(yōu)先查找是否有異常堆棧,如存在則查找第一個本地代碼的位置;如果不存在,則取日志打印位置。
- 異常來源
獲得故障所在位置后,優(yōu)先使用此信息確定異常報警的來源(需要預先定義詞典支持);如不能獲取,則在日志內(nèi)容中根據(jù)關(guān)鍵字匹配(需要預先定義詞典支持)。
- 報警日志文本關(guān)鍵內(nèi)容
優(yōu)先查找是否有異常堆棧,如存在,則查找最后一個異常(通常為真正的故障原因);如不能獲取,則在日志中查找是否存在“code=……,message=……” 這樣形式的錯誤提示;如不能獲取,則取日志內(nèi)容的第一行內(nèi)容(以換行符為界),并去除其中可能存在的Case相關(guān)的提示信息
- 提取“機房和環(huán)境”這兩個指標比較簡單,在此不做贅述。
(2) 聚類算法
算法的執(zhí)行,我們以圖4來表示。
(3) min_size 選擇
考慮到日志數(shù)據(jù)中可能包含種類極多,且根據(jù)小規(guī)模數(shù)據(jù)實驗表明,min_size = 1?5 * 報警日志數(shù)量時,算法已經(jīng)有較好的表現(xiàn),再高會增加過度聚合的風險,因此我們?nèi)in_size = 1?5 * 報警日志數(shù)量,?參考論文中的實驗,取0.05。
(4) 聚類停止條件
考慮到部分場景下,報警日志可能較少,因此min_size的值也較少,此時聚類已無太大意義,因此設(shè)定聚類停止條件為:聚類結(jié)果的報警摘要數(shù)量小于等于20或已經(jīng)存在某個類別的count值達到min_size的閾值,即停止聚類。
3. 泛化層次結(jié)構(gòu)
泛化層次結(jié)構(gòu),用于記錄屬性的泛化關(guān)系,是泛化時向上抽象的依據(jù),需要預先定義。
根據(jù)實驗所用項目的實際使用環(huán)境,我們定義的泛化層次結(jié)構(gòu)如下:
“故障所在位置”此屬性無需泛化層次結(jié)構(gòu),每次泛化時直接按照包路徑向上層截斷,直到系統(tǒng)包名。
實驗
以下三個實驗均使用C端API系統(tǒng)。
1. 單依賴故障
實驗材料來自于線上某業(yè)務(wù)系統(tǒng)真實故障時所產(chǎn)生的大量報警日志。
- 環(huán)境:線上
- 故障原因:產(chǎn)品中心線上單機故障
- 報警日志數(shù)量:939條
部分原始報警日志如圖9所示,初次觀察時,很難理出頭緒。
經(jīng)過聚類后的報警摘要如表1所示:
| 1 | 所有機房 | 產(chǎn)品中心 | Prod | com.*.*.*.CommonProductQueryClient | com.netflix.hystrix.exception.HystrixTimeoutException: commonQueryClient.getProductType execution timeout after waiting for 150ms. | 249 |
| 2 | 所有機房 | 業(yè)務(wù)插件 | Prod | com.*.*.*.PluginRegistry.lambda | java.lang.IllegalArgumentException: 未找到業(yè)務(wù)插件:所有產(chǎn)品類型 | 240 |
| 3 | 所有機房 | 產(chǎn)品中心 | Prod | com.*.*.*.TrProductQueryClient | com.netflix.hystrix.exception.HystrixTimeoutException: TrQueryClient.listTrByDids2C execution timeout after waiting for 1000ms. | 145 |
| 4 | 所有機房 | 對外接口(猜喜/貨架/目的地) | Prod | com.*.*.*.RemoteDealServiceImpl | com.netflix.hystrix.exception.HystrixTimeoutException: ScenicDealList.listDealsByScenic execution timeout after waiting for 300ms. | 89 |
| 5 | 所有機房 | 產(chǎn)品中心 | Prod | com.*.*.*.CommonProductQueryClient | com.netflix.hystrix.exception.HystrixTimeoutException: commonQueryClient.listTrByDids2C execution timeout after waiting for 1000ms. | 29 |
| 6 | 所有機房 | 產(chǎn)品中心 | Prod | com.*.*.*.ActivityQueryClientImpl | com.netflix.hystrix.exception.HystrixTimeoutException: commonQueryClient.getBusinessLicense execution timeout after waiting for 100ms. | 21 |
| 7 | 所有機房 | 產(chǎn)品中心 | prod | com.*.*.*.CommonProductQueryClient | com.netflix.hystrix.exception.HystrixTimeoutException: commonQueryClient.getBusinessLicense execution timeout after waiting for 100ms. | 21 |
| 8 | 所有機房 | 對外接口(猜喜/貨架/目的地) | Prod | com.*.*.*.RemoteDealServiceImpl | com.netflix.hystrix.exception.HystrixTimeoutException: HotelDealList.hotelShelf execution timeout after waiting for 500ms. | 17 |
| 9 | 所有機房 | 產(chǎn)品中心 | Prod | com.*.*.*.TrProductQueryClient | Caused by: java.lang.InterruptedException | 16 |
| 10 | 所有機房 | 產(chǎn)品中心 | Prod | com.*.*.*.TrProductQueryClient | Caused by: java.lang.InterruptedException | 13 |
我們可以看到前三條報警摘要的Count遠超其他報警摘要,并且它們指明了故障主要發(fā)生在產(chǎn)品中心的接口。
2. 無相關(guān)的多依賴同時故障
實驗材料為利用故障注入工具,在Staging環(huán)境模擬運營置頂服務(wù)和A/B測試服務(wù)同時產(chǎn)生故障的場景。
- 環(huán)境:Staging(使用線上錄制流量和壓測平臺模擬線上正常流量環(huán)境)
- 模擬故障原因:置頂與A/B測試接口大量超時
- 報警日志數(shù)量:527條
部分原始報警日志如圖10所示:
經(jīng)過聚類后的報警摘要如表2所示:
| 1 | 所有機房 | 運營活動 | Staging | com.*.*.*.ActivityQueryClientImpl | [hystrix]置頂失敗, circuit short is open | 291 |
| 2 | 所有機房 | A/B測試 | Staging | com.*.*.*.AbExperimentClient | [hystrix] tripExperiment error, circuit short is open | 105 |
| 3 | 所有機房 | 緩存 | Staging | com.*.*.*.CacheClientFacade | com.netflix.hystrix.exception.HystrixTimeoutException: c-cache-rpc.common_deal_base.rpc execution timeout after waiting for 1000ms. | 15 |
| 4 | 所有機房 | 產(chǎn)品信息 | Staging | com.*.*.*.queryDealModel | Caused by: com.meituan.service.mobile.mtthrift.netty.exception.RequestTimeoutException: request timeout | 14 |
| 5 | 所有機房 | 產(chǎn)品中心 | Staging | com.*.*.*.CommonProductQueryClient | com.netflix.hystrix.exception.HystrixTimeoutException: commonQueryClient.getBusinessLicense execution timeout after waiting for 100ms. | 9 |
| 6 | 所有機房 | 產(chǎn)品中心 | Staging | com.*.*.*.getOrderForm | java.lang.IllegalArgumentException: 產(chǎn)品無庫存 | 7 |
| 7 | 所有機房 | 彈性工程 | Staging | com.*.*.*.PreSaleChatClient | com.netflix.hystrix.exception.HystrixTimeoutException: CustomerService.PreSaleChat execution timeout after waiting for 50ms. | 7 |
| 8 | 所有機房 | 緩存 | Staging | com.*.*.*.SpringCacheManager | Caused by: java.net.SocketTimeoutException: Read timed out | 7 |
| 9 | 所有機房 | 產(chǎn)品信息 | Staging | com.*.*.*.queryDetailUrlVO | java.lang.IllegalArgumentException: 未知的產(chǎn)品類型 | 2 |
| 10 | 所有機房 | 產(chǎn)品信息 | Staging | com.*.*.*.queryDetailUrlVO | java.lang.IllegalArgumentException: 無法獲取鏈接地址 | 1 |
從上表可以看到,前兩條報警摘要符合本次試驗的預期,定位到了故障發(fā)生的原因。說明在多故障的情況下,算法也有較好的效果。
3. 中間件與相關(guān)依賴同時故障
實驗材料為利用故障注入工具,在Staging環(huán)境模擬產(chǎn)品中心服務(wù)和緩存服務(wù)同時產(chǎn)生超時故障的場景。
- 環(huán)境:Staging(使用線上錄制流量和壓測平臺模擬線上正常流量環(huán)境)
- 模擬故障原因:產(chǎn)品中心所有接口超時,所有緩存服務(wù)超時
- 報警日志數(shù)量:2165
部分原始報警日志如圖11所示:
經(jīng)過聚類后的報警摘要如表3所示:
| 1 | 所有機房 | Squirrel | Staging | com.*.*.*.cache | Timeout | 491 |
| 2 | 所有機房 | Cellar | Staging | com.*.*.*.cache | Timeout | 285 |
| 3 | 所有機房 | Squirrel | Staging | com.*.*.*.TdcServiceImpl | Other Exception | 149 |
| 4 | 所有機房 | 評論 | Staging | com.*.*.*.cache | Timeout | 147 |
| 5 | 所有機房 | Cellar | Staging | com.*.*.*.TdcServiceImpl | Other Exception | 143 |
| 6 | 所有機房 | Squirrel | Staging | com.*.*.*.PoiManagerImpl | 熔斷 | 112 |
| 7 | 所有機房 | 產(chǎn)品中心 | Staging | com.*.*.*.CommonProductQueryClient | Other Exception | 89 |
| 8 | 所有機房 | 評論 | Staging | com.*.*.*.TrDealProcessor | Other Exception | 83 |
| 9 | 所有機房 | 評論 | Staging | com.*.*.*.poi.PoiInfoImpl | Other Exception | 82 |
| 10 | 所有機房 | 產(chǎn)品中心 | Staging | com.*.*.*.client | Timeout | 74 |
從上表可以看到,緩存(Squirrel和Cellar雙緩存)超時最多,產(chǎn)品中心的超時相對較少,這是因為我們系統(tǒng)針對產(chǎn)品中心的部分接口做了兜底處理,當超時發(fā)生時后先查緩存,如果緩存查不到會穿透調(diào)用一個離線信息緩存系統(tǒng),因此產(chǎn)品中心超時總體較少。
綜合上述三個實驗得出結(jié)果,算法對于報警日志的泛化是具有一定效果。在所進行實驗的三個場景中,均能夠定位到關(guān)鍵問題。但是依然存在一些不足,報警摘要中,有的經(jīng)過泛化的信息過于籠統(tǒng)(比如Other Exception)。
經(jīng)過分析,我們發(fā)現(xiàn)主要的原因有:其一,對于錯誤信息中關(guān)鍵字段的提取,在一定程度上決定了向上泛化的準確度。其二,系統(tǒng)本身日志設(shè)計存在一定的局限性。
同時,在利用這個泛化后的報警摘要進行分析時,需要使用者具備相應(yīng)領(lǐng)域的知識。
未來規(guī)劃
本文所關(guān)注的工作,主要在于驗證聚類算法效果,還有一些方向可以繼續(xù)完善和優(yōu)化:
參考資料
作者簡介
- 劉玚,美團點評后端工程師。2017 年加入美團點評,負責美團點評境內(nèi)度假的業(yè)務(wù)開發(fā)。
- 千釗,美團點評后端工程師。2017 年加入美團點評,負責美團點評境內(nèi)度假的業(yè)務(wù)開發(fā)。
總結(jié)
以上是生活随笔為你收集整理的根因分析初探:一种报警聚类算法在业务系统的落地实施的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Logan:美团点评的开源移动端基础日志
- 下一篇: 美团深度学习系统的工程实践