有貨前端 Web-APM 實(shí)踐

背景

有貨電商技術(shù)架構(gòu)上采用的是前后端分離,前端是主要以業(yè)務(wù)展示和接口聚合為主,擁有自己的 BFF (Backend For Frontend),以 nodejs 為核心;后端以提供較小的業(yè)務(wù)數(shù)據(jù)接口,業(yè)務(wù)服務(wù)實(shí)現(xiàn)為主,以 java 技術(shù)體系為核心。在實(shí)際應(yīng)用場(chǎng)景下,前后端應(yīng)用對(duì)系統(tǒng)性能的關(guān)注點(diǎn)是不一樣。因此,前端團(tuán)隊(duì)需要跟據(jù)自身的需求,來(lái)搭建自己的 APM 系統(tǒng)。

1 前端的需求

對(duì)前端團(tuán)隊(duì)來(lái)說(shuō),用戶體驗(yàn)至關(guān)重要,而頁(yè)面的打開(kāi)速度就是用戶能感知因素中最重要的一環(huán)。前端團(tuán)隊(duì)對(duì) APM 的需求就是要盡可能地收集與頁(yè)面打開(kāi)速度有關(guān)的因素。經(jīng)過(guò)對(duì)業(yè)務(wù)和技術(shù)的討論,我們認(rèn)為以下方面影響了頁(yè)面的加載速度:

(1) 前端加載時(shí)間

在前端頁(yè)面加載是用戶感知的第一層。我們使用以下指標(biāo):

首屏加載時(shí)間:first-Screen

文檔加載時(shí)間:DOMContentLoaded

頁(yè)面加載時(shí)間:load

頁(yè)面腳本錯(cuò)誤:error

除了加載時(shí)間,我們還會(huì)把前端 js 運(yùn)行過(guò)程中出現(xiàn)的錯(cuò)誤上報(bào)。這樣,我們就能夠及時(shí)發(fā)現(xiàn)問(wèn)題,快速修復(fù)上線,使公司損失最小化。

(2) 業(yè)務(wù)處理時(shí)間

http 響應(yīng)時(shí)間:req-res-time

http 請(qǐng)求狀態(tài):http-status

這塊我們主要關(guān)注頁(yè)面請(qǐng)求到頁(yè)面響應(yīng)完成的時(shí)間:req-res-time,這個(gè)時(shí)間能夠代表我們系統(tǒng)的響應(yīng)速度,所以這個(gè)指標(biāo)能衡量當(dāng)時(shí)系統(tǒng)的性能。

此外,還會(huì)針對(duì) http 狀態(tài)碼這個(gè)值也進(jìn)行記錄,這樣就可以知道哪些路由有問(wèn)題,這樣就可以通過(guò)狀態(tài)碼的情況得到系統(tǒng)的健康程度。

(3) 接口調(diào)用時(shí)間

api 調(diào)用時(shí)間:api-time

api 調(diào)用狀態(tài):api-status

這塊我們會(huì)對(duì)每個(gè)接口都監(jiān)控其調(diào)用時(shí)間:api-time。同時(shí)我們還會(huì)針對(duì)每一次請(qǐng)求生成一個(gè)唯一 ID,對(duì)這個(gè)請(qǐng)求所調(diào)用的 api 進(jìn)行標(biāo)識(shí),這樣我們就能分析出,頁(yè)面調(diào)用的接口數(shù),每個(gè)接口調(diào)用的時(shí)間,接口的調(diào)用順序等,這些數(shù)據(jù)對(duì)后端的壓測(cè)和服務(wù)治理會(huì)非常有用。

同時(shí),對(duì) api 的響應(yīng)狀態(tài)碼進(jìn)行監(jiān)控,方便及時(shí)了解后端接口的基本情況。

(4) 系統(tǒng)運(yùn)行狀態(tài)

cpu 使用率: process-cpu

memory 使用率: process-mem

這塊包括系統(tǒng) cpu 和 memory 的使用情況做了收集,方便我們知道機(jī)器的情況。

針對(duì)這四個(gè)方面,我們?cè)O(shè)定了這 10 項(xiàng)指標(biāo),通過(guò)這些指標(biāo),我們這能全方位對(duì)我們網(wǎng)站業(yè)務(wù)的速度和穩(wěn)定性進(jìn)行了解,方便以后優(yōu)化。

2 整體架構(gòu)

