騰訊三面:說說前端監(jiān)控平臺/監(jiān)控SDK的架構(gòu)設(shè)計和難點亮點?
點擊上方 前端Q,關(guān)注公眾號
回復(fù)加群,加入前端Q技術(shù)交流群
前言
事情是這樣的,上周,我的一位兩年前端經(jīng)驗的發(fā)小,在 騰訊三輪面試 的時候被問了一個問題:說說你們公司前端監(jiān)控項目的架構(gòu)設(shè)計和亮點設(shè)計 ;
而說回我這位發(fā)小,因為做過他們公司監(jiān)控項目的可視化報表界面,所以簡歷上有寫著前端監(jiān)控項目的項目經(jīng)驗;但是不幸的是,他雖然前端基礎(chǔ)相當(dāng)不錯,但并沒有實際參與監(jiān)控SDK的設(shè)計開發(fā)(只負(fù)責(zé)寫監(jiān)控的可視化分析界面),所以被問到這個問題,直接就一個懵了;結(jié)果也很正常,面試沒過;
那么這篇文章,我就來介紹一下對于前端監(jiān)控項目的 整體架構(gòu) 和 可以做的亮點優(yōu)化 ;前文幾篇文章有介紹具體的前端監(jiān)控實現(xiàn),感興趣的小伙伴可以點擊鏈接跳轉(zhuǎn)過去閱讀; 傳送門就在下面。
傳送門
這篇文章的標(biāo)題原擬定是:一文摸清前端監(jiān)控實踐要點(四)架構(gòu)設(shè)計;但是我的發(fā)小面試剛好碰上了這么一個問題,于是我便將標(biāo)題改為了這個。
一文摸清前端監(jiān)控實踐要點(一)性能監(jiān)控[1]
一文摸清前端監(jiān)控實踐要點(二)行為監(jiān)控[2]
一文摸清前端監(jiān)控實踐要點(三)錯誤監(jiān)控[3]
騰訊三面:說說前端監(jiān)控告警分析平臺的架構(gòu)設(shè)計和難點亮點?[4]
整體 架構(gòu)設(shè)計

直接上圖,我們在應(yīng)用層SDK上報的數(shù)據(jù),在接入層經(jīng)過 削峰限流 和 數(shù)據(jù)加工 后,將原始日志存儲于 ES 中,再經(jīng)過 數(shù)據(jù)清洗 、數(shù)據(jù)聚合 后,將 issue(聚合的數(shù)據(jù)) 持久化存儲 于 MySQL ,最后提供 RESTful API 提供給監(jiān)控平臺調(diào)用;
削峰限流是為了避免激增的大數(shù)據(jù)量、惡意用戶訪問等高并發(fā)數(shù)據(jù)導(dǎo)致的服務(wù)崩潰;數(shù)據(jù)加工是為了將IP、運營商、歸屬地等各種二次加工數(shù)據(jù),封裝進(jìn)上報數(shù)據(jù)里;數(shù)據(jù)清洗是為了經(jīng)由白名單、黑名單過濾等的業(yè)務(wù)需要,還有避免已關(guān)閉的應(yīng)用數(shù)據(jù)繼續(xù)入庫;數(shù)據(jù)聚合是為了將相同信息的數(shù)據(jù)進(jìn)行抽象聚合成issue,以便查詢和追蹤;
SDK 架構(gòu)設(shè)計
為支持多平臺、可拓展、可插拔的特點,整體SDK的架構(gòu)設(shè)計是 內(nèi)核+插件 的插件式設(shè)計;每個 SDK 首先繼承于平臺無關(guān)的 Core 層代碼。然后在自身SDK中,初始化內(nèi)核實例和插件;


值得一談的點
下面將主要談?wù)勥@些內(nèi)容:前端監(jiān)控項目除了正常的數(shù)據(jù)采集、數(shù)據(jù)報表分析以外;會碰上哪些難點可以去突破,或者說可以做出哪些亮點的內(nèi)容?
SDK 如何設(shè)計成多平臺支持?
首先我們先來了解一下,在前端監(jiān)控的領(lǐng)域里,我們可能不僅僅只是監(jiān)控一個 web環(huán)境 下的數(shù)據(jù),包括 Nodejs、微信小程序、Electron 等各種其余的環(huán)境都是有監(jiān)控的業(yè)務(wù)需求在的;
那么我們就要思考一個點,我們的一個 SDK 項目,既然功能全,又要支持多平臺,那么怎么設(shè)計這個 SDK 可以讓它既支持多平臺,但是在啟用某個平臺的時候不會引入無用的代碼呢?
最簡單的辦法:將每個平臺單獨放一個倉庫,單獨維護(hù) ;但是這種辦法的問題也很嚴(yán)重:人力資源浪費嚴(yán)重;會導(dǎo)致一些重復(fù)的代碼很多;維護(hù)非常困難;
而較好一點的解決方案:我們可以通過插件化對代碼進(jìn)行組織:見下圖

