背景
自研工具是為了解決內部問題而生,希望通過這些問題引起大家的共鳴:
- 是否知道重要的業務,該頁面是可以正常服務於使用者的?
- 能否在問題還沒有大規模爆發之前,快速的感知到業務的異常?
- 怎麼不去使用者的電腦上就能直觀的看到問題所在,從而俯瞰專案全域性;能否從巨集觀到微觀一路下鑽快速的定位線上告警資訊?
- 在跨部門溝通時拿出合理的證據,來告訴他這個時間段該介面就是無法訪問的,並告知我們的引數傳的很正確,幫助服務端反查問題。
- 產品和設計同學想要提升使用者體驗,研發不斷迭代功能版本。那這些我們以為的優化點,效果究竟如何?怎麼去衡量?
- 哪個廣告位,哪個資源位更有價值?怎麼能更為精準的觸達使用者痛點,為提升業務賦能?
我們看到這些疑問,都需要資料指標的支撐。從解決這些問題的角度出發,把反覆出現或無法跟其他部門交代的問題,打造成可以幫助我們解決問題的產品。
所以在這種場景下,易車·前端監控應運而生。
它主要是多場景多維度實時的監控大盤,實現瀏覽器客戶端的全鏈路監控,方便團隊事後追查和整改,轉變為事前預警和快速判定根因。
經過詳細的規劃以後,我們把前端監控分為四期,分別為:異常監控(一期)、效能監控(二期)、資料埋點(三期)、行為採集(四期),於 2020 年 6 月 23 號正式啟動研發,目前處於二期階段。
關鍵結構
為實現上述需求,監控系統主要分為四個階段來實現;分別是:指標採集、指標儲存、統計與分析、視覺化展示。
指標採集階段:通過前端整合的 SDK 收集請求、效能、異常等指標資訊;在客戶端簡單的處理一次,然後上報到伺服器。
指標儲存階段:用於接收前端上報的採集資訊,主要目的是資料落地。
統計與分析階段:自動分析,通過資料的統計,讓程式發現問題從而觸發報警。人工分析,是通過視覺化的資料皮膚,讓使用者看到具體的日誌資料,從而發現異常問題根源。
視覺化展示階段:通過視覺化的平臺;在這些指標(API 監控、異常監控、資源監控、效能監控)中,追查使用者行為來定位各項問題。
整體架構圖
隨著統計需求的增加以及前端應用的上線,資料量由早期的每天 100 多萬條資料;到現在的每天約 7000 萬條資料。架構上也經歷了三次版本的迭代。這是最新版的架構圖,主要經過 6 層處理。
採集層:PC 和 H5 使用了一套 SDK 監聽事件採集指標,然後將監聽到的指標通過 REST 介面往 Logback 推送資料。Logback 以長連線的方式,會把這些不同型別的指標資料推送到 Flume 叢集當中。Flume 叢集會將這些資料,分發到 Kafka Topic 進行儲存。
處理層:由 Flink 去實時消費;Flink 會消費三種型別,分別是:離線資料落地、實時 ETL+圖譜、明細日誌。
儲存層:離線資料會儲存到 HDFS 中;實時 ETL+圖譜資料會儲存到 MySQL 中;明細資料會落入到 ES 中。
統計層:離線(DW、DM)、實時(分鐘級->十分鐘級->小時級)的方式,對指標進行彙總和統計。
應用層:最後由介面去彙總表和明細 ES 裡查詢資料。
展示層:然後前端輸出圖表、報表、明細、鏈路等資訊。
技術方案
資料採集
採集最初的願景是希望對業務無侵入性,業務系統無需改造,只需要嵌入一段程式碼即可。所以這些採集,都是 SDK 自動化的處理。
SDK 會全域性監聽幾個事件,分別為:錯誤監聽、資源異常的監聽、頁面效能的監聽、API 呼叫的監聽。
通過這幾項監聽,最終彙總為 3 項指標的採集。
異常採集:呼叫 error/unhandledrejection 事件,用於捕獲 JS、圖片、CSS 等資源異常資訊。**
效能採集:呼叫瀏覽器原生的 performance.timing API 捕獲頁面的效能指標。
介面採集:通過 Object.definePropety 代理全域性的 XHR 用於捕獲瀏覽器的 XHR/FETCH 的請求。
採集端 SDK 架構
SDK 主要分為兩部分:
第一部分:SDK 主要是 SDK 的驅動,包含:入口、核心工具以及通用型別的推斷。
第二部分:也叫做外掛部分(藍色區域),主要實現上面的三項資料指標的採集。
接下來主要會詳細的介紹第二部分,各項指標的採集方案。
異常採集方案
通過監聽 error 錯誤,即可捕獲到所有(JS 錯誤、圖片載入、CSS 載入、JS 載入、Promise 等)異常;它也支援 InternalError、ReferenceError 等 7 種錯誤捕獲。
以下是關鍵性程式碼。
監聽事件
/**
* 監聽 error、unhandledrejection 方法處理異常資訊
*
* @param {YicheMonitorInstance} instance SDK 例項
*/
export default function setupErrorPlugin(instance: YicheMonitorInstance) {
// JS 錯誤或靜態資源載入錯誤
on('error', (e: Event, url: any, lineno: any) => {
handleError(instance, e, url, lineno);
});
// Promise 錯誤,IE 不支援
on('unhandledrejection', (e: any) => {
handleError(instance, e);
});
}
判斷異常型別
/**
* W3C 模式支援 ErrorEvent,所有的異常從 ErrorEvent 這裡取
*
* @param {MutationEvent} error 資源錯誤、程式碼錯誤
*/
function handleW3C(event: any) {
switch (event.type) {
// 判斷指令碼錯誤,還是資源錯誤
case 'error':
event instanceof ErrorEvent
? reportJSError(instance, event)
: reportResourceError(instance, event);
break;
// Promise 是否存在未捕獲 reject 的錯誤
case 'unhandledrejection':
reportPromiseError(instance, event);
break;
}
}
捕獲異常資料
/**
* 上報 JS 異常
*
* @param {YicheMonitorInstance} instance SDK 例項
* @param {ErrorEvent} event
*/
export default function reportJSError(
instance: YicheMonitorInstance,
event: ErrorEvent,
): void {
// 設定上報資料
const report = new ReportDataStruct('error', 'js');
const errorInfo = event.error
? event.error.message
: `未知錯誤:${event.message}`;
// 設定錯誤資訊,相容遠端指令碼不設定 Script error 導致的異常
report.setData({
det: errorInfo.substring(0, 2000),
des: event.error ? event.error.stack : '',
defn: event.filename,
deln: event.lineno,
delc: event.colno,
rre: 1,
});
}
處理 IE 相容問題
捕獲異常時處理下 IE 的相容性問題即可,IE 的方案如下:
/**
* IE 8 的錯誤項,所以針對於 IE 8 瀏覽器,我們只需要獲取到它出錯了即可。
*
* 1. 錯誤訊息
* 2. 錯誤頁面
* 3. 錯誤行號(因為檔案通常是壓縮的,所以統計 IE8 的行號是沒有任何意義的)
*
* @param {string} error 錯誤訊息
* @param {string | undefined} url 異常的 URL
* @param {number | undefined} lineno 異常行數,IE 沒有列數
*/
export function handleIE8Error(
error: string,
url?: string | undefined,
lineno?: number | undefined,
) {
return {
colno: 0,
lineno: lineno,
filename: url,
message: error,
error: {
message: error,
stack: `IE8 Error:${error}`,
},
} as ErrorEvent;
}
/**
* IE 9 的錯誤,需要在 target 裡面獲取到
*
* @param { Element | any } error IE9 異常的元素
*/
export function handleIE9Error(error: any) {
// 獲取 Event
const event = error.currentTarget.event;
return {
colno: event.errorCharacter,
lineno: event.errorLine,
filename: event.errorUrl,
message: event.errorMessage,
error: {
message: event.errorMessage,
stack: `IE9 Error:${event.errorMessage}`,
},
} as ErrorEvent;
}
效能採集方案
瀏覽器頁面載入過程
效能指標獲取方式
我們藉助於瀏覽器原生的 Navigation Timing API 能夠獲取到上述頁面載入過程中的各項效能指標資料,用於效能分析,它的時間單位是納秒級。
當然也藉助於 PerformanceObserver API 等用於測量 FCP、LCP、FID、TTI、TBT、CLS 等關鍵性指標。
詳細的計算公式
指標 | 含義 | 計算公式 |
---|---|---|
ttfb | 首位元組時間 | timing.responseStart - timing.requestStart |
domReady | Dom Ready時間 | timing.domContentLoadedEventEnd - timing.fetchStart |
pageLoad | 頁面完全載入時間 | timing.loadEventStart - timing.fetchStart |
dns | DNS 查詢時間 | timing.domainLookupEnd - timing.domainLookupStart |
tcp | TCP 連線時間 | timing.connectEnd - timing.connectStart |
ssl | SSL 連線時間 | timing.secureConnectionStart > 0 ? timing.connectEnd - timing.secureConnectionStart) : 0 |
contentDownload | 內容傳輸時間 | timing.responseEnd - timing.responseStart |
domParse | DOM 解析時間 | timing.domInteractive - timing.responseEnd |
resourceDownload | 資源載入耗時 | timing.loadEventStart - timing.domContentLoadedEventEnd |
waiting | 請求響應 | timing.responseStart - timing.requestStart |
fpt | 白屏時間,老 | timing.responseEnd - timing.fetchStart |
tti | 首次可互動 | timing.domInteractive - timing.fetchStart |
firstByte | 首包時間 | timing.responseStart - timing.domainLookupStart |
domComplete | DOM 完成時間 | timing.domComplete - timing.domLoading |
fp | 白屏時間,新指標 | performance.getEntriesByType('paint')[0] |
fcp | 首次有效內容繪製 | performance.getEntriesByType('paint')[1] |
lcp | 首屏大內容繪製時間 | PerformanceObserver('largest-contentful-paint')" |
快開比 | 頁面完全載入時長 ≤ 某時長(如2s)的 取樣PV / 總取樣PV * 100% | |
慢開比 | 頁面完全載入時長 ≥ 某時長(如5s)的 取樣PV / 總取樣PV * 100% |
網路請求採集方案
網路請求,通過 Object.definePropety 的方式對 XHR 做的代理。關鍵性程式碼如下。
重寫 XMLHttpRequest
這部分可以直接參考 ajax-hook 的實現原理。
export function hook(proxy) {
window[realXhr] = window[realXhr] || XMLHttpRequest
XMLHttpRequest = function () {
const xhr = new window[realXhr];
for (let attr in xhr) {
let type = "";
try {
type = typeof xhr[attr]
} catch (e) {
}
if (type === "function") {
this[attr] = hookFunction(attr);
} else {
Object.defineProperty(this, attr, {
get: getterFactory(attr),
set: setterFactory(attr),
enumerable: true
})
}
}
const that = this;
xhr.getProxy = function () {
return that
}
this.xhr = xhr;
}
return window[realXhr];
}
攔截所有請求
正常的情況下一個頁面會請求多個介面,假如有 20 個請求;
我們期望在階段性的所有請求都結束已後,彙總成一條記錄合併上報,這樣能有效減少請求的併發量。
關鍵性程式碼如下:
/**
* Ajax 請求外掛
*
* @author wubaiqing <wubaiqing@vip.qq.com>
*/
// 所有的資料請求,以及總量
let allRequestRecordArray: any = [];
let allRequestRecordCount: any = [];
// 成功的資料,200,304 的資料
let allRequestData: any = [];
// 異常的資料,超時,405 等介面不存在的資料
let errorData: any = [];
/**
* 監聽 Ajax 請求資訊
*
* @param {YicheMonitorInstance} instance SDK 例項
*/
export default function setupAjaxPlugin(instance: YicheMonitorInstance) {
let id = 0;
proxy({
onRequest: (config, handler) => {
// 過濾掉聽雲、福爾摩斯、APM
if (filterDomain(config)) {
// 新增請求記錄的佇列
allRequestRecordArray.push({
id,
timeStamp: new Date().getTime(), // 記錄請求時長
config, // 包含:請求地址、body 等內容
handler, // XHR 實體
});
// 記錄請求總數
allRequestRecordCount.push(1);
id++;
}
handler.next(config);
},
// 失敗時會觸發一次
onError: (err, handler) => {
if (allRequestRecordArray.length === 0) {
handler.next(err);
return;
}
for (let i = 0; i < allRequestRecordArray.length; i++) {
// 當前的資料
const currentData = allRequestRecordArray[i];
if (
currentData.handler.xhr.status === 0 && // 未傳送
currentData.handler.xhr.readyState === 4
) {
errorData.push(
JSON.stringify(handleReportDataStruct(instance, currentData)),
);
allRequestRecordArray.splice(i, 1);
}
}
sendAllRequestData(instance);
handler.next(err);
},
onResponse: (response, handler) => {
// 沒有請求就返回 Null
if (allRequestRecordArray.length === 0) {
handler.next(response);
return;
}
for (let i = 0; i < allRequestRecordArray.length; i++) {
// 當前的資料
const currentData = allRequestRecordArray[i];
// 只要請求載入完成,不管是成功還是失敗,都記錄是一次請求
if (currentData.handler.xhr.readyState === 4) {
// 正常的請求
if (
(currentData.handler.xhr.status >= 200 &&
currentData.handler.xhr.status < 300) ||
currentData.handler.xhr.status === 304
) {
allRequestData.push(
JSON.stringify(handleReportDataStruct(instance, currentData)),
);
} else {
if (currentData.handler.xhr.status > 0) {
// 具備狀態碼
// 錯誤的請求
errorData.push(
JSON.stringify(handleReportDataStruct(instance, currentData)),
);
}
}
// 刪除當前陣列的值
allRequestRecordArray.splice(i, 1);
}
}
// 傳送資料
sendAllRequestData(instance);
handler.next(response);
},
});
}
function sendAllRequestData(instance) {
if (
allRequestData.length + errorData.length ===
allRequestRecordCount.length
) {
// 處理正常請求
if (allRequestData.length > 0 || errorData.length > 0) {
handleAllRequestData(instance);
}
// 處理異常請求
if (errorData.length > 0) {
handleErrorData(instance);
}
// 所有的資料請求,以及總量
allRequestRecordArray = [];
allRequestRecordCount = [];
// 成功的資料,200,304 的資料
allRequestData = [];
// 異常的資料,超時,405 等介面不存在的資料
errorData = [];
}
}
探針載入方案
探針載入有兩種方式,他們分別有一些優缺點:
同步載入:採集 SDK 放到所有 JS 請求頭的前面;因為載入順序的問題,如果放在其他 JS 請求之後,之前的 JS 出現了異常,就捕獲不到了。因為要提前載入 JS 資源,會對效能有一定影響。
非同步載入:採集 SDK 通過執行 JS 後注入到頁面中;如果能保障首次的 JS 無異常,也可以使用非同步的方式載入 SDK,對首屏優化有好處。
目前我們採用的是第一種同步載入的方式。
產品部分截圖
首頁
首頁會展示所有應用的情報,在首頁可以直觀的發現各應用的異常資料。
大盤頁面
如果想對某個應用細項的排查,會進入到應用的大盤頁面;
主要會展示該應用,前端的重要性指標,近一個小時內的資料狀況。
目前主要有頁面效能、資源異常、JS 異常、API 介面成功率等重要指標作為衡量。
詳情頁
詳情頁,就可以看到該應用某項指標的資料細項。方便團隊進行事後的追查、整改,提前預警和快速判定根因所用。
遇到的問題
SDK 採集到指標以後對資料進行上報時,會做一些過濾性的前置操作,如:
- 遮蔽掉一些黑名單。
- 指標的削峰填谷。
- 應用資訊的轉換。
- 客戶端 IP 獲取。
- Token 的驗證。
前置處理有一個弊端,因為伺服器會經過解析轉換環節;當資料量達到每日 7000 萬左右,上報的伺服器就扛不住了。
所以我們把資料前置處理,變為資料落地後置處理;後置處理就是在資料清洗的過程中,在過濾掉黑名單以及異常指標。這樣就減輕了上報伺服器的壓力。
並且倉庫也會保留所有的原始資料,如果出現異常的時,也方便我們溯源,對資料進行恢復。
整體規劃
我們分為了四期,目前還處於二期效能監控階段。
計劃 | 目標 | 優先順序 | 支援平臺 | 主要解決的問題點 |
---|---|---|---|---|
一期 | 異常監控 | 高 | PC、Mobile、小程式 | 異常影響的影響使用者,資源載入異常感知,網路請求異常感知,程式碼報錯異常感知,程式碼報錯的細項(SourceMap)分析 |
二期 | 效能監控 | 高 | 效能值(首位元組、DOMReady、頁面完全載入、重定向、DNS、TCP、請求響應等耗時),API 監控(成功率、成功耗時、失敗次數等),頁面引用資源統計,和資源佔比(JS、CSS、圖片、字型、iFrame、Ajax 等),位數對比,95% 的使用者、99% 的使用者、平均使用者 | |
三期 | 資料埋點 | 中 | 作業系統、解析度、瀏覽器,事件分類(點選事件、滾動事件),具體的指定的事件型別(點選 Banner 圖),事件發生時間,觸發事件的位置(滑鼠 X、Y,可生成熱力圖),訪客標識,使用者標識,鏈路採集 | |
四期 | 行為採集 | 低 | 進入頁面,離開頁面,點選元素,滾動頁面,操作鏈路,自定義(如,點選廣告位的圖),Chrome 外掛直觀看到埋點 |
其它
自研 APM 系統方便與內部進行的打通和整合;比如應用釋出後就可以直接推送 SourceMap 檔案;並且能實現線上釋出以後自動進行頁面效能的分析等工作。
如果目前發展階段還不需要自建一個這樣的系統,但業務需要這樣的能力,也可以考慮第三方的一些產品。
商業產品分析
易車 | 聽雲 | 阿里雲 ARMS | Fundebug | 嶽鷹 | FrontJS | |
---|---|---|---|---|---|---|
頁面效能監控 | 功能齊全 | 基礎功能 | 功能齊全 | 弱 | 功能齊全 | 功能齊全 |
異常監控 | 基礎功能 | 基礎功能 | 功能齊全 | 功能齊全 | 功能齊全 | 功能齊全 |
API 監控 | 功能齊全 | 基礎功能 | 功能齊全 | 基礎功能 | 基礎功能 | 基礎功能 |
頁面載入瀑布圖 | 無 | 功能齊全 | 基礎功能 | 無 | 無 | 功能齊全 |
互動性 | 好 | 一般 | 好 | 不清晰 | 好 | 好 |
重要性指標對和阿里 ARMS 對比
易車·前端監控和阿里雲 ARMS 做了一些重要性的指標對比,均值的浮動在上下在 5%-8% 左右;