Web-APM 在整體架構(gòu)設(shè)計(jì)上,分成了六個(gè)部分,如下圖所示,包括 client,service , collector, storage, api ,ui。箭頭代表數(shù)據(jù)的流向。其中,client 和 service 是收集指標(biāo)并發(fā)送指標(biāo),而 collector 作用是匯集指標(biāo),過(guò)濾數(shù)據(jù),存入 storage。 stoarge 的存在,我們是希望能保存一段時(shí)間數(shù)據(jù)情況,方便事后進(jìn)行查找和分析。 而 api 則是對(duì) storage 的數(shù)據(jù)對(duì)外提供一個(gè)接口,方便監(jiān)控和分析;ui 是提供一個(gè)界面,方便使用者進(jìn)行查看。


3 實(shí)現(xiàn)

從實(shí)現(xiàn)角度來(lái)看,我們還是比較功利的,即采用我們自己熟悉的技術(shù),并沒(méi)有上來(lái)就使用 ELK Stack,這其中是有原因的:

  1. 數(shù)據(jù)的體量上,百萬(wàn)級(jí)的已經(jīng)夠用了,用不上大數(shù)據(jù)這一套存儲(chǔ)

  2. 前端組的技術(shù)能力上,與 ELK Stack 技術(shù)棧不匹配,不能吃透這套技術(shù)

  3. 當(dāng)前也沒(méi)有合適的人去做這塊技術(shù)工作

于是我們跟據(jù)自己的需求和整體架構(gòu),在實(shí)現(xiàn)系統(tǒng)角度上,劃分成了多個(gè)層,每個(gè)層有各自的選擇,如下圖所示:


從圖中可以看出,Web-APM 系統(tǒng)實(shí)現(xiàn)上分成了 4 個(gè)層,分別是采集層,收集層,存儲(chǔ)層和監(jiān)控層,每一層我們都選擇了合適的技術(shù)來(lái)實(shí)現(xiàn)。

(1) 采集層

采集層對(duì)應(yīng)著我們的需求,也分成了四塊,包括前端層(yas.js), 業(yè)務(wù)層(yohobuy-node),數(shù)據(jù)層(yohonode-lib),系統(tǒng)層(yohonode-lib)。不同的層采集不同的數(shù)據(jù),最終數(shù)據(jù)發(fā)送到收集層。

收集數(shù)據(jù)時(shí),解決以下問(wèn)題:

首屏加載

我們參考了這篇文檔,思路就是計(jì)算首屏基線高度之上的所有圖片元素 onload 之后的時(shí)間。

錯(cuò)誤處理

利用 window.error 接口來(lái)實(shí)現(xiàn),self.writeError 里面就是我們自己的上報(bào)邏輯。

數(shù)據(jù)一致性

數(shù)據(jù)采集時(shí),我們注意到數(shù)據(jù)不一致的問(wèn)題。如路由。電商的頁(yè)面很多,最多可能就商品詳情頁(yè)。從前端的角度來(lái)看商品詳情頁(yè),每個(gè)商品詳情頁(yè)就是不同的 path,如 https://www.yohobuy.com/product/51768088.html。但從后端來(lái)看就不過(guò)是一個(gè)參數(shù) :id 而已,如 https://www.yohobuy.com/product/:id.html。我們做監(jiān)控的時(shí)候,不可能針對(duì)一個(gè)商品的鏈接進(jìn)行監(jiān)控,這樣是沒(méi)有什么用的,我們希望對(duì)商品詳細(xì)頁(yè)這一類(lèi)的頁(yè)面進(jìn)行監(jiān)控?!∪绻郧岸说?path 來(lái)進(jìn)行數(shù)據(jù)統(tǒng)計(jì)就只能統(tǒng)計(jì)到單個(gè)頁(yè)面的問(wèn)題,不能統(tǒng)計(jì)到商品詳情頁(yè)這一類(lèi)的情況。當(dāng)然可以在進(jìn)行數(shù)據(jù)匯集的時(shí)候,進(jìn)行正則匹配。這對(duì)我們來(lái)說(shuō)不太現(xiàn)實(shí),因?yàn)槲覀兙W(wǎng)站信息架構(gòu)調(diào)整了多次,路由也已經(jīng)調(diào)整多次,在另一個(gè)地方進(jìn)行正則就意味著,要在另一個(gè)地方維護(hù)一張正則映射表。我們希望的是就一個(gè)地方維護(hù)路由。