我們用 Core來管理SDK內(nèi)與平臺無關(guān)的一些代碼,比如一些公共方法(生成mid方法、格式化);然后每個平臺單獨一個 SDK;去繼承core的類;SDK 內(nèi)自己管理SDK特有的核心方法邏輯,比如上報、參數(shù)初始化;最后就是 Plugins插件,每個SDK都是由內(nèi)核+插件組成的,我們將所有的插件功能,比如性能監(jiān)控、錯誤監(jiān)控都抽離成插件;
這樣子進(jìn)行 SDK 的設(shè)計有很多好處:
每個平臺分開打包,每個包的體積會大大縮小; 代碼的邏輯更加 清晰自恰
最后打包上線時,我們通過修改 build 的腳本,對 packages 文件夾下的每個平臺都單獨打一個包,并且分開上傳到 npm 平臺;
SDK 如何方便的進(jìn)行業(yè)務(wù)拓展和定制?
業(yè)務(wù)功能總是會不斷迭代的,SDK 也一樣,所以說我們在設(shè)計SDK的時候就要考慮它的一個拓展性;我們來看下圖:

上圖是 SDK 內(nèi)部的一個架構(gòu)設(shè)計 :內(nèi)核+插件 的設(shè)計;
內(nèi)核里是SDK內(nèi)的 公共邏輯或者基礎(chǔ)邏輯;比如數(shù)據(jù)格式化和數(shù)據(jù)上報是底下插件都要用到的公共邏輯;而配置初始化是SDK運行的一個基礎(chǔ)邏輯;插件里是SDK的 上層拓展業(yè)務(wù),比如說監(jiān)聽js錯誤、監(jiān)聽promise錯誤,每一個小功能都是一個插件;內(nèi)核和插件一起組成了 SDK實例Instance,最后暴露給客戶端使用;
而看了上圖已經(jīng)上文的解釋,可拓展這個問題的答案已經(jīng)很清晰了,我們需要拓展業(yè)務(wù),只需要在內(nèi)核的基礎(chǔ)上,不斷的往上疊加 Monitor 插件的數(shù)量就可以了;
至于說定制化,插件里的功能,都是使用與否不影響整個SDK運行的,所以我們可以自由的讓用戶對插件里的功能進(jìn)行定制化,決定哪個監(jiān)控功能啟用、哪個監(jiān)控功能不啟用等等....
我這邊舉個代碼例子,大家可以參考著看看就行:
// 服務(wù)于 Web 的SDK,繼承了 Core 上的與平臺無關(guān)方法;
class WebSdk extends Core {
// 性能監(jiān)控實例,實例里每個插件實現(xiàn)一個性能監(jiān)控功能;
public performanceInstance: WebVitals;
// 行為監(jiān)控實例,實例里每個插件實現(xiàn)一個行為監(jiān)控功能;
public userInstance: UserVitals;
// 錯誤監(jiān)控實例,實例里每個插件實現(xiàn)一個錯誤監(jiān)控功能;
public errorInstance: ErrorVitals;
// 上報實例,這里面封裝上報方法
public transportInstance: TransportInstance;
// 數(shù)據(jù)格式化實例
public builderInstance: BuilderInstance;
// 維度實例,用以初始化 uid、sid等信息
public dimensionInstance: DimensionInstance;
// 參數(shù)初始化實例
public configInstance: ConfigInstance;
private options: initOptions;
constructor(options: initOptions) {
super();
this.configInstance = new ConfigInstance(this, options);
// 各種初始化......
}
}
export default WebSdk;
看上面的代碼,我在初始化每個插件的時候,都將 this 傳入進(jìn)去,那么每個插件里面都可以訪問內(nèi)核里的方法;
SDK 在拓展新業(yè)務(wù)的時候,如何保證原有業(yè)務(wù)的正確性?
在上述的 內(nèi)核+插件 設(shè)計下,我們開發(fā)新業(yè)務(wù)對原功能的影響基本上可以忽略不計,但是難免有意外,所以在 SDK 項目的層面上,需要有 單元測試 的來保證業(yè)務(wù)的穩(wěn)定性;
我們可以引入單元測試,并對 每一個插件,每一個內(nèi)核方法,都單獨編寫測試用例,在覆蓋率達(dá)標(biāo)的情況下,只要每次代碼上傳都測試通過,就可以保證原有業(yè)務(wù)的一個穩(wěn)定性;
SDK 如何實現(xiàn)異常隔離以及上報?
首先,我們引入監(jiān)控系統(tǒng)的原因之一就是為了避免頁面產(chǎn)生錯誤,而如果因為監(jiān)控SDK報錯,導(dǎo)致整個應(yīng)用主業(yè)務(wù)流程被中斷,這是我們不能夠接收的;
實際上,我們無法保證我們的 SDK 不出現(xiàn)錯誤,那么假如萬一SDK本身報錯了,我們就需要它不會去影響主業(yè)務(wù)流程的運行;最簡單粗暴的方法就是把整個 SDK 都用 try catch 包裹起來,那么這樣子即使出現(xiàn)了錯誤,也會被攔截在我們的 catch 里面;
但是我們回過頭來想一想,這樣簡單粗暴的包裹,會帶來哪些問題:
我們只能獲取到一個報錯的信息,但是我們無法得知報錯的位置、插件; 我們沒有將其上報,我們無法感知到 SDK 產(chǎn)生了錯誤 我們沒法獲取 SDK 出錯的一個環(huán)境數(shù)據(jù)
那么,我們就需要一個相對優(yōu)雅的一個異常隔離+上報機(jī)制,回想我們上文的架構(gòu):內(nèi)核+插件的形式;我們對每一個插件模塊,都單獨的用trycatch包裹起來,然后當(dāng)拋出錯誤的時候,進(jìn)行數(shù)據(jù)的封裝、上報;
這樣子,就完成了一個異常隔離機(jī)制:
它實現(xiàn)了:當(dāng)SDK產(chǎn)生異常時不會影響主業(yè)務(wù)的流程; 當(dāng)SDK產(chǎn)生異常時進(jìn)行數(shù)據(jù)的封裝、上報; 出現(xiàn)異常后,中止 SDK 的運行,并移除所有的監(jiān)聽;
SDK 如何實現(xiàn)服務(wù)端時間的校對?
看到這里,可能有的同學(xué)并不明白,進(jìn)行服務(wù)端時間的校對是什么意思;我們首先要明白,我們通過 JS 調(diào)用 new Date() 獲取的時間,是我們的機(jī)器時間;也就是說:這個時間是一個隨時都有可能不準(zhǔn)確的時間;
那么既然時間是不準(zhǔn)確的,假如有一個對時間精準(zhǔn)度要求比較敏感的功能:比如說 API全鏈路監(jiān)控;最后整體繪制出來的全鏈路圖直接客戶端的訪問時間點變成了未來的時間點,直接時間穿梭那可不行;

