<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          騰訊三面:說(shuō)說(shuō)前端監(jiān)控平臺(tái)/監(jiān)控SDK的架構(gòu)設(shè)計(jì)和難點(diǎn)亮點(diǎn)?

          共 18306字,需瀏覽 37分鐘

           ·

          2022-06-30 15:36

          前言

          事情是這樣的,上周,我的一位兩年前端經(jīng)驗(yàn)的發(fā)小,在 騰訊三輪面試 的時(shí)候被問(wèn)了一個(gè)問(wèn)題:說(shuō)說(shuō)你們公司前端監(jiān)控項(xiàng)目的架構(gòu)設(shè)計(jì)和亮點(diǎn)設(shè)計(jì) ;

          而說(shuō)回我這位發(fā)小,因?yàn)樽鲞^(guò)他們公司監(jiān)控項(xiàng)目的可視化報(bào)表界面,所以簡(jiǎn)歷上有寫(xiě)著前端監(jiān)控項(xiàng)目的項(xiàng)目經(jīng)驗(yàn)但是不幸的是,他雖然前端基礎(chǔ)相當(dāng)不錯(cuò),但并沒(méi)有實(shí)際參與監(jiān)控SDK的設(shè)計(jì)開(kāi)發(fā)(只負(fù)責(zé)寫(xiě)監(jiān)控的可視化分析界面),所以被問(wèn)到這個(gè)問(wèn)題,直接就一個(gè)懵了;結(jié)果也很正常,面試沒(méi)過(guò);

          那么這篇文章,我就來(lái)介紹一下對(duì)于前端監(jiān)控項(xiàng)目的 整體架構(gòu)可以做的亮點(diǎn)優(yōu)化 ;前文幾篇文章有介紹具體的前端監(jiān)控實(shí)現(xiàn),感興趣的小伙伴可以點(diǎn)擊鏈接跳轉(zhuǎn)過(guò)去閱讀; 傳送門(mén)就在下面

          傳送門(mén)

          這篇文章的標(biāo)題原擬定是:一文摸清前端監(jiān)控實(shí)踐要點(diǎn)(四)架構(gòu)設(shè)計(jì);但是我的發(fā)小面試剛好碰上了這么一個(gè)問(wèn)題,于是我便將標(biāo)題改為了這個(gè)。

          一文摸清前端監(jiān)控實(shí)踐要點(diǎn)(一)性能監(jiān)控[1]

          一文摸清前端監(jiān)控實(shí)踐要點(diǎn)(二)行為監(jiān)控[2]

          一文摸清前端監(jiān)控實(shí)踐要點(diǎn)(三)錯(cuò)誤監(jiān)控[3]

          騰訊三面:說(shuō)說(shuō)前端監(jiān)控告警分析平臺(tái)的架構(gòu)設(shè)計(jì)和難點(diǎn)亮點(diǎn)?[4]

          整體 架構(gòu)設(shè)計(jì)

          image.png

          直接上圖,我們?cè)趹?yīng)用層SDK上報(bào)的數(shù)據(jù),在接入層經(jīng)過(guò) 削峰限流數(shù)據(jù)加工 后,將原始日志存儲(chǔ)于 ES 中,再經(jīng)過(guò) 數(shù)據(jù)清洗數(shù)據(jù)聚合 后,將 issue(聚合的數(shù)據(jù)) 持久化存儲(chǔ)MySQL ,最后提供 RESTful API 提供給監(jiān)控平臺(tái)調(diào)用;

          • 削峰限流是為了避免 激增的大數(shù)據(jù)量、惡意用戶訪問(wèn)等高并發(fā)數(shù)據(jù)導(dǎo)致的服務(wù)崩潰;
          • 數(shù)據(jù)加工是為了將 IP、運(yùn)營(yíng)商、歸屬地等各種二次加工數(shù)據(jù),封裝進(jìn)上報(bào)數(shù)據(jù)里;
          • 數(shù)據(jù)清洗是為了經(jīng)由白名單黑名單過(guò)濾等的業(yè)務(wù)需要,還有避免已關(guān)閉的應(yīng)用數(shù)據(jù)繼續(xù)入庫(kù);
          • 數(shù)據(jù)聚合是為了將相同信息的數(shù)據(jù)進(jìn)行抽象聚合issue,以便查詢和追蹤;

          SDK 架構(gòu)設(shè)計(jì)

          為支持多平臺(tái)、可拓展可插拔的特點(diǎn),整體SDK的架構(gòu)設(shè)計(jì)是 內(nèi)核+插件 的插件式設(shè)計(jì);每個(gè) SDK 首先繼承于平臺(tái)無(wú)關(guān)的 Core 層代碼。然后在自身SDK中,初始化內(nèi)核實(shí)例和插件

          image.png
          image.png

          值得一談的點(diǎn)

          下面將主要談?wù)勥@些內(nèi)容:前端監(jiān)控項(xiàng)目除了正常的數(shù)據(jù)采集、數(shù)據(jù)報(bào)表分析以外;會(huì)碰上哪些難點(diǎn)可以去突破,或者說(shuō)可以做出哪些亮點(diǎn)的內(nèi)容

          SDK 如何設(shè)計(jì)成多平臺(tái)支持?

          首先我們先來(lái)了解一下,在前端監(jiān)控的領(lǐng)域里,我們可能不僅僅只是監(jiān)控一個(gè) web環(huán)境 下的數(shù)據(jù),包括 Nodejs微信小程序、Electron 等各種其余的環(huán)境都是有監(jiān)控的業(yè)務(wù)需求在的;

          那么我們就要思考一個(gè)點(diǎn),我們的一個(gè) SDK 項(xiàng)目,既然功能全,又要支持多平臺(tái),那么怎么設(shè)計(jì)這個(gè) SDK 可以讓它既支持多平臺(tái),但是在啟用某個(gè)平臺(tái)的時(shí)候不會(huì)引入無(wú)用的代碼呢?

          最簡(jiǎn)單的辦法:將每個(gè)平臺(tái)單獨(dú)放一個(gè)倉(cāng)庫(kù),單獨(dú)維護(hù) ;但是這種辦法的問(wèn)題也很?chē)?yán)重:人力資源浪費(fèi)嚴(yán)重;會(huì)導(dǎo)致一些重復(fù)的代碼很多;維護(hù)非常困難;

          而較好一點(diǎn)的解決方案:我們可以通過(guò)插件化對(duì)代碼進(jìn)行組織:見(jiàn)下圖

          image.png
          • 我們用 Core 來(lái)管理 SDK 內(nèi)與平臺(tái)無(wú)關(guān)的一些代碼,比如一些公共方法(生成mid方法、格式化);
          • 然后每個(gè)平臺(tái)單獨(dú)一個(gè) SDK;去繼承 core 的類(lèi);SDK 內(nèi)自己管理SDK特有的核心方法邏輯,比如上報(bào)、參數(shù)初始化;
          • 最后就是 Plugins 插件,每個(gè)SDK都是由 內(nèi)核+插件 組成的,我們將所有的插件功能,比如性能監(jiān)控、錯(cuò)誤監(jiān)控都抽離成插件;

          這樣子進(jìn)行 SDK 的設(shè)計(jì)有很多好處:

          • 每個(gè)平臺(tái)分開(kāi)打包,每個(gè)包的體積會(huì)大大縮小;
          • 代碼的邏輯更加清晰自恰

          最后打包上線時(shí),我們通過(guò)修改 build 的腳本,對(duì) packages 文件夾下的每個(gè)平臺(tái)都單獨(dú)打一個(gè)包,并且分開(kāi)上傳npm 平臺(tái);

          SDK 如何方便的進(jìn)行業(yè)務(wù)拓展和定制?

          業(yè)務(wù)功能總是會(huì)不斷迭代的,SDK 也一樣,所以說(shuō)我們?cè)谠O(shè)計(jì)SDK的時(shí)候就要考慮它的一個(gè)拓展性;我們來(lái)看下圖:

          image.png

          上圖是 SDK 內(nèi)部的一個(gè)架構(gòu)設(shè)計(jì) :內(nèi)核+插件 的設(shè)計(jì);

          • 內(nèi)核里是SDK內(nèi)的公共邏輯或者基礎(chǔ)邏輯;比如數(shù)據(jù)格式化數(shù)據(jù)上報(bào)是底下插件都要用到的公共邏輯;而配置初始化是SDK運(yùn)行的一個(gè)基礎(chǔ)邏輯
          • 插件里是SDK的上層拓展業(yè)務(wù),比如說(shuō)監(jiān)聽(tīng)js錯(cuò)誤、監(jiān)聽(tīng)promise錯(cuò)誤,每一個(gè)小功能都是一個(gè)插件;
          • 內(nèi)核和插件一起組成了 SDK實(shí)例 Instance,最后暴露給客戶端使用;

          而看了上圖已經(jīng)上文的解釋?zhuān)?code style="font-size: 14px;word-wrap: break-word;border-radius: 4px;font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;color: #9b6e23;background-color: #fff5e3;padding: 3px;margin: 3px;">可拓展這個(gè)問(wèn)題的答案已經(jīng)很清晰了,我們需要拓展業(yè)務(wù),只需要在內(nèi)核的基礎(chǔ)上,不斷的往上疊加 Monitor 插件的數(shù)量就可以了;

          至于說(shuō)定制化,插件里的功能,都是使用與否不影響整個(gè)SDK運(yùn)行的,所以我們可以自由的讓用戶對(duì)插件里的功能進(jìn)行定制化,決定哪個(gè)監(jiān)控功能啟用、哪個(gè)監(jiān)控功能不啟用等等....

          我這邊舉個(gè)代碼例子,大家可以參考著看看就行:

          // 服務(wù)于 Web 的SDK,繼承了 Core 上的與平臺(tái)無(wú)關(guān)方法;
          class WebSdk extends Core {
            // 性能監(jiān)控實(shí)例,實(shí)例里每個(gè)插件實(shí)現(xiàn)一個(gè)性能監(jiān)控功能;
            public performanceInstance: WebVitals;

            // 行為監(jiān)控實(shí)例,實(shí)例里每個(gè)插件實(shí)現(xiàn)一個(gè)行為監(jiān)控功能;
            public userInstance: UserVitals;

            // 錯(cuò)誤監(jiān)控實(shí)例,實(shí)例里每個(gè)插件實(shí)現(xiàn)一個(gè)錯(cuò)誤監(jiān)控功能;
            public errorInstance: ErrorVitals;

            // 上報(bào)實(shí)例,這里面封裝上報(bào)方法
            public transportInstance: TransportInstance;

            // 數(shù)據(jù)格式化實(shí)例
            public builderInstance: BuilderInstance;

            // 維度實(shí)例,用以初始化 uid、sid等信息
            public dimensionInstance: DimensionInstance;

            // 參數(shù)初始化實(shí)例
            public configInstance: ConfigInstance;

            private options: initOptions;

            constructor(options: initOptions) {
              super();
              this.configInstance = new ConfigInstance(this, options);
              // 各種初始化......
            }
          }

          export default WebSdk;

          看上面的代碼,我在初始化每個(gè)插件的時(shí)候,都將 this 傳入進(jìn)去,那么每個(gè)插件里面都可以訪問(wèn)內(nèi)核里的方法;

          SDK 在拓展新業(yè)務(wù)的時(shí)候,如何保證原有業(yè)務(wù)的正確性?

          在上述的 內(nèi)核+插件 設(shè)計(jì)下,我們開(kāi)發(fā)新業(yè)務(wù)對(duì)原功能的影響基本上可以忽略不計(jì),但是難免有意外,所以在 SDK 項(xiàng)目的層面上,需要有 單元測(cè)試 的來(lái)保證業(yè)務(wù)的穩(wěn)定性;

          我們可以引入單元測(cè)試,并對(duì) 每一個(gè)插件,每一個(gè)內(nèi)核方法,都單獨(dú)編寫(xiě)測(cè)試用例,在覆蓋率達(dá)標(biāo)的情況下,只要每次代碼上傳都測(cè)試通過(guò),就可以保證原有業(yè)務(wù)的一個(gè)穩(wěn)定性;

          SDK 如何實(shí)現(xiàn)異常隔離以及上報(bào)?

          首先,我們引入監(jiān)控系統(tǒng)的原因之一就是為了避免頁(yè)面產(chǎn)生錯(cuò)誤,而如果因?yàn)楸O(jiān)控SDK報(bào)錯(cuò),導(dǎo)致整個(gè)應(yīng)用主業(yè)務(wù)流程被中斷,這是我們不能夠接收的;

          實(shí)際上,我們無(wú)法保證我們的 SDK 不出現(xiàn)錯(cuò)誤,那么假如萬(wàn)一SDK本身報(bào)錯(cuò)了,我們就需要它不會(huì)去影響主業(yè)務(wù)流程的運(yùn)行;最簡(jiǎn)單粗暴的方法就是把整個(gè) SDK 都用 try catch 包裹起來(lái),那么這樣子即使出現(xiàn)了錯(cuò)誤,也會(huì)被攔截在我們的 catch 里面;

          但是我們回過(guò)頭來(lái)想一想,這樣簡(jiǎn)單粗暴的包裹,會(huì)帶來(lái)哪些問(wèn)題:

          • 我們只能獲取到一個(gè)報(bào)錯(cuò)的信息,但是我們無(wú)法得知報(bào)錯(cuò)的位置、插件;
          • 我們沒(méi)有將其上報(bào),我們無(wú)法感知到 SDK 產(chǎn)生了錯(cuò)誤
          • 我們沒(méi)法獲取 SDK 出錯(cuò)的一個(gè)環(huán)境數(shù)據(jù)

          那么,我們就需要一個(gè)相對(duì)優(yōu)雅的一個(gè)異常隔離+上報(bào)機(jī)制,回想我們上文的架構(gòu):內(nèi)核+插件的形式;我們對(duì)每一個(gè)插件模塊,都單獨(dú)的用trycatch包裹起來(lái),然后當(dāng)拋出錯(cuò)誤的時(shí)候,進(jìn)行數(shù)據(jù)的封裝、上報(bào);

          這樣子,就完成了一個(gè)異常隔離機(jī)制

          • 它實(shí)現(xiàn)了:當(dāng)SDK產(chǎn)生異常時(shí)不會(huì)影響主業(yè)務(wù)的流程;
          • 當(dāng)SDK產(chǎn)生異常時(shí)進(jìn)行數(shù)據(jù)的封裝、上報(bào);
          • 出現(xiàn)異常后,中止 SDK 的運(yùn)行,并移除所有的監(jiān)聽(tīng);

          SDK 如何實(shí)現(xiàn)服務(wù)端時(shí)間的校對(duì)?

          看到這里,可能有的同學(xué)并不明白,進(jìn)行服務(wù)端時(shí)間的校對(duì)是什么意思;我們首先要明白,我們通過(guò) JS 調(diào)用 new Date() 獲取的時(shí)間,是我們的機(jī)器時(shí)間;也就是說(shuō):這個(gè)時(shí)間是一個(gè)隨時(shí)都有可能不準(zhǔn)確的時(shí)間;

          那么既然時(shí)間是不準(zhǔn)確的,假如有一個(gè)對(duì)時(shí)間精準(zhǔn)度要求比較敏感的功能:比如說(shuō) API全鏈路監(jiān)控;最后整體繪制出來(lái)的全鏈路圖直接客戶端的訪問(wèn)時(shí)間點(diǎn)變成了未來(lái)的時(shí)間點(diǎn),直接時(shí)間穿梭那可不行;

          image.png

          如上圖,我們先要了解的是,http響應(yīng)頭 上有一個(gè)字段 Date;它的值是服務(wù)端發(fā)送資源時(shí)的服務(wù)器時(shí)間,我們可以在初始化SDK的時(shí)候,發(fā)送一個(gè)簡(jiǎn)單的請(qǐng)求給上報(bào)服務(wù)器,獲取返回的 Date 值后計(jì)算 Diff差值 存在本地;

          這樣子就可以提供一個(gè) 公共API,來(lái)提供一個(gè)時(shí)間校對(duì)的服務(wù),讓本地的時(shí)間 比較趨近于 服務(wù)端的真實(shí)時(shí)間;(只是比較趨近的原因是:還會(huì)有一個(gè)單程傳輸耗時(shí)的誤差

          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)然,這里還可以做的更精確一點(diǎn),我們可以讓后端服務(wù)在返回的時(shí)候,帶上 API 請(qǐng)求在后端服務(wù)執(zhí)行完畢所消耗的時(shí)間 server-timing,放在響應(yīng)頭里;我們?nèi)〉綌?shù)據(jù)后,將 ttfb 耗時(shí) 減去返回的 server-timing 再除以 2;就是單程傳輸?shù)暮臅r(shí);那這樣我們上文的計(jì)算中差的 單程傳輸耗時(shí)的誤差 就可以補(bǔ)上了;

          SDK 如何實(shí)現(xiàn)會(huì)話級(jí)別的錯(cuò)誤上報(bào)去重?

          首先,我們需要理清一個(gè)概念,我們可以認(rèn)為:

          • 在用戶的一次會(huì)話中,如果產(chǎn)生了同一個(gè)錯(cuò)誤,那么將這同一個(gè)錯(cuò)誤上報(bào)多次沒(méi)有意義的;
          • 在用戶的不同會(huì)話中,如果產(chǎn)生了同一個(gè)錯(cuò)誤,那么將不同會(huì)話中產(chǎn)生的錯(cuò)誤進(jìn)行上報(bào)有意義的;

          為什么有上面的結(jié)論呢?理由很簡(jiǎn)單:

          • 在用戶的同一次會(huì)話中,如果點(diǎn)擊一個(gè)按鈕出現(xiàn)了錯(cuò)誤,那么再次點(diǎn)擊同一個(gè)按鈕,必定會(huì)出現(xiàn)同一個(gè)錯(cuò)誤,而這出現(xiàn)的多次錯(cuò)誤,影響的是同一個(gè)用戶、同一次訪問(wèn);所以將其全部上報(bào)是沒(méi)有意義的;
          • 而在同一個(gè)用戶的不同會(huì)話中,如果出現(xiàn)了同一個(gè)錯(cuò)誤,那么這不同會(huì)話里的錯(cuò)誤進(jìn)行上報(bào)就顯得有意義了;

          所以說(shuō)我們?cè)诘谌恼?span style="font-weight: bold;color: #ffb11b;padding: 3px;">《一文摸清前端監(jiān)控實(shí)踐要點(diǎn)(三)錯(cuò)誤監(jiān)控》[5]中有一個(gè)生成 錯(cuò)誤mid 的操作,這是一個(gè)唯一id,但是它的唯一規(guī)則是針對(duì)于不同錯(cuò)誤的唯一;

          // 對(duì)每一個(gè)錯(cuò)誤詳情,生成一串編碼
          export const getErrorUid = (input: string) => {
            return window.btoa(unescape(encodeURIComponent(input)));
          };

          所以說(shuō)我們傳入的參數(shù),是 錯(cuò)誤信息、錯(cuò)誤行號(hào)、錯(cuò)誤列號(hào)、錯(cuò)誤文件等可能的關(guān)鍵信息的一個(gè)集合,這樣保證了產(chǎn)生在同一個(gè)地方的錯(cuò)誤,生成的 錯(cuò)誤mid 都是相等的;這樣子,我們才能在錯(cuò)誤上報(bào)的入口函數(shù)里,做上報(bào)去重;

          // 封裝錯(cuò)誤的上報(bào)入口,上報(bào)前,判斷錯(cuò)誤是否已經(jīng)發(fā)生過(guò)
          errorSendHandler = (data: ExceptionMetrics) => {
            // 統(tǒng)一加上 用戶行為追蹤 和 頁(yè)面基本信息
            const submitParams = {
              ...data,
              breadcrumbs: this.engineInstance.userInstance.breadcrumbs.get(),
              pageInformation: this.engineInstance.userInstance.metrics.get('page-information'),
            } as ExceptionMetrics;
            // 判斷同一個(gè)錯(cuò)誤在本次頁(yè)面訪問(wèn)中是否已經(jīng)發(fā)生過(guò);
            const hasSubmitStatus = this.submitErrorUids.includes(submitParams.errorUid);
            // 檢查一下錯(cuò)誤在本次頁(yè)面訪問(wèn)中,是否已經(jīng)產(chǎn)生過(guò)
            if (hasSubmitStatus) return;
            this.submitErrorUids.push(submitParams.errorUid);
            // 記錄后清除 breadcrumbs
            this.engineInstance.userInstance.breadcrumbs.clear();
            // 一般來(lái)說(shuō),有報(bào)錯(cuò)就立刻上報(bào);
            this.engineInstance.transportInstance.kernelTransportHandler(
              this.engineInstance.transportInstance.formatTransportData(transportCategory.ERROR, submitParams),
            );
          };

          SDK 采用什么樣的上報(bào)策略?

          對(duì)于上報(bào)方面來(lái)說(shuō),SDK的數(shù)據(jù)上報(bào)可不是隨隨便便就上報(bào)上去了,里面有涉及到數(shù)據(jù)上報(bào)的方式取舍以及上報(bào)時(shí)機(jī)的選擇等等,還有一些可以讓數(shù)據(jù)上報(bào)更加優(yōu)雅的優(yōu)化點(diǎn);

          首先,日志上報(bào)并不是應(yīng)用的主要功能邏輯,日志上報(bào)行為不應(yīng)該影響業(yè)務(wù)邏輯,不應(yīng)該占用業(yè)務(wù)計(jì)算資源;那么在往下閱讀之前,我們先來(lái)了解一下目前通用的幾個(gè)上報(bào)方式:

          • 信標(biāo)(Beacon API
          • Ajax(XMLHttpRequestfetch
          • Image(GIF、PNG

          我們來(lái)簡(jiǎn)單講一下上述的幾個(gè)上報(bào)方式

          首先 Beacon API[6] 是一個(gè)較新的 API

          • 它可以將數(shù)據(jù)以 POST 方法將少量數(shù)據(jù)發(fā)送到服務(wù)端
          • 它保證頁(yè)面卸載之前啟動(dòng)信標(biāo)請(qǐng)求
          • 并允許運(yùn)行完成且不會(huì)阻塞請(qǐng)求或阻塞處理用戶交互事件的任務(wù)。

          然后 Ajax 請(qǐng)求方式就不用我多說(shuō)了,大家應(yīng)該平常用的最多的異步請(qǐng)求就是 Ajax;

          最后來(lái)說(shuō)一下 Image 上報(bào)方式:我們可以以向服務(wù)端請(qǐng)求圖片資源的形式,像服務(wù)端傳輸少量數(shù)據(jù),這種方式不會(huì)造成跨域;

          上報(bào)方式

          看了上面的三種上報(bào)方式,我們最終采用 sendBeacon + xmlHttpRequest 降級(jí)上報(bào)的方式,當(dāng)瀏覽器不支持 sendBeacon 或者 傳輸?shù)臄?shù)據(jù)量超過(guò)了 sendBeacon 的限制,我們就降級(jí)采用 xmlHttpRequest 進(jìn)行上報(bào)數(shù)據(jù);

          優(yōu)先選用 Beacon API 的理由上文已經(jīng)有提到:它可以保證頁(yè)面卸載之前啟動(dòng)信標(biāo)請(qǐng)求,是一種數(shù)據(jù)可靠,傳輸異步并且不會(huì)影響下一頁(yè)面的加載 的傳輸方式。

          而降級(jí)使用 XMLHttpRequest 的原因是, Beacon API 現(xiàn)在并不是所有的瀏覽器都完全支持,我們需要一個(gè)保險(xiǎn)方案兜底,并且 sendbeacon 不能傳輸大數(shù)據(jù)量的信息,這個(gè)時(shí)候還是得回到 Ajax 來(lái);

          看到了這里,有的同學(xué)可能會(huì)問(wèn):為什么不用 Image?跨域怎么辦呀?原因也很簡(jiǎn)單:

          • Image 是以GET方式請(qǐng)求圖片資源的方式,將上報(bào)數(shù)據(jù)附在 URL 上攜帶到服務(wù)端,而URL地址的長(zhǎng)度是有一定限制的。規(guī)范對(duì) URL 長(zhǎng)度并沒(méi)有要求,但是瀏覽器、服務(wù)器、代理服務(wù)器都對(duì) URL 長(zhǎng)度有要求。有的瀏覽器要求URL中path部分不超過(guò) 2048這就導(dǎo)致有些請(qǐng)求會(huì)發(fā)送不完全。
          • 至于跨域問(wèn)題,作為接受數(shù)據(jù)上報(bào)的服務(wù)端,允許跨域是理所應(yīng)當(dāng)?shù)模?/section>

          我們將其簡(jiǎn)單封裝一下:

          export enum transportCategory {
            // PV訪問(wèn)數(shù)據(jù)
            PV = 'pv',
            // 性能數(shù)據(jù)
            PERF = 'perf',
            // api 請(qǐng)求數(shù)據(jù)
            API = 'api',
            // 報(bào)錯(cuò)數(shù)據(jù)
            ERROR = 'error',
            // 自定義行為
            CUS = 'custom',
          }

          export interface DimensionStructure {
            // 用戶id,存儲(chǔ)于cookie
            uid: string;
            // 會(huì)話id,存儲(chǔ)于cookiestorage
            sid: string;
            // 應(yīng)用id,使用方傳入
            pid: string;
            // 應(yīng)用版本號(hào)
            release: string;
            // 應(yīng)用環(huán)境
            environment: string;
          }

          export interface TransportStructure {
            // 上報(bào)類(lèi)別
            category: transportCategory;
            // 上報(bào)的維度信息
            dimension: DimensionStructure;
            // 上報(bào)對(duì)象(正文)
            context?: Object;
            // 上報(bào)對(duì)象數(shù)組
            contexts?: Array<Object>;
            // 捕獲的sdk版本信息,版本號(hào)等...
            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;
            };

            // 初始化上報(bào)方法
            initTransportHandler = () => {
              return typeof navigator.sendBeacon === 'function' ? this.beaconTransport() : this.xmlTransport();
            };

            // beacon 形式上報(bào)
            beaconTransport = (): Function => {
              const handler = (data: TransportStructure) => {
                const status = window.navigator.sendBeacon(this.options.transportUrl, JSON.stringify(data));
                // 如果數(shù)據(jù)量過(guò)大,則本次大數(shù)據(jù)量用 XMLHttpRequest 上報(bào)
                if (!status) this.xmlTransport().apply(this, data);
              };
              return handler;
            };

            // XMLHttpRequest 形式上報(bào)
            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;
            };
          }
          上報(bào)時(shí)機(jī)

          上報(bào)時(shí)機(jī)這里,一般來(lái)說(shuō):

          • PV、錯(cuò)誤、用戶自定義行為 都是觸發(fā)后立即就進(jìn)行上報(bào);
          • 性能數(shù)據(jù) 需要等待頁(yè)面加載完成、數(shù)據(jù)采集完畢后進(jìn)行上報(bào);
          • API請(qǐng)求數(shù)據(jù) 會(huì)進(jìn)行本地暫存,在數(shù)據(jù)量達(dá)到10條(自擬)時(shí)觸發(fā)一次上報(bào),并且在頁(yè)面可見(jiàn)性變化、以及頁(yè)面關(guān)閉之前進(jìn)行上報(bào);
          • 如果你還要上報(bào) 點(diǎn)擊行為 等其余的數(shù)據(jù),跟 API請(qǐng)求數(shù)據(jù) 一樣的上報(bào)時(shí)機(jī);
          上報(bào)優(yōu)化

          或許,我們想把我們的數(shù)據(jù)上報(bào)做的再優(yōu)雅一點(diǎn),那么我們還有什么可以優(yōu)化的點(diǎn)呢?還是有的:

          • 啟用 HTTP2,在 HTTP1 中,每次日志上報(bào)請(qǐng)求頭都攜帶了大量的重復(fù)數(shù)據(jù)導(dǎo)致性能浪費(fèi)。HTTP2頭部壓縮 采用Huffman Code壓縮請(qǐng)求頭,能有效減少請(qǐng)求頭的大??;
          • 服務(wù)端可以返回 204 狀態(tài)碼,省去響應(yīng)體;
          • 使用 `requestIdleCallback`[7] ,這是一個(gè)較新的 API,它可以插入一個(gè)函數(shù),這個(gè)函數(shù)將在瀏覽器空閑時(shí)期被調(diào)用。這使開(kāi)發(fā)者能夠在主事件循環(huán)上執(zhí)行后臺(tái)和低優(yōu)先級(jí)工作,而不會(huì)影響延遲關(guān)鍵事件,如果不支持的話,就使用 settimeout

          平臺(tái)數(shù)據(jù)如何進(jìn)行 削峰限流?

          假設(shè)說(shuō),有某一個(gè)時(shí)間點(diǎn),突然間流量爆炸,無(wú)數(shù)的數(shù)據(jù)向服務(wù)器訪問(wèn)過(guò)來(lái),這時(shí)如果沒(méi)有一個(gè)削峰限流的策略,很可能會(huì)導(dǎo)致機(jī)器Down掉,

          所以說(shuō)我們有必要去做一個(gè)削峰限流,從概率學(xué)的角度上講,在大數(shù)據(jù)量的基礎(chǔ)上我們對(duì)于整體數(shù)據(jù)做一個(gè)百分比的截?cái)?,并不?huì)影響整體的一個(gè)數(shù)據(jù)比例。

          簡(jiǎn)單方案-隨機(jī)丟棄策略進(jìn)行限流

          前端做削峰限流最簡(jiǎn)單的方法是什么?沒(méi)錯(cuò),就是 Math.random() ,我們讓用戶傳入一個(gè)采樣率,

          if(Math.random()<0.5return;

          非常簡(jiǎn)單的就實(shí)現(xiàn)了!但是這個(gè)方案不是一個(gè)很優(yōu)雅的解決辦法,為什么呢?

          • 大流量項(xiàng)目限制了 50% 的流量,它的流量仍然多;
          • 小流量項(xiàng)目限制了 50% 的流量,那就沒(méi)有流量了;
          優(yōu)化方案-流量整型

          現(xiàn)在做流量整形的方法很多,最常見(jiàn)的就是三種:

          • 計(jì)數(shù)器算法:計(jì)數(shù)器算法就是單位時(shí)間內(nèi)入庫(kù)數(shù)量固定,后面的數(shù)據(jù)全部丟棄;缺點(diǎn)是無(wú)法應(yīng)對(duì)惡意用戶;
          • 漏桶算法:漏桶算法就是系統(tǒng)以固有的速率處理請(qǐng)求,當(dāng)請(qǐng)求太多超過(guò)了桶的容量時(shí),請(qǐng)求就會(huì)被丟棄;缺點(diǎn)是漏桶算法對(duì)于驟增的流量來(lái)說(shuō)缺乏效率;
          • 令牌桶算法:令牌桶算法就是系統(tǒng)會(huì)以恒定的速度往固定容量的桶里放入令牌,當(dāng)請(qǐng)求需要被處理時(shí)就會(huì)從桶里取一個(gè)令牌,當(dāng)沒(méi)有令牌可取的時(shí)候就會(huì)據(jù)拒絕服務(wù);

          對(duì)于上述三種限流方案的文章很多,我這里就不細(xì)展開(kāi)描述,有興趣的同學(xué)自己去找一下資料閱讀;

          我們先來(lái)分析一下:

          • 計(jì)數(shù)器能夠削峰限制最大并發(fā)數(shù)以保證服務(wù)高可用
          • 令牌桶實(shí)現(xiàn)流量均勻入庫(kù),保證下游服務(wù)健康

          最后我們團(tuán)隊(duì)在上述的方案選擇中,最終選擇了 計(jì)數(shù)器 + 令牌桶 的方案;這也是參考了 前端早早聊 李振:如何從 0 到 1 建設(shè)前端性能監(jiān)控系統(tǒng)[8] 的限流方案分享;

          image.png
          • 首先從外部來(lái)的流量是我們無(wú)法預(yù)估的,假設(shè)如上圖我們有三個(gè) 服務(wù)器Pod ,如果總流量來(lái)的非常大,那么這時(shí)我們通過(guò)計(jì)數(shù)器算法,給它設(shè)置一個(gè)很大的最大值;這個(gè)最大值只防小人不防君子,可能 99% 的項(xiàng)目都不會(huì)觸發(fā);
          • 這樣經(jīng)過(guò)總流量的計(jì)數(shù)器削峰后,再到中心化的令牌桶限流:通過(guò) redis 來(lái)實(shí)現(xiàn),我們先做一個(gè)定時(shí)器每分鐘都去令牌桶里寫(xiě)令牌,然后單機(jī)的流量每個(gè)進(jìn)來(lái)后,都去 redis 里取令牌,有令牌就處理入庫(kù);沒(méi)有令牌就把流量拋棄;
          • 這樣子我們就實(shí)現(xiàn)了一個(gè) 單機(jī)的削峰 + 中心化的限流,兩者一結(jié)合,就是解決了小流量應(yīng)用限流后沒(méi)流量的問(wèn)題,以及控制了入庫(kù)的數(shù)量均勻且穩(wěn)定;

          平臺(tái)數(shù)據(jù)為什么需要 數(shù)據(jù)加工?

          那么,為什么需要數(shù)據(jù)加工,以及數(shù)據(jù)加工需要做什么處理?

          當(dāng)我們的數(shù)據(jù)上報(bào)之后,因?yàn)?IP地址 是在服務(wù)端獲取的嘛,所以服務(wù)端就需要有一個(gè)服務(wù),去統(tǒng)一給請(qǐng)求數(shù)據(jù)中家加上 IP地址 以及 IP地址 解析后的歸屬地、運(yùn)營(yíng)商等信息;

          根據(jù)業(yè)務(wù)需要,還可以加上服務(wù)端服務(wù)版本號(hào) 等其余信息,方便后續(xù)做追蹤;

          這里就不展開(kāi)描述~

          平臺(tái)數(shù)據(jù)為什么需要 數(shù)據(jù)清洗、聚合?

          在一開(kāi)始的整體架構(gòu)設(shè)計(jì)中已經(jīng)說(shuō)明:

          • 數(shù)據(jù)清洗是為了白名單、黑名單過(guò)濾等的業(yè)務(wù)需要,還有避免已關(guān)閉的應(yīng)用數(shù)據(jù)繼續(xù)入庫(kù);
          • 數(shù)據(jù)聚合是為了將相同信息的數(shù)據(jù)進(jìn)行抽象聚合issue,以便查詢和追蹤;

          這樣子假設(shè)后續(xù)我們需要在數(shù)據(jù)庫(kù)查詢:某一條錯(cuò)誤,產(chǎn)生了幾次,影響了幾個(gè)人,錯(cuò)誤率是多少,這樣子可以不用再去 ES 中撈日志,而是在 MySQL 中直接查詢即可;

          并且,我們還可以將抽象聚合出來(lái)的 issue ,關(guān)聯(lián)于公司的 缺陷平臺(tái)(類(lèi)bug管理平臺(tái)) ,實(shí)現(xiàn) issue追蹤直接自動(dòng)貼bug到負(fù)責(zé)人頭上 等業(yè)務(wù)功能;

          平臺(tái)數(shù)據(jù)如何進(jìn)行 多維度追蹤?

          首先我們會(huì)對(duì)每一個(gè)用戶(user),會(huì)去生成一個(gè) 用戶id(uid ;并對(duì)每一次會(huì)話(session),生成一個(gè) 會(huì)話id(sid) ;

          uidsid 都是28位的隨機(jī)ID,siduid 都在初始化時(shí)生成,不同的是,因?yàn)?uid 的生命周期只在一次會(huì)話之中(關(guān)閉頁(yè)簽之前),所以 sid 我們存放在 sessionStorage 中,而 uid 我們存放在 cookie 里,過(guò)期時(shí)間設(shè)置六個(gè)月

          每次SDK初始化時(shí),都先去 cookiesessionStorage 里取 uidsid,如果取不到就重新生成一份;并且在每次數(shù)據(jù)上報(bào)時(shí),都將這些 id 附帶上去;

          你如果有需要,還可以再搞一個(gè)登錄id,由使用方傳入,專(zhuān)門(mén)存放登錄成功后的登錄態(tài)ID;

          這樣一系列搞完之后,我們?cè)?code style="font-size: 14px;word-wrap: break-word;border-radius: 4px;font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;color: #9b6e23;background-color: #fff5e3;padding: 3px;margin: 3px;">第二篇文章《一文摸清前端監(jiān)控實(shí)踐要點(diǎn)(二)行為監(jiān)控》[9]中收集了很多的行為數(shù)據(jù),包括PV訪問(wèn)、路由跳轉(zhuǎn)、http請(qǐng)求click事件、自定義事件、甚至第三章的錯(cuò)誤數(shù)據(jù)等等;這些種種零零散散的數(shù)據(jù)就可以被串聯(lián)起來(lái),得到新的分析價(jià)值;

          因?yàn)?cookie 有極小的可能性被用戶手動(dòng)禁用,這種情況下 uidnull 就可以了

          代碼錯(cuò)誤如何進(jìn)行 源碼映射?

          第三篇文章中,我們通過(guò)解析錯(cuò)誤堆棧,得到了錯(cuò)誤的文件、行列號(hào)等信息,可以通過(guò)對(duì) sourcemap 可以對(duì)源碼進(jìn)行映射,定位錯(cuò)誤源碼的位置;

          大家可以跳轉(zhuǎn)閱讀相應(yīng)的代碼:一文摸清前端監(jiān)控自研實(shí)踐(三)錯(cuò)誤監(jiān)控 \- Source Map[10]

          當(dāng)然需要注意的是,在生產(chǎn)環(huán)境我們是不可以將 sourcemap 文件發(fā)布上線的,我們可以通過(guò)手動(dòng)上傳到監(jiān)控平臺(tái)的形式去進(jìn)行錯(cuò)誤的分析定位;

          如何設(shè)計(jì)監(jiān)控告警的維度?

          首先,監(jiān)控告警不是一個(gè)易事,在什么情況下,我們需要進(jìn)行告警的推送?

          我們先來(lái)了解兩個(gè)概念:宏觀告警微觀告警;

          key宏觀告警微觀告警
          告警依據(jù)是否超出了閾值?是否有產(chǎn)生新的異常?
          關(guān)鍵指標(biāo)數(shù)量、比率單個(gè)異常
          比對(duì)方法時(shí)間區(qū)間內(nèi)的 異常數(shù)量、異常比率新增的異常且異常uid未解決
          • 宏觀告警 更加關(guān)注的是:一段時(shí)間區(qū)間內(nèi),新增異常的數(shù)量、比率是否超過(guò)了閾值;如果超過(guò)了那就進(jìn)行告警;
          • 微觀告警 更加關(guān)注的是:是否有新增的、且未解決的異常;

          我們團(tuán)隊(duì)這邊目前做的都是微觀告警;只要出現(xiàn)的新異常,它的 uid 是當(dāng)前已激活的異常中全新的一個(gè);那么就進(jìn)行告警,通知大群通知負(fù)責(zé)人、在缺陷平臺(tái)上新建 bug 指派給負(fù)責(zé)人

          監(jiān)控告警如何指派給代碼提交者?

          如上文提到,我們當(dāng)發(fā)現(xiàn)新 bug 產(chǎn)生時(shí),我們可以將這個(gè) bug 指派給負(fù)責(zé)人;這里其實(shí)還可以做的更細(xì)致一點(diǎn),我們可以做一個(gè) 處理人自動(dòng)分配 的機(jī)制;

          處理人自動(dòng)分配,分配給誰(shuí)呢?還記得我們?cè)?code style="font-size: 14px;word-wrap: break-word;border-radius: 4px;font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;color: #9b6e23;background-color: #fff5e3;padding: 3px;margin: 3px;">第三篇錯(cuò)誤監(jiān)控中,捕獲錯(cuò)誤時(shí)上報(bào)了錯(cuò)誤的位置,也就是源碼所在;那么我們只需要找到最近一次提交這行代碼的人就可以了;

          Git Blame

          那么找出 出錯(cuò)行author 的原理其實(shí)就是 Git Blame ;這方面的文檔很多,不了解的同學(xué)可以看一下 Git Blame[11];

          image.png

          看上圖,指令其實(shí)很簡(jiǎn)單,

          // git blame -L <n,m> <file>
          // n是起始行,m是結(jié)束行,file是指定文件
          // eg:
          git blame -L 2,2 LICENSE

          查詢返回的結(jié)構(gòu)是:

          commitID (代碼提交作者 提交時(shí)間 代碼位于文件中的行數(shù)) 實(shí)際代碼

          這樣子,我們就可以獲取到具體的提交記錄是哪次,并且提交者是誰(shuí);

          利用 Gitlab Open-api 在服務(wù)端集成
          image.png

          gitlab 文檔[12] 中,詳細(xì)說(shuō)明了API的使用和參數(shù)方法;我們只需傳入 range[start]range[end] ,還有具體的 分支文件名 ;我們就可以像下面這個(gè)官方給出的例子一樣調(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)控實(shí)踐要點(diǎn)(一)性能監(jiān)控[13]

          一文摸清前端監(jiān)控實(shí)踐要點(diǎn)(二)行為監(jiān)控[14]

          一文摸清前端監(jiān)控實(shí)踐要點(diǎn)(三)錯(cuò)誤監(jiān)控[15]

          前端早早聊 李振:如何從 0 到 1 建設(shè)前端性能監(jiān)控系統(tǒng)[16]

          Git Blame[17]

          Gitlab 查詢 Blame[18]

          參考資料

          [1]

          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)于本文:

          來(lái)源:菜貓子neko

          https://juejin.cn/post/7108660942686126093


          最后


          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對(duì)你有幫助,在看」是最大的支持
           》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持


          瀏覽 29
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美操逼視频在线观看 | 午夜一级毛片 | 蜜桃成人无码AV在线观看一电影 | 干屄 | 无码在线免费看 |