#研發解決方案#易車前端監控系統

itread01 2021-02-22 21:04:53
前端 itread01 方案


# 背景自研工具是為了解決內部問題而生,希望通過這些問題引起大家的共鳴:1. 是否知道重要的業務,該頁面是可以正常服務於使用者的?2. 能否在問題還沒有大規模爆發之前,快速的感知到業務的異常?3. 怎麼不去使用者的電腦上就能直觀的看到問題所在,從而俯瞰專案全域性;能否從巨集觀到微觀一路下鑽快速的定位線上告警資訊?4. 在跨部門溝通時拿出合理的證據,來告訴他這個時間段該介面就是無法訪問的,並告知我們的引數傳的很正確,幫助服務端**反查**問題。5. 產品和設計同學想要提升使用者體驗,研發不斷迭代功能版本。那這些我們以為的優化點,效果究竟如何?怎麼去衡量?6. 哪個廣告位,哪個資源位更有價值?怎麼能更為精準的觸達使用者痛點,為提升業務賦能?我們看到這些疑問,都需要**資料指標**的支撐。從解決這些問題的角度出發,把反覆出現或無法跟其他部門交代的問題,打造成可以幫助我們解決問題的產品。所以在這種場景下,易車·前端監控應運而生。它主要是多場景多維度實時的監控大盤,實現瀏覽器客戶端的全鏈路監控,方便團隊事後追查和整改,轉變為事前預警和快速判定根因。經過詳細的規劃以後,我們把前端監控分為四期,分別為:異常監控(一期)、效能監控(二期)、資料埋點(三期)、行為採集(四期),於 2020 年 6 月 23 號正式啟動研發,目前處於二期階段。# 關鍵結構為實現上述需求,監控系統主要分為四個階段來實現;分別是:指標採集、指標儲存、統計與分析、視覺化展示。![](https://img2020.cnblogs.com/blog/328599/202102/328599-20210222161132813-571147359.png)**指標採集階段:**通過前端整合的 SDK 收集請求、效能、異常等指標資訊;在客戶端簡單的處理一次,然後上報到伺服器。**指標儲存階段:**用於接收前端上報的採集資訊,主要目的是資料落地。**統計與分析階段:**自動分析,通過資料的統計,讓程式發現問題從而觸發報警。人工分析,是通過視覺化的資料面板,讓使用者看到具體的日誌資料,從而發現異常問題根源。**視覺化展示階段:**通過視覺化的平臺;在這些指標(API 監控、異常監控、資源監控、效能監控)中,追查使用者行為來定位各項問題。# 整體架構圖隨著統計需求的增加以及前端應用的上線,資料量由早期的每天 100 多萬條資料;到現在的每天約 7000 萬條資料。架構上也經歷了三次版本的迭代。這是最新版的架構圖,主要經過 6 層處理。![](https://img2020.cnblogs.com/blog/328599/202102/328599-20210222161202658-493211893.png)**採集層:**PC 和 H5 使用了一套 SDK 監聽事件採集指標,然後將監聽到的指標通過 REST 介面往 Logback 推送資料。Logback 以長連線的方式,會把這些不同型別的指標資料推送到 Flume 叢集當中。Flume 叢集會將這些資料,分發到 Kafka Topic 進行儲存。**處理層:**由 Flink 去實時消費;Flink 會消費三種類型,分別是:離線資料落地、實時 ETL+圖譜、明細日誌。**儲存層:**離線資料會儲存到 HDFS 中;實時 ETL+圖譜資料會儲存到 MySQL 中;明細資料會落入到 ES 中。**統計層:**離線(DW、DM)、實時(分鐘級->十分鐘級->小時級)的方式,對指標進行彙總和統計。**應用層:**最後由介面去彙總表和明細 ES 裡查詢資料。**展示層:**然後前端輸出圖表、報表、明細、鏈路等資訊。# 技術方案## 資料採集採集最初的願景是希望對業務**無侵入性**,業務系統無需改造,只需要嵌入一段程式碼即可。所以這些採集,都是 SDK 自動化的處理。SDK 會全域性監聽幾個事件,分別為:錯誤監聽、資源異常的監聽、頁面效能的監聽、API 呼叫的監聽。![](https://img2020.cnblogs.com/blog/328599/202102/328599-20210222161216386-1081843.png)通過這幾項監聽,最終彙總為 3 項指標的採集。**異常採集:**呼叫 error/unhandledrejection 事件,用於捕獲 JS、圖片、CSS 等資源異常資訊。****效能採集:**呼叫瀏覽器原生的 performance.timing API 捕獲頁面的效能指標。**介面採集:**通過 Object.definePropety 代理全域性的 XHR 用於捕獲瀏覽器的 XHR/FETCH 的請求。## 採集端 SDK 架構![](https://img2020.cnblogs.com/blog/328599/202102/328599-20210222161239970-1393936010.png)SDK 主要分為兩部分:**第一部分:**SDK 主要是 SDK 的驅動,包含:入口、核心工具以及通用型別的推斷。**第二部分:**也叫做外掛部分(藍色區域),主要實現上面的三項資料指標的採集。接下來主要會詳細的介紹第二部分,各項指標的採集方案。## 異常採集方案通過監聽 error 錯誤,即可捕獲到所有(JS 錯誤、圖片載入、CSS 載入、JS 載入、Promise 等)異常;它也支援 InternalError、ReferenceError [等 7 種錯誤捕獲](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error)。以下是關鍵性程式碼。### 監聽事件```javascript/** * 監聽 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); });}```### 判斷異常型別```javascript/** * 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; }}```### 捕獲異常資料```javascript/** * 上報 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 的方案如下:```javascript/** * 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;}```## 效能採集方案 ### 瀏覽器頁面載入過程![](https://img2020.cnblogs.com/blog/328599/202102/328599-20210222161302193-1167348100.png)### 效能指標獲取方式我們藉助於瀏覽器原生的 [Navigation Timing API](https://w3c.github.io/navigation-timing/) 能夠獲取到上述**頁面載入過程**中的各項效能指標資料,用於效能分析,它的時間單位是納秒級。![](https://img2020.cnblogs.com/blog/328599/202102/328599-20210222161343315-1496046922.png)當然也藉助於 [PerformanceObserver API](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver) 等用於測量 [FCP](https://web.dev/fcp/)、[LCP](https://web.dev/lcp/)、[FID](https://web.dev/fid/)、[TTI](https://web.dev/tti/)、[TBT](https://web.dev/tbt/)、[CLS](https://web.dev/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](https://github.com/wendux/Ajax-hook) 的實現原理。```javascriptexport 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 個請求;我們期望在階段性的所有請求都結束已後,彙總成一條記錄合併上報,這樣能有效減少請求的併發量。關鍵性程式碼如下:```javascript/** * Ajax 請求外掛 * * @author wubaiqing */// 所有的資料請求,以及總量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,對首屏優化有好處。目前我們採用的是第一種**同步載入**的方式。# 產品部分截圖## 首頁首頁會展示所有應用的情報,在首頁可以直觀的發現各應用的異常資料。![](https://img2020.cnblogs.com/blog/328599/202102/328599-20210222161416646-1203440516.png)## 大盤頁面如果想對某個應用細項的排查,會進入到應用的大盤頁面;主要會展示該應用,前端的重要性指標,近一個小時內的資料狀況。目前主要有頁面效能、資源異常、JS 異常、API 介面成功率等重要指標作為衡量。![](https://img2020.cnblogs.com/blog/328599/202102/328599-20210222161425488-1297879458.png)## 詳情頁詳情頁,就可以看到該應用某項指標的資料細項。方便團隊進行事後的追查、整改,提前預警和快速判定根因所用。![](https://img2020.cnblogs.com/blog/328599/202102/328599-20210222161450754-1508935139.png)# # 遇到的問題SDK 採集到指標以後對資料進行上報時,會做一些過濾性的**前置**操作,如:- 遮蔽掉一些黑名單。- 指標的削峰填谷。- 應用資訊的轉換。- 客戶端 IP 獲取。- Token 的驗證。前置處理有一個弊端,因為伺服器會經過解析轉換環節;當資料量達到每日 7000 萬左右,上報的伺服器就扛不住了。所以我們把資料**前置**處理,變為資料落地**後置**處理;後置處理就是在資料清洗的過程中,在過濾掉黑名單以及異常指標。這樣就減輕了上報伺服器的壓力。並且倉庫也會保留所有的原始資料,如果出現異常的時,也方便我們溯源,對資料進行恢復。# 整體規劃我們分為了四期,目前還處於二期效能監控階段。| 計劃 | 目標 | 優先順序 | 支援平臺 | 主要解決的問題點 || :---: | :---: | :---: | :---: | :---: || 一期 | 異常監控 | 高 | PC、Mobile、小程式 | 異常影響的影響使用者,資源載入異常感知,網路請求異常感知,程式碼報錯異常感知,程式碼報錯的細項(SourceMap)分析 || 二期 | 效能監控 | 高 | | 效能值(首位元組、DOMReady、頁面完全載入、重定向、DNS、TCP、請求響應等耗時),API 監控(成功率、成功耗時、失敗次數等),頁面引用資源統計,和資源佔比(JS、CSS、圖片、字型、iFrame、Ajax 等),位數對比,95% 的使用者、99% 的使用者、平均使用者 || 三期 | 資料埋點 | 中 | | 作業系統、解析度、瀏覽器,事件分類(點選事件、滾動事件),具體的指定的事件型別(點選 Banner 圖),事件發生時間,觸發事件的位置(滑鼠 X、Y,可生成熱力圖),訪客標識,使用者標識,鏈路採集 || 四期 | 行為採集 | 低 | | 進入頁面,離開頁面,點選元素,滾動頁面,操作鏈路,自定義(如,點選廣告位的圖),Chrome 外掛直觀看到埋點 |# 其它自研 APM 系統方便與內部進行的打通和整合;比如應用釋出後就可以直接推送 SourceMap 檔案;並且能實現線上釋出以後自動進行頁面效能的分析等工作。如果目前發展階段還不需要自建一個這樣的系統,但業務需要這樣的能力,也可以考慮第三方的一些產品。## 商業產品分析| | 易車 | [聽雲](https://www.tingyun.com/) | [阿里雲 ARMS](https://www.aliyun.com/product/arms) | [Fundebug](https://www.yuque.com/ab93na/project/fcqxvc?inner=2HKDM) | [嶽鷹](https://yueying.effirst.com) | [FrontJS](https://www.frontjs.com/) || --- | --- | --- | --- | --- | --- | --- || 頁面效能監控 | 功能齊全 | 基礎功能 | 功能齊全 | 弱 | 功能齊全 | 功能齊全 || 異常監控 | 基礎功能 | 基礎功能 | 功能齊全 | 功能齊全 | 功能齊全 | 功能齊全 || API 監控 | 功能齊全 | 基礎功能 | 功能齊全 | 基礎功能 | 基礎功能 | 基礎功能 || 頁面載入瀑布圖 | 無 | 功能齊全 | 基礎功能 | 無 | 無 | 功能齊全 || 互動性 | 好 | 一般 | 好 | 不清晰 | 好 | 好 |## 重要性指標對和阿里 ARMS 對比易車·前端監控和阿里雲 ARMS 做了一些重要性的指標對比,**均值**的浮動在上下在 5%-8% 左右;![](https://img2020.cnblogs.com/blog/328599/202102/328599-20210222161632849-1036373400.jpg)![](https://img2020.cnblogs.com/blog/328599/202102/328599-20210222161640796-828289541.jpg)![](https://img2020.cnblogs.com/blog/328599/202102/328599-20210222161645032-1511614805.jpg)## 參考連結- [User Timing API](https://w3c.github.io/user-timing/)- [Long Tasks API](https://w3c.github.io/longtasks/)- [Element Timing API](https://wicg.github.io/element-timing/)- [Navigation Timing API](https://w3c.github.io/navigation-timing/)- [Resource Timing API](https://w3c.github.io/resource-timing/)- [Server timing](https://w3c.github.io/server-timing/)- [Custom Metrics](https://web.dev/custom-metrics/)- [Lighthouse performance scoring](https://web.dev/performance-scoring/)- [FCP](https://web.dev/fcp/)- [LCP](https://web.dev/lcp/)- [FID](https://web.dev/fid/)- [TTI](https://web.dev/tti/)- [TBT](https://web.dev/tbt/)- [CLS](https://web.dev/cls/)
版权声明
本文为[itread01]所创,转载请带上原文链接,感谢
https://www.itread01.com/content/1613997665.html

  1. vue.js项目win10 npm install的时候报错
  2. springboot 开启http2
  3. Vue事件总线(EventBus)
  4. jQuery EasyUI使用教程:自定义数据网格分页
  5. 使用OkHttp和OkHttpGo获取OneNET云平台数据
  6. Vue3组件(九)Vue + element-Plus + json = 动态渲染的表单控件
  7. Python belongs to back-end development or front-end development? Introduction to Python!
  8. HTTP 1.x 学习笔记 —— Web 性能权威指南
  9. Vue3组件(九)Vue + element-Plus + json = 动态渲染的表单控件
  10. HTTP 1.x 学习笔记 —— Web 性能权威指南
  11. Javascript中的事件冒泡与捕获
  12. The root element is missing 解决方法
  13. Javascript中的事件冒泡与捕获
  14. 010_ HTML5
  15. 020_ CSS3
  16. 030_ JavaScript
  17. Anti shake and throttling and corresponding react hooks package
  18. Using CSS in JS in svelte
  19. Pure CSS free website with dark mode switching function
  20. Front end interview daily 3 + 1 - day 678
  21. How to insert an element into the specified index of an array?
  22. 配置证书使得ngnix能够发布https的可信网站
  23. HTTP接口调试利器!4.8万Star的HTTP命令行客户端!
  24. 前端url链接带的参数加密
  25. HTTP接口调试利器!4.8万Star的HTTP命令行客户端!
  26. Is react server components OK?
  27. Summary of front end basic knowledge (4) - webpack
  28. Sass nesting rule
  29. 前端三大框架:数据绑定与数据流
  30. axios 源码阅读(一)--探究基础能力的实现
  31. Javascript中的事件冒泡与捕获
  32. #研发解决方案#易车前端监控系统
  33. 【JS】877- 35 个 JavaScript 的奇葩知识,长见识了!
  34. #研发解决方案#易车前端监控系统
  35. 高性能 Nginx HTTPS 调优 - 如何为 HTTPS 提速 30%
  36. Front end, school recruitment, Taobao, guide
  37. Front end, social recruitment, Taobao, guide
  38. javascript 十大经典排序
  39. Draw a mellow cactus with the boneless technique of Meticulous Brushwork
  40. HTTP 1.x 學習筆記 —— Web 效能權威指南
  41. Vue3元件(九)Vue + element-Plus + json = 動態渲染的表單控制元件
  42. 我的 HTTP/1.1 好慢啊!
  43. Vue為何採用非同步渲染
  44. The response status was 0. Check out the W3C XMLHttpRequest Level 2 spec for
  45. Front end monastery
  46. How to quickly understand a new front end project?
  47. webpack4.X核心工具库之tapable实例对象Hook
  48. webpack4.X核心工具库之tapable实例对象Hook
  49. C++使用libcurl进行http通讯
  50. Can be directly used in HTML special character table Unicode character set
  51. C++使用libcurl进行http通讯
  52. Java traverses list < string > and takes out the string elements in it, and splices them with ","
  53. Self taught web front end more than two months, talk about my harvest and progress, learning summary
  54. vue 中使用 css 变量
  55. 深入了解React中state和props的更新
  56. 百度分享不支持https的解决方案
  57. [practical] ABAP mail sending (HTML + attachment)
  58. [practical] ABAP mail sending (HTML + attachment)
  59. Teach you how to select products with less profit and competition in Amazon Product Development (1)
  60. 【微前端】微前端最终章-qiankun指南以及微前端整体探索