如上圖,我們先要了解的是,http響應(yīng)頭 上有一個字段 Date;它的值是服務(wù)端發(fā)送資源時的服務(wù)器時間,我們可以在初始化SDK的時候,發(fā)送一個簡單的請求給上報服務(wù)器,獲取返回的 Date 值后計算 Diff差值 存在本地;
這樣子就可以提供一個 公共API,來提供一個時間校對的服務(wù),讓本地的時間 比較趨近于 服務(wù)端的真實時間;(只是比較趨近的原因是:還會有一個單程傳輸耗時的誤差)
let diff = 0;
export const diffTime = (date: string) => {
const serverDate = new Date(date);
const inDiff = Date.now() - serverDate.getTime();
if (diff === 0 || diff > inDiff) {
diff = inDiff;
}
};
export const getTime = () => {
return new Date(Date.now() - diff);
};
當(dāng)然,這里還可以做的更精確一點,我們可以讓后端服務(wù)在返回的時候,帶上 API 請求在后端服務(wù)執(zhí)行完畢所消耗的時間
server-timing,放在響應(yīng)頭里;我們?nèi)〉綌?shù)據(jù)后,將ttfb 耗時減去返回的server-timing再除以2;就是單程傳輸?shù)暮臅r;那這樣我們上文的計算中差的單程傳輸耗時的誤差就可以補(bǔ)上了;
SDK 如何實現(xiàn)會話級別的錯誤上報去重?
首先,我們需要理清一個概念,我們可以認(rèn)為:
在用戶的 一次會話中,如果產(chǎn)生了同一個錯誤,那么將這同一個錯誤上報多次是沒有意義的;在用戶的 不同會話中,如果產(chǎn)生了同一個錯誤,那么將不同會話中產(chǎn)生的錯誤進(jìn)行上報是有意義的;
為什么有上面的結(jié)論呢?理由很簡單:
在用戶的同一次會話中,如果點擊一個按鈕出現(xiàn)了錯誤,那么再次點擊同一個按鈕,必定會出現(xiàn)同一個錯誤,而這出現(xiàn)的多次錯誤,影響的是同一個用戶、同一次訪問;所以將其全部上報是沒有意義的; 而在同一個用戶的不同會話中,如果出現(xiàn)了同一個錯誤,那么這不同會話里的錯誤進(jìn)行上報就顯得有意義了;
所以說我們在第三篇文章《一文摸清前端監(jiān)控實踐要點(三)錯誤監(jiān)控》[5]中有一個生成 錯誤mid 的操作,這是一個唯一id,但是它的唯一規(guī)則是針對于不同錯誤的唯一;
// 對每一個錯誤詳情,生成一串編碼
export const getErrorUid = (input: string) => {
return window.btoa(unescape(encodeURIComponent(input)));
};
所以說我們傳入的參數(shù),是 錯誤信息、錯誤行號、錯誤列號、錯誤文件等可能的關(guān)鍵信息的一個集合,這樣保證了產(chǎn)生在同一個地方的錯誤,生成的 錯誤mid 都是相等的;這樣子,我們才能在錯誤上報的入口函數(shù)里,做上報去重;
// 封裝錯誤的上報入口,上報前,判斷錯誤是否已經(jīng)發(fā)生過
errorSendHandler = (data: ExceptionMetrics) => {
// 統(tǒng)一加上 用戶行為追蹤 和 頁面基本信息
const submitParams = {
...data,
breadcrumbs: this.engineInstance.userInstance.breadcrumbs.get(),
pageInformation: this.engineInstance.userInstance.metrics.get('page-information'),
} as ExceptionMetrics;
// 判斷同一個錯誤在本次頁面訪問中是否已經(jīng)發(fā)生過;
const hasSubmitStatus = this.submitErrorUids.includes(submitParams.errorUid);
// 檢查一下錯誤在本次頁面訪問中,是否已經(jīng)產(chǎn)生過
if (hasSubmitStatus) return;
this.submitErrorUids.push(submitParams.errorUid);
// 記錄后清除 breadcrumbs
this.engineInstance.userInstance.breadcrumbs.clear();
// 一般來說,有報錯就立刻上報;
this.engineInstance.transportInstance.kernelTransportHandler(
this.engineInstance.transportInstance.formatTransportData(transportCategory.ERROR, submitParams),
);
};
SDK 采用什么樣的上報策略?
對于上報方面來說,SDK的數(shù)據(jù)上報可不是隨隨便便就上報上去了,里面有涉及到數(shù)據(jù)上報的方式取舍以及上報時機(jī)的選擇等等,還有一些可以讓數(shù)據(jù)上報更加優(yōu)雅的優(yōu)化點;
首先,日志上報并不是應(yīng)用的主要功能邏輯,日志上報行為不應(yīng)該影響業(yè)務(wù)邏輯,不應(yīng)該占用業(yè)務(wù)計算資源;那么在往下閱讀之前,我們先來了解一下目前通用的幾個上報方式:
信標(biāo)( Beacon API)Ajax( XMLHttpRequest和fetch)Image( GIF、PNG)
我們來簡單講一下上述的幾個上報方式
首先 Beacon API[6] 是一個較新的 API
它可以將數(shù)據(jù)以 POST方法將少量數(shù)據(jù)發(fā)送到服務(wù)端它保證頁面卸載之前啟動信標(biāo)請求 并允許運行完成且不會阻塞請求或阻塞處理用戶交互事件的任務(wù)。
然后 Ajax 請求方式就不用我多說了,大家應(yīng)該平常用的最多的異步請求就是 Ajax;
最后來說一下 Image 上報方式:我們可以以向服務(wù)端請求圖片資源的形式,像服務(wù)端傳輸少量數(shù)據(jù),這種方式不會造成跨域;
上報方式
看了上面的三種上報方式,我們最終采用 sendBeacon + xmlHttpRequest 降級上報的方式,當(dāng)瀏覽器不支持 sendBeacon 或者 傳輸?shù)臄?shù)據(jù)量超過了 sendBeacon 的限制,我們就降級采用 xmlHttpRequest 進(jìn)行上報數(shù)據(jù);
優(yōu)先選用 Beacon API 的理由上文已經(jīng)有提到:它可以保證頁面卸載之前啟動信標(biāo)請求,是一種數(shù)據(jù)可靠,傳輸異步并且不會影響下一頁面的加載 的傳輸方式。
而降級使用 XMLHttpRequest 的原因是, Beacon API 現(xiàn)在并不是所有的瀏覽器都完全支持,我們需要一個保險方案兜底,并且 sendbeacon 不能傳輸大數(shù)據(jù)量的信息,這個時候還是得回到 Ajax 來;
看到了這里,有的同學(xué)可能會問:為什么不用 Image 呀?那跨域怎么辦呀?原因也很簡單:
Image是以GET方式請求圖片資源的方式,將上報數(shù)據(jù)附在URL上攜帶到服務(wù)端,而URL地址的長度是有一定限制的。規(guī)范對URL長度并沒有要求,但是瀏覽器、服務(wù)器、代理服務(wù)器都對URL長度有要求。有的瀏覽器要求URL中path部分不超過2048,這就導(dǎo)致有些請求會發(fā)送不完全。至于 跨域問題,作為接受數(shù)據(jù)上報的服務(wù)端,允許跨域是理所應(yīng)當(dāng)?shù)模?/section>
我們將其簡單封裝一下:
export enum transportCategory {
// PV訪問數(shù)據(jù)
PV = 'pv',
// 性能數(shù)據(jù)
PERF = 'perf',
// api 請求數(shù)據(jù)
API = 'api',
// 報錯數(shù)據(jù)
ERROR = 'error',
// 自定義行為
CUS = 'custom',
}
export interface DimensionStructure {
// 用戶id,存儲于cookie
uid: string;
// 會話id,存儲于cookiestorage
sid: string;
// 應(yīng)用id,使用方傳入
pid: string;
// 應(yīng)用版本號
release: string;
// 應(yīng)用環(huán)境
environment: string;
}
export interface TransportStructure {
// 上報類別
category: transportCategory;
// 上報的維度信息
dimension: DimensionStructure;
// 上報對象(正文)
context?: Object;
// 上報對象數(shù)組
contexts?: Array<Object>;
// 捕獲的sdk版本信息,版本號等...
sdk: Object;
}
export default class TransportInstance {
private engineInstance: EngineInstance;
public kernelTransportHandler: Function;
private options: TransportParams;
constructor(engineInstance: EngineInstance, options: TransportParams) {
this.engineInstance = engineInstance;
this.options = options;
this.kernelTransportHandler = this.initTransportHandler();
}
// 格式化數(shù)據(jù),傳入部分為 category 和 context \ contexts
formatTransportData = (category: transportCategory, data: Object | Array<Object>): TransportStructure => {
const transportStructure = {
category,
dimension: this.engineInstance.dimensionInstance.getDimension(),
sdk: getSdkVersion(),
} as TransportStructure;
if (data instanceof Array) {
transportStructure.contexts = data;
} else {
transportStructure.context = data;
}
return transportStructure;
};
// 初始化上報方法
initTransportHandler = () => {
return typeof navigator.sendBeacon === 'function' ? this.beaconTransport() : this.xmlTransport();
};
// beacon 形式上報
beaconTransport = (): Function => {
const handler = (data: TransportStructure) => {
const status = window.navigator.sendBeacon(this.options.transportUrl, JSON.stringify(data));
// 如果數(shù)據(jù)量過大,則本次大數(shù)據(jù)量用 XMLHttpRequest 上報
if (!status) this.xmlTransport().apply(this, data);
};
return handler;
};
// XMLHttpRequest 形式上報
xmlTransport = (): Function => {
const handler = (data: TransportStructure) => {
const xhr = new (window as any).oXMLHttpRequest();
xhr.open('POST', this.options.transportUrl, true);
xhr.send(JSON.stringify(data));
};
return handler;
};
}
上報時機(jī)
上報時機(jī)這里,一般來說:
PV、錯誤、用戶自定義行為都是觸發(fā)后立即就進(jìn)行上報;性能數(shù)據(jù)需要等待頁面加載完成、數(shù)據(jù)采集完畢后進(jìn)行上報;API請求數(shù)據(jù)會進(jìn)行本地暫存,在數(shù)據(jù)量達(dá)到10條(自擬)時觸發(fā)一次上報,并且在頁面可見性變化、以及頁面關(guān)閉之前進(jìn)行上報;如果你還要上報 點擊行為等其余的數(shù)據(jù),跟API請求數(shù)據(jù)一樣的上報時機(jī);
上報優(yōu)化
或許,我們想把我們的數(shù)據(jù)上報做的再優(yōu)雅一點,那么我們還有什么可以優(yōu)化的點呢?還是有的:
啟用 HTTP2,在HTTP1中,每次日志上報請求頭都攜帶了大量的重復(fù)數(shù)據(jù)導(dǎo)致性能浪費。HTTP2頭部壓縮采用Huffman Code壓縮請求頭,能有效減少請求頭的大小;服務(wù)端可以返回 204狀態(tài)碼,省去響應(yīng)體;使用 `requestIdleCallback`[7] ,這是一個較新的 API,它可以插入一個函數(shù),這個函數(shù)將在瀏覽器空閑時期被調(diào)用。這使開發(fā)者能夠在主事件循環(huán)上執(zhí)行后臺和低優(yōu)先級工作,而不會影響延遲關(guān)鍵事件,如果不支持的話,就使用 settimeout;
平臺數(shù)據(jù)如何進(jìn)行 削峰限流?
假設(shè)說,有某一個時間點,突然間流量爆炸,無數(shù)的數(shù)據(jù)向服務(wù)器訪問過來,這時如果沒有一個削峰限流的策略,很可能會導(dǎo)致機(jī)器Down掉,
所以說我們有必要去做一個削峰限流,從概率學(xué)的角度上講,在大數(shù)據(jù)量的基礎(chǔ)上我們對于整體數(shù)據(jù)做一個百分比的截斷,并不會影響整體的一個數(shù)據(jù)比例。
簡單方案-隨機(jī)丟棄策略進(jìn)行限流
前端做削峰限流最簡單的方法是什么?沒錯,就是 Math.random() ,我們讓用戶傳入一個采樣率,
if(Math.random()<0.5) return;
非常簡單的就實現(xiàn)了!但是這個方案不是一個很優(yōu)雅的解決辦法,為什么呢?
大流量項目限制了 50% 的流量,它的流量仍然多; 小流量項目限制了 50% 的流量,那就沒有流量了;
優(yōu)化方案-流量整型
現(xiàn)在做流量整形的方法很多,最常見的就是三種:
計數(shù)器算法:計數(shù)器算法就是單位時間內(nèi)入庫數(shù)量固定,后面的數(shù)據(jù)全部丟棄;缺點是無法應(yīng)對惡意用戶;漏桶算法:漏桶算法就是系統(tǒng)以固有的速率處理請求,當(dāng)請求太多超過了桶的容量時,請求就會被丟棄;缺點是漏桶算法對于驟增的流量來說缺乏效率;令牌桶算法:令牌桶算法就是系統(tǒng)會以恒定的速度往固定容量的桶里放入令牌,當(dāng)請求需要被處理時就會從桶里取一個令牌,當(dāng)沒有令牌可取的時候就會據(jù)拒絕服務(wù);
對于上述三種限流方案的文章很多,我這里就不細(xì)展開描述,有興趣的同學(xué)自己去找一下資料閱讀;
我們先來分析一下:
計數(shù)器能夠削峰,限制最大并發(fā)數(shù)以保證服務(wù)高可用令牌桶實現(xiàn)流量均勻入庫,保證下游服務(wù)健康
最后我們團(tuán)隊在上述的方案選擇中,最終選擇了 計數(shù)器 + 令牌桶 的方案;這也是參考了 前端早早聊 李振:如何從 0 到 1 建設(shè)前端性能監(jiān)控系統(tǒng)[8] 的限流方案分享;