這個(gè)問(wèn)題我們進(jìn)行了多次討論,在技術(shù)的可行性下,選擇了一個(gè)技術(shù)方案,在每個(gè)頁(yè)面中,寫(xiě)入一個(gè)全局變量,把這個(gè)頁(yè)面的后端路由的 md5 寫(xiě)入到頁(yè)面中,這樣只要這個(gè)頁(yè)面路由不變,這個(gè)值就一直不變,發(fā)送監(jiān)控?cái)?shù)據(jù)就把這個(gè)路由值也帶上,這樣前后端的數(shù)據(jù)情況就能通過(guò)路 由對(duì)應(yīng)起來(lái),這樣就能更方便的統(tǒng)計(jì)數(shù)據(jù)。

請(qǐng)求跟蹤

有貨前端項(xiàng)目都是以 nodejs 為核心建立的技術(shù)棧。在 nodejs 中,一直缺較好的技術(shù)手段對(duì)異步進(jìn)行跟蹤和標(biāo)識(shí)。目前來(lái)看 async_hooks 技術(shù)應(yīng)該是比較好的候選方案。但在對(duì)我們來(lái)說(shuō),該技術(shù)不是很合適。因?yàn)槲覀冴P(guān)注點(diǎn)會(huì)更高一點(diǎn),只是針對(duì)業(yè)務(wù)流程進(jìn)行跟蹤,而不是對(duì)每一個(gè)異步進(jìn)行細(xì)致的分析。在考慮到業(yè)務(wù)實(shí)際情況和技術(shù)實(shí)現(xiàn),我們選擇了基于 reqId 進(jìn)行業(yè)務(wù)流程跟蹤的方案。該方案時(shí)序圖如下:

在頁(yè)面請(qǐng)求過(guò)來(lái)時(shí),我們針對(duì)這個(gè)請(qǐng)求生成 reqId,并且在調(diào)用后端 api 時(shí),會(huì)帶上 reqId 生成的上下文 ctx。最后返回頁(yè)面時(shí),把 reqId 寫(xiě)入用戶的 cookie 中。同一個(gè)頁(yè)面 ajax 發(fā)送請(qǐng)求時(shí),就會(huì)去判斷是否有這個(gè) reqId,這樣就能區(qū)分是頁(yè)面上的 ajax 的請(qǐng)求,還是別的方式造成的請(qǐng)求,同時(shí)也能統(tǒng)計(jì)出頁(yè)面上調(diào)用 ajax 請(qǐng)求的個(gè)數(shù)。

(2) 收集層

收集層,主要定義一下從采集端過(guò)來(lái)的數(shù)據(jù)的形式(influxdb),以什么協(xié)議傳輸(http)。

對(duì)打入的監(jiān)控?cái)?shù)據(jù)進(jìn)行一個(gè)緩沖(buffer),依據(jù)條件過(guò)濾(filter)出我們需要的數(shù)據(jù),把數(shù)據(jù)形式轉(zhuǎn)化(tranform)成我們想要的形式,洗好的數(shù)據(jù)定時(shí)寫(xiě)入存儲(chǔ)層中。

(3) 存儲(chǔ)層

在存儲(chǔ)層,我們分為在線存儲(chǔ)和離線存儲(chǔ)。在線存儲(chǔ)是與監(jiān)控有關(guān)需要實(shí)時(shí)交互的數(shù)據(jù),使用 influxdb 時(shí)序數(shù)據(jù)庫(kù)。在寫(xiě)入時(shí),我們會(huì)把數(shù)據(jù)再精簡(jiǎn),把最簡(jiǎn)單關(guān)鍵數(shù)據(jù)寫(xiě)入 influxdb 中,如 http-status, api-status, process, 方便下一層監(jiān)控層使用。

influxdb 的使用中,我們也碰到了問(wèn)題。例如:寫(xiě)入數(shù)據(jù)時(shí) tag 過(guò)多,導(dǎo)致查詢數(shù)據(jù)緩慢,我們就精減數(shù)據(jù);為提高 influxdb 性能,會(huì)做一個(gè)隊(duì)列,批量寫(xiě)入數(shù)據(jù)。

離線存儲(chǔ),我們選擇是 mysql。在寫(xiě)入時(shí),我們對(duì)數(shù)據(jù)進(jìn)行過(guò)濾,主要保存錯(cuò)誤,異常和慢路由。如前端和后端發(fā)生的錯(cuò)誤(包括堆棧),還有哪些路由的請(qǐng)求時(shí)間長(zhǎng)于 2000ms,這樣方便我們進(jìn)行離線地分析查看和統(tǒng)計(jì)。

