前端異常監(jiān)控解決方案研究
前端監(jiān)控包括行為監(jiān)控、 異常監(jiān)控 、性能監(jiān)控等,本文主要討論異常監(jiān)控。對于前端而言,和后端處于同一個監(jiān)控系統(tǒng)中,前端有自己的監(jiān)控方案,后端也有自己等監(jiān)控方案,但兩者并不分離,因為一個用戶在操作應(yīng)用過程中如果出現(xiàn)異常,有可能是前端引起,也有可能是后端引起,需要有一個機制,將前后端串聯(lián)起來,使監(jiān)控本身統(tǒng)一于監(jiān)控系統(tǒng)。因此,即使只討論前端異常監(jiān)控,其實也不能嚴(yán)格區(qū)分前后端界限,而要根據(jù)實際系統(tǒng)的設(shè)計,在最終的報表中體現(xiàn)出監(jiān)控對開發(fā)和業(yè)務(wù)的幫助。
一般而言,一個監(jiān)控系統(tǒng),大致可以分為四個階段:日志采集、日志存儲、統(tǒng)計與分析、報告和警告。
采集階段:收集異常日志,先在本地做一定的處理,采取一定的方案上報到服務(wù)器。
存儲階段:后端接收前端上報的異常日志,經(jīng)過一定處理,按照一定的存儲方案存儲。
分析階段:分為機器自動分析和人工分析。機器自動分析,通過預(yù)設(shè)的條件和算法,對存儲的日志信息進行統(tǒng)計和篩選,發(fā)現(xiàn)問題,觸發(fā)報警。人工分析,通過提供一個可視化的數(shù)據(jù)面板,讓系統(tǒng)用戶可以看到具體的日志數(shù)據(jù),根據(jù)信息,發(fā)現(xiàn)異常問題根源。
報警階段:分為告警和預(yù)警。告警按照一定的級別自動報警,通過設(shè)定的渠道,按照一定的觸發(fā)規(guī)則進行。預(yù)警則在異常發(fā)生前,提前預(yù)判,給出警告。
1 前端異常
前端異常是指在用戶使用Web應(yīng)用時無法快速得到符合預(yù)期結(jié)果的情況,不同的異常帶來的后果程度不同,輕則引起用戶使用不悅,重則導(dǎo)致產(chǎn)品無法使用,使用戶喪失對產(chǎn)品的認(rèn)可。
1.1 前端異常分類
根據(jù)異常代碼的后果的程度,對前端異常的表現(xiàn)分為如下幾類
a. 出錯
界面呈現(xiàn)的內(nèi)容與用戶預(yù)期的內(nèi)容不符,例如點擊進入非目標(biāo)界面,數(shù)據(jù)不準(zhǔn)確,出現(xiàn)的錯誤提示不可理解,界面錯位,提交后跳轉(zhuǎn)到錯誤界面等情況。這類異常出現(xiàn)時,雖然產(chǎn)品本身功能還能正常使用,但用戶無法達(dá)成自己目標(biāo)。
b. 呆滯
界面出現(xiàn)操作后沒有反應(yīng)的現(xiàn)象,例如點擊按鈕無法提交,提示成功后無法繼續(xù)操作。這類異常出現(xiàn)時,產(chǎn)品已經(jīng)存在界面級局部不可用現(xiàn)象。
c. 損壞
界面出現(xiàn)無法實現(xiàn)操作目的的現(xiàn)象,例如點擊無法進入目標(biāo)界面,點擊無法查看詳情內(nèi)容等。這類異常出現(xiàn)時,應(yīng)用部分功能無法被正常使用。
d. 假死
界面出現(xiàn)卡頓,無法對任何功能進行使用的現(xiàn)象。例如用戶無法登陸導(dǎo)致無法使用應(yīng)用內(nèi)功能,由于某個遮罩層阻擋且不可關(guān)閉導(dǎo)致無法進行任何后續(xù)操作。這類異常出現(xiàn)時,用戶很可能殺死應(yīng)用。
e. 崩潰
應(yīng)用出現(xiàn)經(jīng)常性自動退出或無法操作的現(xiàn)象。例如間歇性crash,網(wǎng)頁無法正常加載或加載后無法進行任何操作。這類異常持續(xù)出現(xiàn),將直接導(dǎo)致用戶流失,影響產(chǎn)品生命力。
1.2 異常錯誤原因分類
前端產(chǎn)生異常的原因主要分5類:
原因 | 案例 | 頻率 |
邏輯錯誤 | 1) 業(yè)務(wù)邏輯判斷條件錯誤 2) 事件綁定順序錯誤 3) 調(diào)用棧時序錯誤 4) 錯誤的操作js對象 |
經(jīng)常 |
數(shù)據(jù)類型錯誤 | 1) 將null視作對象讀取property 2) 將undefined視作數(shù)組進行遍歷 3) 將字符串形式的數(shù)字直接用于加運算 4) 函數(shù)參數(shù)未傳 |
經(jīng)常 |
語法句法錯誤 | 較少 | |
網(wǎng)絡(luò)錯誤 | 1) 慢 2) 服務(wù)端未返回數(shù)據(jù)但仍200,前端按正常進行數(shù)據(jù)遍歷 3) 提交數(shù)據(jù)時網(wǎng)絡(luò)中斷 4) 服務(wù)端500錯誤時前端未做任何錯誤處理 |
偶爾 |
系統(tǒng)錯誤 | 1) 內(nèi)存不夠用 2) 磁盤塞滿 3) 殼不支持API 4) 不兼容 |
較少 |
2 異常采集
2.1 采集內(nèi)容
當(dāng)異常出現(xiàn)的時候,我們需要知道異常的具體信息,根據(jù)異常的具體信息來決定采用什么樣的解決方案。在采集異常信息時,可以遵循4W原則:
WHO did WHAT and get WHICH exception in WHICH environment ?
a. 用戶信息
出現(xiàn)異常時該用戶的信息,例如該用戶在當(dāng)前時刻的狀態(tài)、權(quán)限等,以及需要區(qū)分用戶可多終端登錄時,異常對應(yīng)的是哪一個終端。
b. 行為信息
用戶進行什么操作時產(chǎn)生了異常:所在的界面路徑;執(zhí)行了什么操作;操作時使用了哪些數(shù)據(jù);當(dāng)時的API吐了什么數(shù)據(jù)給客戶端;如果是提交操作,提交了什么數(shù)據(jù);上一個路徑;上一個行為日志記錄ID等。
c. 異常信息
產(chǎn)生異常的代碼信息:用戶操作的DOM元素節(jié)點;異常級別;異常類型;異常描述;代碼stack信息等。
d. 環(huán)境信息
網(wǎng)絡(luò)環(huán)境;設(shè)備型號和標(biāo)識碼;操作系統(tǒng)版本;客戶端版本;API接口版本等。
字段 | 類型 | 解釋 |
requestId | String | 一個界面產(chǎn)生一個requestId |
traceId | String | 一個階段產(chǎn)生一個traceId,用于追蹤和一個異常相關(guān)的所有日志記錄 |
hash | String | 這條log的唯一標(biāo)識碼,相當(dāng)于logId,但它是根據(jù)當(dāng)前日志記錄的具體內(nèi)容而生成的 |
time | Number | 當(dāng)前日志產(chǎn)生的時間(保存時刻) |
userId | String | |
userStatus | Number | 當(dāng)時,用戶狀態(tài)信息(是否可用/禁用) |
userRoles | Array | 當(dāng)時,前用戶的角色列表 |
userGroups | Array | 當(dāng)時,用戶當(dāng)前所在組,組別權(quán)限可能影響結(jié)果 |
userLicenses | Array | 當(dāng)時,許可證,可能過期 |
path | String | 所在路徑,URL |
action | String | 進行了什么操作 |
referer | String | 上一個路徑,來源URL |
prevAction | String | 上一個操作 |
data | Object | 當(dāng)前界面的state、data |
dataSources | Array<Object> | 上游api給了什么數(shù)據(jù) |
dataSend | Object | 提交了什么數(shù)據(jù) |
targetElement | HTMLElement | 用戶操作的DOM元素 |
targetDOMPath | Array<HTMLElement> | 該DOM元素的節(jié)點路徑 |
targetCSS | Object | 該元素的自定義樣式表 |
targetAttrs | Object | 該元素當(dāng)前的屬性及值 |
errorType | String | 錯誤類型 |
errorLevel | String | 異常級別 |
errorStack | String | 錯誤stack信息 |
errorFilename | String | 出錯文件 |
errorLineNo | Number | 出錯行 |
errorColNo | Number | 出錯列位置 |
errorMessage | String | 錯誤描述(開發(fā)者定義) |
errorTimeStamp | Number | 時間戳 |
eventType | String | 事件類型 |
pageX | Number | 事件x軸坐標(biāo) |
pageY | Number | 事件y軸坐標(biāo) |
screenX | Number | 事件x軸坐標(biāo) |
screenY | Number | 事件y軸坐標(biāo) |
pageW | Number | 頁面寬度 |
pageH | Number | 頁面高度 |
screenW | Number | 屏幕寬度 |
screenH | Number | 屏幕高度 |
eventKey | String | 觸發(fā)事件的鍵 |
network | String | 網(wǎng)絡(luò)環(huán)境描述 |
userAgent | String | 客戶端描述 |
device | String | 設(shè)備描述 |
system | String | 操作系統(tǒng)描述 |
appVersion | String | 應(yīng)用版本 |
apiVersion | String | 接口版本 |
這是一份非常龐大的日志字段表,它幾乎囊括了一個異常發(fā)生時,能夠?qū)Ξ惓V茉猸h(huán)境進行詳細(xì)描述的所有信息。不同情況下,這些字段并不一定都會收集,由于我們會采用文檔數(shù)據(jù)庫存儲日志,因此,并不影響它的實際存儲結(jié)果。
2.2 異常捕獲
前端捕獲異常分為全局捕獲和單點捕獲。全局捕獲代碼集中,易于管理;單點捕獲作為補充,對某些特殊情況進行捕獲,但分散,不利于管理。
a、全局捕獲
通過全局的接口,將捕獲代碼集中寫在一個地方,可以利用的接口有:
- window.addEventListener(‘error’) / window.addEventListener(“unhandledrejection”) / document.addEventListener(‘click’) 等
- 框架級別的全局監(jiān)聽,例如aixos中使用interceptor進行攔截,vue、react都有自己的錯誤采集接口
- 通過對全局函數(shù)進行封裝包裹,實現(xiàn)在在調(diào)用該函數(shù)時自動捕獲異常
- 對實例方法重寫(Patch),在原有功能基礎(chǔ)上包裹一層,例如對console.error進行重寫,在使用方法不變的情況下也可以異常捕獲
b、單點捕獲
在業(yè)務(wù)代碼中對單個代碼塊進行包裹,或在邏輯流程中打點,實現(xiàn)有針對性的異常捕獲:
- try…catch
- 專門寫一個函數(shù)來收集異常信息,在異常發(fā)生時,調(diào)用該函數(shù)
- 專門寫一個函數(shù)來包裹其他函數(shù),得到一個新函數(shù),該新函數(shù)運行結(jié)果和原函數(shù)一模一樣,只是在發(fā)生異常時可以捕獲異常
2.3 跨域腳本異常
由于瀏覽器安全策略限制,跨域腳本報錯時,無法直接獲取錯誤的詳細(xì)信息,只能得到一個Script Error。例如,我們會引入第三方依賴,或者將自己的腳本放在CDN時。
解決Script Error的方法:
方案一:
- 將js內(nèi)聯(lián)到HTML中
- 將js文件與HTML放在同域下
方案二:
- 為頁面上script標(biāo)簽添加crossorigin屬性
- 被引入腳本所在服務(wù)端響應(yīng)頭中,增加 Access-Control-Allow-Origin 來支持跨域資源共享
2.4 異常錄制
對于一個異常,僅僅擁有該異常的信息還不足以完全抓住問題的本質(zhì),因為異常發(fā)生的位置,并不一定是異常根源所在的位置。我們需要對異?,F(xiàn)場進行還原,才能復(fù)原問題全貌,甚至避免類似問題在其他界面中發(fā)生。這里需要引進一個概念,就是“異常錄制”。錄制通過“時間”“空間”兩個維度記錄異常發(fā)生前到發(fā)生的整個過程,對于找到異常根源更有幫助。
上圖表示,當(dāng)異常發(fā)生時,異常的根源可能離我們很遠(yuǎn),我們需要回到異常發(fā)生的現(xiàn)場,找到異常根源。就像現(xiàn)實生活中破案一樣,如果有監(jiān)控攝影機對案發(fā)過程的錄影,對破案來說更加容易。如果僅僅關(guān)注異常本身,要找到異常的根源,需要憑借運氣,但有了異常錄制的幫助,找到根源就更加容易。
所謂的“異常錄制”,實際上就是通過技術(shù)手段,收集用戶的操作過程,對用戶的每一個操作都進行記錄,在發(fā)生異常時,把一定時間區(qū)間內(nèi)的記錄重新運行,形成影像進行播放,讓調(diào)試者無需向用戶詢問,就能看到用戶當(dāng)時的操作過程。
上圖是來自阿里的一套異常錄制還原方案示意圖,用戶在界面上的操作產(chǎn)生的events和mutation被產(chǎn)品收集起來,上傳到服務(wù)器,經(jīng)過隊列處理按順序存放到數(shù)據(jù)庫中。當(dāng)需要進行異常重現(xiàn)的時候,將這些記錄從數(shù)據(jù)庫中取出,采用一定的技術(shù)方案,順序播放這些記錄,即可實現(xiàn)異常還原。
2.5 異常級別
一般而言,我們會將收集信息的級別分為info,warn,error等,并在此基礎(chǔ)上進行擴展。
當(dāng)我們監(jiān)控到異常發(fā)生時,可以將該異常劃分到“重要——緊急”模型中分為A、B、C、D四個等級。有些異常,雖然發(fā)生了,但是并不影響用戶的正常使用,用戶其實并沒有感知到,雖然理論上應(yīng)該修復(fù),但是實際上相對于其他異常而言,可以放在后面進行處理。
下文會討論告警策略,一般而言,越靠近右上角的異常會越快通知,保證相關(guān)人員能最快接收到信息,并進行處理。A級異常需要快速響應(yīng),甚至需要相關(guān)負(fù)責(zé)人知悉。
在收集異常階段,可根據(jù)第一節(jié)劃分的異常后果來判斷異常的嚴(yán)重程度,在發(fā)生異常時選擇對應(yīng)的上報方案進行上報。
3 整理與上報方案
前文已經(jīng)提到,除了異常報錯信息本身,我們還需要記錄用戶操作日志,以實現(xiàn)場景復(fù)原。這就涉及到上報的量和頻率問題。如果任何日志都立即上報,這無異于自造的DDOS攻擊。因此,我們需要合理的上報方案。下文會介紹4種上報方案,但實際我們不會僅限于其中一種,而是經(jīng)常同時使用,對不同級別的日志選擇不同的上報方案。
3.1 前端存儲日志
我們前面提到,我們并不單單采集異常本身日志,而且還會采集與異常相關(guān)的用戶行為日志。單純一條異常日志并不能幫助我們快速定位問題根源,找到解決方案。但如果要收集用戶的行為日志,又要采取一定的技巧,而不能用戶每一個操作后,就立即將該行為日志傳到服務(wù)器,對于具有大量用戶同時在線的應(yīng)用,如果用戶一操作就立即上傳日志,無異于對日志服務(wù)器進行DDOS攻擊。因此,我們先將這些日志存儲在用戶客戶端本地,達(dá)到一定條件之后,再同時打包上傳一組日志。
那么,如何進行前端日志存儲呢?我們不可能直接將這些日志用一個變量保存起來,這樣會擠爆內(nèi)存,而且一旦用戶進行刷新操作,這些日志就丟失了,因此,我們自然而然想到前端數(shù)據(jù)持久化方案。
目前,可用的持久化方案可選項也比較多了,主要有:Cookie、localStorage、sessionStorage、IndexedDB、webSQL 、FileSystem 等等。那么該如何選擇呢?我們通過一個表來進行對比:
存儲方式 | cookie | localStorage | sessionStorage | IndexedDB | webSQL | FileSystem |
類型 | key-value | key-value | NoSQL | SQL | ||
數(shù)據(jù)格式 | string | string | string | object | ||
容量 | 4k | 5M | 5M | 500M | 60M | |
進程 | 同步 | 同步 | 同步 | 異步 | 異步 | |
檢索 | key | key | key, index | field | ||
性能 | 讀快寫慢 | 讀慢寫快 |
綜合之后,IndexedDB是最好的選擇,它具有容量大、異步的優(yōu)勢,異步的特性保證它不會對界面的渲染產(chǎn)生阻塞。而且IndexedDB是分庫的,每個庫又分store,還能按照索引進行查詢,具有完整的數(shù)據(jù)庫管理思維,比localStorage更適合做結(jié)構(gòu)化數(shù)據(jù)管理。但是它有一個缺點,就是api非常復(fù)雜,不像localStorage那么簡單直接。針對這一點,我們可以使用hello-indexeddb這個工具,它用Promise對復(fù)雜api進行來封裝,簡化操作,使IndexedDB的使用也能做到localStorage一樣便捷。另外,IndexedDB是被廣泛支持的HTML5標(biāo)準(zhǔn),兼容大部分瀏覽器,因此不用擔(dān)心它的發(fā)展前景。
接下來,我們究竟應(yīng)該怎么合理使用IndexedDB,保證我們前端存儲的合理性呢?
上圖展示了前端存儲日志的流程和數(shù)據(jù)庫布局。當(dāng)一個事件、變動、異常被捕獲之后,形成一條初始日志,被立即放入暫存區(qū)(indexedDB的一個store),之后主程序就結(jié)束了收集過程,后續(xù)的事只在webworker中發(fā)生。在一個webworker中,一個循環(huán)任務(wù)不斷從暫存區(qū)中取出日志,對日志進行分類,將分類結(jié)果存儲到索引區(qū)中,并對日志記錄的信息進行豐富,將最終將會上報到服務(wù)端的日志記錄轉(zhuǎn)存到歸檔區(qū)。而當(dāng)一條日志在歸檔區(qū)中存在的時間超過一定天數(shù)之后,它就已經(jīng)沒有價值了,但是為了防止特殊情況,它被轉(zhuǎn)存到回收區(qū),再經(jīng)歷一段時間后,就會被從回收區(qū)中清除。
3.2 前端整理日志
上文講到,在一個webworker中對日志進行整理后存到索引區(qū)和歸檔區(qū),那么這個整理過程是怎樣的呢?
由于我們下文要講的上報,是按照索引進行的,因此,我們在前端的日志整理工作,主要就是根據(jù)日志特征,整理出不同的索引。我們在收集日志時,會給每一條日志打上一個type,以此進行分類,并創(chuàng)建索引,同時通過object-hashcode計算每個log對象的hash值,作為這個log的唯一標(biāo)志。
- 將所有日志記錄按時序存放在歸檔區(qū),并將新入庫的日志加入索引
- BatchIndexes:批量上報索引(包含性能等其他日志),可一次批量上報100條
- MomentIndexes:即時上報索引,一次全部上報
- FeedbackIndexes:用戶反饋索引,一次上報一條
- BlockIndexes:區(qū)塊上報索引,按異常/錯誤(traceId,requestId)分塊,一次上報一塊
- 上報完成后,被上報過的日志對應(yīng)的索引刪除
- 3天以上日志進入回收區(qū)
- 7天以上的日志從回收區(qū)清除
rquestId:同時追蹤前后端日志。由于后端也會記錄自己的日志,因此,在前端請求api的時候,默認(rèn)帶上requestId,后端記錄的日志就可以和前端日志對應(yīng)起來。
traceId:追蹤一個異常發(fā)生前后的相關(guān)日志。當(dāng)應(yīng)用啟動時,創(chuàng)建一個traceId,直到一個異常發(fā)生時,刷新traceId。把一個traceId相關(guān)的requestId收集起來,把這些requestId相關(guān)的日志組合起來,就是最終這個異常相關(guān)的所有日志,用來對異常進行復(fù)盤。
上圖舉例展示了如何利用traceId和requestId找出和一個異常相關(guān)的所有日志。在上圖中,hash4是一條異常日志,我們找到hash4對應(yīng)的traceId為traceId2,在日志列表中,有兩條記錄具有該traceId,但是hash3這條記錄并不是一個動作的開始,因為hash3對應(yīng)的requestId為reqId2,而reqId2開始于hash2,因此,我們實際上要把hash2也加入到該異常發(fā)生的整個復(fù)盤備選記錄中??偨Y(jié)起來就是,我們要找出同一個traceId對應(yīng)的所有requestId對應(yīng)的日志記錄,雖然有點繞,但稍理解就可以明白其中的道理。
我們把這些和一個異常相關(guān)的所有日志集合起來,稱為一個block,再利用日志的hash集合,得出這個block的hash,并在索引區(qū)中建立索引,等待上報。
3.3 上報日志
上報日志也在webworker中進行,為了和整理區(qū)分,可以分兩個worker。上報的流程大致為:在每一個循環(huán)中,從索引區(qū)取出對應(yīng)條數(shù)的索引,通過索引中的hash,到歸檔區(qū)取出完整的日志記錄,再上傳到服務(wù)器。
按照上報的頻率(重要緊急度)可將上報分為四種:
a. 即時上報
收集到日志后,立即觸發(fā)上報函數(shù)。僅用于A類異常。而且由于受到網(wǎng)絡(luò)不確定因素影響,A類日志上報需要有一個確認(rèn)機制,只有確認(rèn)服務(wù)端已經(jīng)成功接收到該上報信息之后,才算完成。否則需要有一個循環(huán)機制,確保上報成功。
b. 批量上報
將收集到的日志存儲在本地,當(dāng)收集到一定數(shù)量之后再打包一次性上報,或者按照一定的頻率(時間間隔)打包上傳。這相當(dāng)于把多次合并為一次上報,以降低對服務(wù)器的壓力。
c. 區(qū)塊上報
將一次異常的場景打包為一個區(qū)塊后進行上報。它和批量上報不同,批量上報保證了日志的完整性,全面性,但會有無用信息。而區(qū)塊上報則是針對異常本身的,確保單個異常相關(guān)的日志被全部上報。
d. 用戶主動提交
在界面上提供一個按鈕,用戶主動反饋bug。這有利于加強與用戶的互動。
或者當(dāng)異常發(fā)生時,雖然對用戶沒有任何影響,但是應(yīng)用監(jiān)控到了,彈出一個提示框,讓用戶選擇是否愿意上傳日志。這種方案適合涉及用戶隱私數(shù)據(jù)時。
即時上報 | 批量上報 | 區(qū)塊上報 | 用戶反饋 | |
時效 | 立即 | 定時 | 稍延時 | 延時 |
條數(shù) | 一次全部上報 | 一次100條 | 單次上報相關(guān)條目 | 一次1條 |
容量 | 小 | 中 | – | – |
緊急 | 緊急重要 | 不緊急 | 不緊急但重要 | 不緊急 |
即時上報雖然叫即時,但是其實也是通過類似隊列的循環(huán)任務(wù)去完成的,它主要是盡快把一些重要的異常提交給監(jiān)控系統(tǒng),好讓運維人員發(fā)現(xiàn)問題,因此,它對應(yīng)的緊急程度比較高。
批量上報和區(qū)塊上報的區(qū)別:批量上報是一次上報一定條數(shù),比如每2分鐘上報1000條,直到上報完成。而區(qū)塊上報是在異常發(fā)生之后,馬上收集和異常相關(guān)的所有日志,查詢出哪些日志已經(jīng)由批量上報上報過了,剔除掉,把其他相關(guān)日志上傳,和異常相關(guān)的這些日志相對而言更重要一些,它們可以幫助盡快復(fù)原異常現(xiàn)場,找出發(fā)生異常的根源。
用戶提交的反饋信息,則可以慢悠悠上報上去。
為了確保上報是成功的,在上報時需要有一個確認(rèn)機制,由于在服務(wù)端接收到上報日志之后,并不會立即存入數(shù)據(jù)庫,而是放到一個隊列中,因此,前后端在確保日志確實已經(jīng)記錄進數(shù)據(jù)庫這一點上需要再做一些處理。
上圖展示了上報的一個大致流程,在上報時,先通過hash查詢,讓客戶端知道準(zhǔn)備要上報的日志集合中,是否存在已經(jīng)被服務(wù)端保存好的日志,如果已經(jīng)存在,就將這些日志去除,避免重復(fù)上報,浪費流量。
3.4 壓縮上報數(shù)據(jù)
一次性上傳批量數(shù)據(jù)時,必然遇到數(shù)據(jù)量大,浪費流量,或者傳輸慢等情況,網(wǎng)絡(luò)不好的狀態(tài)下,可能導(dǎo)致上報失敗。因此,在上報之前進行數(shù)據(jù)壓縮也是一種方案。
對于合并上報這種情況,一次的數(shù)據(jù)量可能要十幾k,對于日 pv 大的站點來說,產(chǎn)生的流量還是很可觀的。所以有必要對數(shù)據(jù)進行壓縮上報。lz-string是一個非常優(yōu)秀的字符串壓縮類庫,兼容性好,代碼量少,壓縮比高,壓縮時間短,壓縮率達(dá)到驚人的60%。但它基于LZ78壓縮,如果后端不支持解壓,可選擇gzip壓縮,一般而言后端會默認(rèn)預(yù)裝gzip,因此,選擇gzip壓縮數(shù)據(jù)也可以,工具包pako中自帶了gzip壓縮,可以嘗試使用。
4 日志接收與存儲
4.1 接入層與消息隊列
一般通過提供獨立的日志服務(wù)器接收客戶端日志,接收過程中,要對客戶端日志內(nèi)容的合法性、安全性等進行甄別,防止被人攻擊。而且由于日志提交一般都比較頻繁,多客戶端同時并發(fā)的情況也常見。通過消息隊列將日志信息逐一處理后寫入到數(shù)據(jù)庫進行保存也是比較常用的方案。
上圖為騰訊BetterJS的架構(gòu)圖,其中“接入層”和“推送中心”就是這里提到的接入層和消息隊列。BetterJS將整個前端監(jiān)控的各個模塊進行拆分,推送中心承擔(dān)了將日志推送到存儲中心進行存儲和推送給其他系統(tǒng)(例如告警系統(tǒng))的角色,但我們可以把接收日志階段的隊列獨立出來看,在接入層和存儲層之間做一個過渡。
4.2 日志存儲系統(tǒng)
存儲日志是一個臟活累活,但是不得不做。對于小應(yīng)用,單庫單表加優(yōu)化就可以應(yīng)付。一個成規(guī)模的應(yīng)用,如果要提供更標(biāo)準(zhǔn)高效的日志監(jiān)控服務(wù),常常需要在日志存儲架構(gòu)上下一些功夫。目前業(yè)界已經(jīng)有比較完備的日志存儲方案,主要有:Hbase系,Dremel系,Lucene系等。總體而言,日志存儲系統(tǒng)主要面對的問題是數(shù)據(jù)量大,數(shù)據(jù)結(jié)構(gòu)不規(guī)律,寫入并發(fā)高,查詢需求大等。一般一套日志存儲系統(tǒng),要解決上面這些問題,就要解決寫入的緩沖,存儲介質(zhì)按日志時間選擇,為方便快速讀取而設(shè)計合理的索引系統(tǒng)等等。
由于日志存儲系統(tǒng)方案比較成熟,這里就不再做更多討論。
4.3 搜索
日志的最終目的是要使用,由于一般日志的體量都非常大,因此,要在龐大的數(shù)據(jù)中找到需要的日志記錄,需要依賴比較好的搜索引擎。Splunk是一套成熟的日志存儲系統(tǒng),但它是付費使用的。按照Splunk的框架,Elk是Splunk的開源實現(xiàn),Elk是ElasticSearch、Logstash、Kibana的結(jié)合,ES基于Lucene的存儲、索引的搜索引擎;logstash是提供輸入輸出及轉(zhuǎn)化處理插件的日志標(biāo)準(zhǔn)化管道;Kibana提供可視化和查詢統(tǒng)計的用戶界面。
5 日志統(tǒng)計與分析
一個完善的日志統(tǒng)計分析工具需要提供各方面方便的面板,以可視化的方式給日志管理員和開發(fā)者反饋信息。
5.1 用戶緯度
同一個用戶的不同請求實際上會形成不同的story線,因此,針對用戶的一系列操作設(shè)計唯一的request id是有必要的。同一個用戶在不同終端進行操作時,也能進行區(qū)分。用戶在進行某個操作時的狀態(tài)、權(quán)限等信息,也需要在日志系統(tǒng)中予以反應(yīng)。
5.2 時間維度
一個異常是怎么發(fā)生的,需要將異常操作的前后story線串聯(lián)起來觀察。它不單單涉及一個用戶的一次操作,甚至不限于某一個頁面,而是一連串事件的最終結(jié)果。
5.3 性能維度
應(yīng)用運行過程中的性能情況,例如,界面加載時間,api請求時長統(tǒng)計,單元計算的消耗,用戶呆滯時間。
5.4 運行環(huán)境維度
應(yīng)用及服務(wù)所運行的環(huán)境情況,例如應(yīng)用所在的網(wǎng)絡(luò)環(huán)境,操作系統(tǒng),設(shè)備硬件信息等,服務(wù)器cpu、內(nèi)存狀況,網(wǎng)絡(luò)、寬帶使用情況等。
5.4 細(xì)粒度代碼追蹤
異常的代碼stack信息,定位到發(fā)生異常的代碼位置和異常堆棧。
5.6 場景回溯
通過將異常相關(guān)的用戶日志連接起來,以動態(tài)的效果輸出發(fā)生異常的過程。
6 監(jiān)控與通知
對異常進行統(tǒng)計和分析只是基礎(chǔ),而在發(fā)現(xiàn)異常時可以推送和告警,甚至做到自動處理,才是一個異常監(jiān)控系統(tǒng)應(yīng)該具備的能力。
6.1 自定義觸發(fā)條件的告警
a. 監(jiān)控實現(xiàn)
當(dāng)日志信息進入接入層時,就可以觸發(fā)監(jiān)控邏輯。當(dāng)日志信息中存在較為高級別的異常時,也可以立即出發(fā)告警。告警消息隊列和日志入庫隊列可以分開來管理,實現(xiàn)并行。
對入庫日志信息進行統(tǒng)計,對異常信息進行告警。對監(jiān)控異常進行響應(yīng)。所謂監(jiān)控異常,是指:有規(guī)律的異常一般而言都比較讓人放心,比較麻煩的是突然之間的異常。例如在某一時段突然頻繁接收到D級異常,雖然D級異常是不緊急一般重要,但是當(dāng)監(jiān)控本身發(fā)生異常時,就要提高警惕。
b. 自定義觸發(fā)條件
除了系統(tǒng)開發(fā)時配置的默認(rèn)告警條件,還應(yīng)該提供給日志管理員可配置的自定義觸發(fā)條件。
- 日志內(nèi)含有什么內(nèi)容時
- 日志統(tǒng)計達(dá)到什么度、量時
- 向符合什么條件的用戶告警
6.2 推送渠道
可選擇的途徑有很多,例如郵件、短信、微信、電話。
6.3 推送頻率
針對不同級別的告警,推送的頻率也可以進行設(shè)定。低風(fēng)險告警可以以報告的形式一天推送一次,高風(fēng)險告警10分鐘循環(huán)推送,直到處理人手動關(guān)閉告警開關(guān)。
6.4 自動報表
對于日志統(tǒng)計信息的推送,可以做到自動生成日報、周報、月報、年報并郵件發(fā)送給相關(guān)群組。
6.5 自動產(chǎn)生bug工單
當(dāng)異常發(fā)生時,系統(tǒng)可以調(diào)用工單系統(tǒng)API實現(xiàn)自動生成bug單,工單關(guān)閉后反饋給監(jiān)控系統(tǒng),形成對異常處理的追蹤信息進行記錄,在報告中予以展示。
7 修復(fù)異常
7.1 sourcemap
前端代碼大部分情況都是經(jīng)過壓縮后發(fā)布的,上報的stack信息需要還原為源碼信息,才能快速定位源碼進行修改。
發(fā)布時,只部署js腳本到服務(wù)器上,將sourcemap文件上傳到監(jiān)控系統(tǒng),在監(jiān)控系統(tǒng)中展示stack信息時,利用sourcemap文件對stack信息進行解碼,得到源碼中的具體信息。
但是這里有一個問題,就是sourcemap必須和正式環(huán)境的版本對應(yīng),還必須和git中的某個commit節(jié)點對應(yīng),這樣才能保證在查異常的時候可以正確利用stack信息,找到出問題所在版本的代碼。這些可以通過建立CI任務(wù),在集成化部署中增加一個部署流程,以實現(xiàn)這一環(huán)節(jié)。
7.2 從告警到預(yù)警
預(yù)警的本質(zhì)是,預(yù)設(shè)可能出現(xiàn)異常的條件,當(dāng)觸發(fā)該條件時異常并沒有真實發(fā)生,因此,可以趕在異常發(fā)生之前對用戶行為進行檢查,及時修復(fù),避免異?;虍惓U大。
怎么做呢?其實就是一個統(tǒng)計聚類的過程。將歷史中發(fā)生異常的情況進行統(tǒng)計,從時間、地域、用戶等不同維度加以統(tǒng)計,找出規(guī)律,并將這些規(guī)律通過算法自動加入到預(yù)警條件中,當(dāng)下次觸發(fā)時,及時預(yù)警。
7.3 智能修復(fù)
自動修復(fù)錯誤。例如,前端要求接口返回數(shù)值,但接口返回了數(shù)值型的字符串,那么可以有一種機制,監(jiān)控系統(tǒng)發(fā)送正確數(shù)據(jù)類型模型給后端,后端在返回數(shù)據(jù)時,根據(jù)該模型控制每個字段的類型。
8 異常測試
8.1 主動異常測試
撰寫異常用例,在自動化測試系統(tǒng)中,加入異常測試用戶。在測試或運行過程中,每發(fā)現(xiàn)一個異常,就將它加入到原有的異常用例列表中。
8.2 隨機異常測試
模擬真實環(huán)境,在模擬器中模擬真實用戶的隨機操作,利用自動化腳本產(chǎn)生隨機操作動作代碼,并執(zhí)行。
定義異常,例如彈出某個彈出框,包含特定內(nèi)容時,就是異常。將這些測試結(jié)果記錄下來,再聚類統(tǒng)計分析,對防御異常也很有幫助。
9 部署
9.1 多客戶端
一個用戶在不同終端上登錄,或者一個用戶在登錄前和登錄后的狀態(tài)。通過特定算法生成requestID,通過該requestId可以確定某個用戶在獨立客戶端上的一系列操作,根據(jù)日志時序,可以梳理出用戶產(chǎn)生異常的具體路徑。
9.2 集成便捷性
前端寫成包,全局引用即可完成大部分日志記錄、存儲和上報。在特殊邏輯里面,可以調(diào)用特定方法記錄日志。
后端與應(yīng)用本身的業(yè)務(wù)代碼解耦,可以做成獨立的服務(wù),通過接口和第三方應(yīng)用交互。利用集成部署,可以將系統(tǒng)隨時進行擴容、移植等操作。
9.3 管理系統(tǒng)的可擴展
整套系統(tǒng)可擴展,不僅服務(wù)單應(yīng)用,可支持多個應(yīng)用同時運行。同一個團隊下的所有應(yīng)用都可以利用同一個平臺進行管理。
9.4 日志系統(tǒng)權(quán)限
不同的人在訪問日志系統(tǒng)時權(quán)限不同,一個訪問者只能查看自己相關(guān)的應(yīng)用,有些統(tǒng)計數(shù)據(jù)如果比較敏感,可以單獨設(shè)置權(quán)限,敏感數(shù)據(jù)可脫敏。
10 其他
10.1 性能監(jiān)控
異常監(jiān)控主要針對代碼級別的報錯,但也應(yīng)該關(guān)注性能異常。性能監(jiān)控主要包括:
- 運行時性能:文件級、模塊級、函數(shù)級、算法級
- 網(wǎng)絡(luò)請求速率
- 系統(tǒng)性能
10.2 API Monitor
后端API對前端的影響也非常大,雖然前端代碼也控制邏輯,但是后端返回的數(shù)據(jù)是基礎(chǔ),因此對API的監(jiān)控可以分為:
- 穩(wěn)定性監(jiān)控
- 數(shù)據(jù)格式和類型
- 報錯監(jiān)控
- 數(shù)據(jù)準(zhǔn)確性監(jiān)控
10.3 數(shù)據(jù)脫敏
敏感數(shù)據(jù)不被日志系統(tǒng)采集。由于日志系統(tǒng)的保存是比較開放的,雖然里面的數(shù)據(jù)很重要,但是在存儲上大部分日志系統(tǒng)都不是保密級,因此,如果應(yīng)用涉及了敏感數(shù)據(jù),最好做到:
- 獨立部署,不和其他應(yīng)用共享監(jiān)控系統(tǒng)
- 不采集具體數(shù)據(jù),只采集用戶操作數(shù)據(jù),在重現(xiàn)時,通過日志信息可以取出數(shù)據(jù)api結(jié)果來展示
- 日志加密,做到軟硬件層面的加密防護
- 必要時,可采集具體數(shù)據(jù)的ID用于調(diào)試,場景重現(xiàn)時,用mock數(shù)據(jù)替代,mock數(shù)據(jù)可由后端采用假的數(shù)據(jù)源生成
- 對敏感數(shù)據(jù)進行混淆
結(jié)語
本文主要是對前端異常監(jiān)控的整體框架進行了研究,沒有涉及到具體的技術(shù)實現(xiàn),涉及前端部分和后臺部分以及與整個問題相關(guān)的一些知識點,主要關(guān)注前端部分,它和后端的監(jiān)控有重疊部分也有分支部分,需要在一個項目中不斷實踐,總結(jié)出項目本身的監(jiān)控需求和策略。