首先從外部來的流量是我們無法預(yù)估的,假設(shè)如上圖我們有三個 服務(wù)器Pod,如果總流量來的非常大,那么這時我們通過計數(shù)器算法,給它設(shè)置一個很大的最大值;這個最大值只防小人不防君子,可能99%的項目都不會觸發(fā);這樣經(jīng)過 總流量的計數(shù)器削峰后,再到中心化的令牌桶限流:通過redis來實現(xiàn),我們先做一個定時器每分鐘都去令牌桶里寫令牌,然后單機(jī)的流量每個進(jìn)來后,都去redis里取令牌,有令牌就處理入庫;沒有令牌就把流量拋棄;這樣子我們就實現(xiàn)了一個 單機(jī)的削峰+中心化的限流,兩者一結(jié)合,就是解決了小流量應(yīng)用限流后沒流量的問題,以及控制了入庫的數(shù)量均勻且穩(wěn)定;
平臺數(shù)據(jù)為什么需要 數(shù)據(jù)加工?
那么,為什么需要數(shù)據(jù)加工,以及數(shù)據(jù)加工需要做什么處理?
當(dāng)我們的數(shù)據(jù)上報之后,因為 IP地址 是在服務(wù)端獲取的嘛,所以服務(wù)端就需要有一個服務(wù),去統(tǒng)一給請求數(shù)據(jù)中家加上 IP地址 以及 IP地址 解析后的歸屬地、運營商等信息;
根據(jù)業(yè)務(wù)需要,還可以加上服務(wù)端服務(wù)的版本號 等其余信息,方便后續(xù)做追蹤;
這里就不展開描述~
平臺數(shù)據(jù)為什么需要 數(shù)據(jù)清洗、聚合?
在一開始的整體架構(gòu)設(shè)計中已經(jīng)說明:
數(shù)據(jù)清洗是為了白名單、黑名單過濾等的業(yè)務(wù)需要,還有避免已關(guān)閉的應(yīng)用數(shù)據(jù)繼續(xù)入庫;數(shù)據(jù)聚合是為了將相同信息的數(shù)據(jù)進(jìn)行抽象聚合成issue,以便查詢和追蹤;
這樣子假設(shè)后續(xù)我們需要在數(shù)據(jù)庫查詢:某一條錯誤,產(chǎn)生了幾次,影響了幾個人,錯誤率是多少,這樣子可以不用再去 ES 中撈日志,而是在 MySQL 中直接查詢即可;
并且,我們還可以將抽象聚合出來的 issue ,關(guān)聯(lián)于公司的 缺陷平臺(類bug管理平臺) ,實現(xiàn) issue追蹤 、 直接自動貼bug到負(fù)責(zé)人頭上 等業(yè)務(wù)功能;
平臺數(shù)據(jù)如何進(jìn)行 多維度追蹤?
首先我們會對每一個用戶(user),會去生成一個 用戶id(uid ;并對每一次會話(session),生成一個 會話id(sid) ;
uid 和 sid 都是28位的隨機(jī)ID,sid 和 uid 都在初始化時生成,不同的是,因為 uid 的生命周期只在一次會話之中(關(guān)閉頁簽之前),所以 sid 我們存放在 sessionStorage 中,而 uid 我們存放在 cookie 里,過期時間設(shè)置六個月;
每次SDK初始化時,都先去 cookie 和 sessionStorage 里取 uid 和 sid,如果取不到就重新生成一份;并且在每次數(shù)據(jù)上報時,都將這些 id 附帶上去;
你如果有需要,還可以再搞一個登錄id,由使用方傳入,專門存放登錄成功后的登錄態(tài)ID;
這樣一系列搞完之后,我們在第二篇文章《一文摸清前端監(jiān)控實踐要點(二)行為監(jiān)控》[9]中收集了很多的行為數(shù)據(jù),包括PV訪問、路由跳轉(zhuǎn)、http請求、click事件、自定義事件、甚至第三章的錯誤數(shù)據(jù)等等;這些種種零零散散的數(shù)據(jù)就可以被串聯(lián)起來,得到新的分析價值;
因為 cookie 有極小的可能性被用戶手動禁用,這種情況下
uid傳null就可以了
代碼錯誤如何進(jìn)行 源碼映射?
在第三篇文章中,我們通過解析錯誤堆棧,得到了錯誤的文件、行列號等信息,可以通過對 sourcemap 可以對源碼進(jìn)行映射,定位錯誤源碼的位置;
大家可以跳轉(zhuǎn)閱讀相應(yīng)的代碼:一文摸清前端監(jiān)控自研實踐(三)錯誤監(jiān)控 \- Source Map[10]
當(dāng)然需要注意的是,在生產(chǎn)環(huán)境我們是不可以將 sourcemap 文件發(fā)布上線的,我們可以通過手動上傳到監(jiān)控平臺的形式去進(jìn)行錯誤的分析定位;
如何設(shè)計監(jiān)控告警的維度?
首先,監(jiān)控告警不是一個易事,在什么情況下,我們需要進(jìn)行告警的推送?
我們先來了解兩個概念:宏觀告警 和 微觀告警;
| key | 宏觀告警 | 微觀告警 |
|---|---|---|
| 告警依據(jù) | 是否超出了閾值? | 是否有產(chǎn)生新的異常? |
| 關(guān)鍵指標(biāo) | 數(shù)量、比率 | 單個異常 |
| 比對方法 | 時間區(qū)間內(nèi)的 異常數(shù)量、異常比率 | 新增的異常且異常uid未解決 |
宏觀告警更加關(guān)注的是:一段時間區(qū)間內(nèi),新增異常的數(shù)量、比率是否超過了閾值;如果超過了那就進(jìn)行告警;微觀告警更加關(guān)注的是:是否有新增的、且未解決的異常;
我們團(tuán)隊這邊目前做的都是微觀告警;只要出現(xiàn)的新異常,它的 uid 是當(dāng)前已激活的異常中全新的一個;那么就進(jìn)行告警,通知大群、通知負(fù)責(zé)人、在缺陷平臺上新建 bug 指派給負(fù)責(zé)人;
監(jiān)控告警如何指派給代碼提交者?
如上文提到,我們當(dāng)發(fā)現(xiàn)新 bug 產(chǎn)生時,我們可以將這個 bug 指派給負(fù)責(zé)人;這里其實還可以做的更細(xì)致一點,我們可以做一個 處理人自動分配 的機(jī)制;
處理人自動分配,分配給誰呢?還記得我們在第三篇錯誤監(jiān)控中,捕獲錯誤時上報了錯誤的位置,也就是源碼所在;那么我們只需要找到最近一次提交這行代碼的人就可以了;
Git Blame
那么找出 出錯行author 的原理其實就是 Git Blame ;這方面的文檔很多,不了解的同學(xué)可以看一下 Git Blame[11];

看上圖,指令其實很簡單,
// git blame -L <n,m> <file>
// n是起始行,m是結(jié)束行,file是指定文件
// eg:
git blame -L 2,2 LICENSE
查詢返回的結(jié)構(gòu)是:
commitID (代碼提交作者 提交時間 代碼位于文件中的行數(shù)) 實際代碼
這樣子,我們就可以獲取到具體的提交記錄是哪次,并且提交者是誰;
利用 Gitlab Open-api 在服務(wù)端集成

在 gitlab 文檔[12] 中,詳細(xì)說明了API的使用和參數(shù)方法;我們只需傳入 range[start] 、 range[end] ,還有具體的 分支 和 文件名 ;我們就可以像下面這個官方給出的例子一樣調(diào)用
curl --head --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/13083/repository/files/path%2Fto%2Ffile.rb/blame?ref=master&range[start]=1&range[end]=2"
參考閱讀
一文摸清前端監(jiān)控實踐要點(一)性能監(jiān)控[13]
一文摸清前端監(jiān)控實踐要點(二)行為監(jiān)控[14]
一文摸清前端監(jiān)控實踐要點(三)錯誤監(jiān)控[15]
前端早早聊 李振:如何從 0 到 1 建設(shè)前端性能監(jiān)控系統(tǒng)[16]
Git Blame[17]
Gitlab 查詢 Blame[18]
參考資料
https://juejin.cn/post/7097157902862909471: https://juejin.cn/post/7097157902862909471
[2]https://juejin.cn/post/7098656658649251877: https://juejin.cn/post/7098656658649251877
[3]https://juejin.cn/post/7100841779854835719/: https://juejin.cn/post/7100841779854835719/
[4]https://juejin.cn/post/7108660942686126093: https://juejin.cn/post/7108660942686126093
[5]https://juejin.cn/post/7100841779854835719/: https://juejin.cn/post/7100841779854835719/
[6]https://developer.mozilla.org/zh-CN/docs/Web/API/Beacon_API: https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FBeacon_API
[7]https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback: https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FWindow%2FrequestIdleCallback
[8]https://www.zaozao.run/video/s8/s8-3: https://link.juejin.cn?target=https%3A%2F%2Fwww.zaozao.run%2Fvideo%2Fs8%2Fs8-3
[9]https://juejin.cn/post/7098656658649251877: https://juejin.cn/post/7098656658649251877
[10]https://juejin.cn/post/7100841779854835719#heading-24: https://juejin.cn/post/7100841779854835719#heading-24
[11]https://git-scm.com/docs/git-blame: https://link.juejin.cn?target=https%3A%2F%2Fgit-scm.com%2Fdocs%2Fgit-blame
[12]https://docs.gitlab.com/ee/api/repository_files.html#get-file-blame-from-repository: https://link.juejin.cn?target=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fapi%2Frepository_files.html%23get-file-blame-from-repository
[13]https://juejin.cn/post/7097157902862909471: https://juejin.cn/post/7097157902862909471
[14]https://juejin.cn/post/7098656658649251877: https://juejin.cn/post/7098656658649251877
[15]https://juejin.cn/post/7100841779854835719/: https://juejin.cn/post/7100841779854835719/
[16]https://www.zaozao.run/video/s8/s8-3: https://link.juejin.cn?target=https%3A%2F%2Fwww.zaozao.run%2Fvideo%2Fs8%2Fs8-3
[17]https://git-scm.com/docs/git-blame: https://link.juejin.cn?target=https%3A%2F%2Fgit-scm.com%2Fdocs%2Fgit-blame
[18]https://docs.gitlab.com/ee/api/repository_files.html#get-file-blame-from-repository: https://link.juejin.cn?target=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fapi%2Frepository_files.html%23get-file-blame-from-repository
關(guān)于本文:
來源:菜貓子neko
https://juejin.cn/post/7108660942686126093

往期推薦



最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個專業(yè)的技術(shù)人...