還有,由于我們項(xiàng)目的特點(diǎn),對(duì)離線儲(chǔ)存不是要求一直保存,我們會(huì)定時(shí)對(duì) mysql 進(jìn)行整理和清除,當(dāng)前我們就只保留最近 7 天的數(shù)據(jù),這樣我們離線儲(chǔ)存的壓力就比較小。

(4) 監(jiān)控層

監(jiān)控層,也是分成兩部分,一個(gè)部分為 grafana ,利用 grafana 和 influxdb 配合的提醒功能,能對(duì)我們的線上環(huán)境通過(guò)短信和郵件實(shí)時(shí)進(jìn)行提醒。



另一部分是我們的 ci 系統(tǒng),包括一個(gè)監(jiān)控面板,用于查看詳細(xì)的錯(cuò)誤情況和路由情況。特別對(duì)于前端,我們發(fā)布的代碼都是經(jīng)過(guò) webpack 打包的代碼,直接去找錯(cuò)誤行列肯定是找不到的,因此我們生打包生產(chǎn)代碼時(shí),會(huì)生成 source-map 文件,ci 查看前端腳本錯(cuò)誤,會(huì)去解析 source-map, 拿到出錯(cuò)代碼的前后 20 行,這樣能方便地定位前端的錯(cuò)誤。下圖為解析 source-map 的相關(guān)代碼和前端的展示結(jié)果。


ci 每天都會(huì)去 mysql 查看路由的情況和異常的情況,生成統(tǒng)計(jì)報(bào)表,郵件給前端團(tuán)隊(duì)。


5 接入方式

當(dāng)前 agent 代碼,前端代碼,為了滿足功能需要,引入方式是將壓縮后的 agent 代碼直接插入到布局模板的 head 中。配置項(xiàng)目是在壓縮時(shí)就寫(xiě)入定值,這樣能減少項(xiàng)目接入的復(fù)雜度。

對(duì)于服務(wù)器端代碼的接入,我們當(dāng)前沒(méi)有采用獨(dú)立進(jìn)程的方式進(jìn)行部署,這是因?yàn)槭褂谜吆途S護(hù)者都是同一個(gè)團(tuán)隊(duì),還有監(jiān)控這塊對(duì)性能影響并不是很大。所以我們接入方式大部分直接引入一個(gè)獨(dú)立的包就可以完成工作,少部分的代碼直接傳入 express 對(duì)象代理接口和監(jiān)聽(tīng)事件,就能拿到數(shù)據(jù),做到業(yè)務(wù)無(wú)感知。

收集層我們是獨(dú)立部署的一臺(tái)服務(wù)器,這樣會(huì)更方便。而且我們?cè)O(shè)計(jì)成收集層掛了,也不會(huì)影響我們采集層的工作。

下圖是系統(tǒng)的調(diào)用情況:


6 總結(jié)

當(dāng)前,有貨 Web-APM 基礎(chǔ)的功能已經(jīng)完成,已經(jīng)在有貨 pc 站和 h5 站項(xiàng)目進(jìn)行部署,進(jìn)行監(jiān)控?cái)?shù)據(jù)的上報(bào)。通過(guò)一段時(shí)間的使用,我們解決了多數(shù)由于字段未定義造成的系統(tǒng)問(wèn)題,使我們的系統(tǒng)更加穩(wěn)定;前端我們也針對(duì)情況有選擇性地進(jìn)行了頁(yè)面的優(yōu)化。詳細(xì)的優(yōu)化方案,請(qǐng)看這篇 有貨移動(dòng)WEB端性能優(yōu)化探索實(shí)踐2

在未來(lái),我們會(huì)對(duì) Web-APM 進(jìn)行以下方面優(yōu)化:

  1. 增加瀏覽器端的數(shù)據(jù)采集深度,會(huì)從 performance, resource, navigator, screen 等方面進(jìn)行信息采集

  2. 結(jié)合用戶的 IP 和 useragent,對(duì)用戶瀏覽器端進(jìn)行分析

  3. 更詳細(xì)的每日分析報(bào)告

  4. 告警機(jī)制更智能化

以上就是我們有貨前端團(tuán)隊(duì) Web-APM 的實(shí)踐分享,歡迎大家批評(píng)指正。

7 參考文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容