成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

字節的前端監控 SDK 是怎樣設計的

原創 精選
開發 前端
一個 SDK 不可能既支持多環境,又滿足體積小、功能全面的要求,這本身互相矛盾。只要兼容其他環境,打包進來的代碼會導致體積變大,因此設計之初的目標就是同一套設計組裝成不同的 SDK 。
作者|彭莉,火山引擎 APM 研發工程師,2020 年加入字節,負責前端監控 SDK 的開發維護、平臺數據消費的探索和落地

摘要

公司內部監控環境多樣( Web 應用、小程序、Electron 應用、跨端應用等等), SDK 如何保證底層邏輯的復用、上層邏輯的解耦。

在業務龐雜、監控需求多樣的背景下, SDK 如何做到足夠靈活,如何實現插件化,并且支持業務自行擴展的。

大型 C 端業務非常注重業務自身的正確性和性能,監控 SDK 如何保證原有業務的正確性;如何保持 SDK 自身的性能,減少對業務的影響。

接入業務眾多,上報量級近千萬 QPS ,在日常需求迭代中, SDK 是如何確保自身穩定性的。

邏輯解耦

前端的領域廣闊,所以作為前端監控,也不只局限在瀏覽器環境,需要同時解決小程序、 Electron 、 Nodejs 等等其他環境的監控需求。不同環境之間差異巨大,從提供的配置項,到監控的功能、上報的方式都會不一樣。

一個 SDK 不可能既支持多環境,又滿足體積小、功能全面的要求,這本身互相矛盾。只要兼容其他環境,打包進來的代碼會導致體積變大,因此設計之初的目標就是同一套設計組裝成不同的 SDK 。此設計的第一要務是要邏輯解耦。雖然多環境下差異很大,但要做的事情是一樣的,比如配置、采集數據、組裝數據、上報數據。

我們設計了五個角色,每個角色只需要實現約定的接口即可。這樣就保證了不同的環境下,各個角色合作的方式是相同的,在實現了一套內核模版后,不同的監控 SDK 就可以快速搭建出來。

圖片

Monitor

收集器,主動或被動地采集特定環境下的原始數據,組裝為平臺無關事件。

Monitor 有若干個,每一個 Monitor 對應一個功能,比如關于 JS 錯誤的監控是一個 Monitor ,關于請求的監控又是另一個 Monitor 。

Builder

組裝器,負責將收集器上報的平臺無關事件轉換為特定平臺的上報格式。

主要負責包裝特定環境下的上下文信息。在瀏覽器環境下,上下文信息包括頁面地址、網絡狀態、當前時間等等,再結合收到的 Monitor 的數據,完成上報格式的組裝。

Sender

發送器,負責發送邏輯,比如批量,重試等功能。

監控 SDK 的 Sender 都是 BatchSender ,它會負責維護一個緩存隊列,按照一定的隊列長度或者緩存時間間隔來聚合上報數據,會開放一些方法自定義緩存隊列長度和緩存間隔時間,也支持立即上報和清空隊列等操作。

特定環境下的 Sender 也需要負責處理一些邊緣 case ,比如瀏覽器環境下的 Sender 在頁面關閉時,需要使用 sendBeacon 立即上報所有隊列數據,以免漏報。

在實際實踐中,我們對 Sender 進行了進一步抽象, Sender 不會內置發送的能力,關于如何發送數據,不同環境依賴的 API 不同,因此會由 Client 在創建 Sender 時將具體的發送能力傳入 Sender 中。

ConfigManager

配置管理器,負責配置邏輯,比如合并初始配置和用戶配置、拉取遠端配置等功能。

一般需要傳入默認配置,支持用戶手動配置,當配置完成時, ConfigManager 會變更 ready 狀態,所以它也支持被訂閱,以便當 ready 時或者配置變更時通知到訂閱方。

export interface ConfigManager<Config> {
setConfig: (c: Partial<Config>) => Config
getConfig: () => Config
onChange: (fn: () => void) => void
onReady: (fn: () => void) => void
}

Client

實例主體,負責串聯配置管理器、收集器、組裝器和發送器,串通整個流程,同時提供生命周期監聽以供擴展 SDK 功能。

下面是一段方便理解串聯過程的偽代碼,僅作參考。

export const createClient = ({ configManager, builder, sender }) => {
let inited = false
let started = false
let preStartQueue = []
const client = {
init: (config) => {
configManager.setConfig(config)
configManager.onReady(() => {
preStartQueue.forEach((e) => { this.report(e) })
started = true
})
inited = true
}
report: (data) => {
if (!started) {
preStartQueue.push(data)
} else {
const builderData = builder.build(data)
builderData && sender.send(builderData)
}
}
}
return client
}

const client = createClient({ configManager, builder, sender })
monitors.forEach((e) => { e(client) })

角色之間足夠抽象,互相獨立、各司其職。比如 Monitor 只負責收集,并不知道最終上報的具體格式;Builder 只做組裝,組裝完成后交給實例主體 Client ,由 Client 交給 Sender ;Sender 不知道收到的具體事件格式,只負責完成發送。

開放豐富的生命周期

監控做的事情就像一條單純的流水線:初始化 => 采集數據 => 組裝數據 => 上報數據,我們希望能在不同階段執行各種操作,但又不希望直接將邏輯耦合在代碼,這樣不利于后期的迭代維護,也會導致體積一步步增加,走向重構的必然結果。

于是我們決定讓內核模版提供規范的生命周期,所有的功能都借助生命周期的監聽來實現,這樣不僅解決了體積不斷膨脹的問題,也讓 SDK 易于擴展。

基于監控 SDK 的各個階段,我們明確了六個主要的生命周期,命名也比較貼切,從上到下分別是:初始化 => 開啟上報 =>  Monitor 監控到數據,傳遞給 Client  => 包裝數據 => 發送數據 => 銷毀實例

圖片

基于這些生命周期,我們提供了十個生命周期鉤子,主要分為兩類:

  • 回調類:只執行回調,不影響流程繼續執行,比如 init / start / beforeConfig / config 等等。
  • 處理類:執行并返回修改后的有效值,如果返回無效值,將不再往下執行,終止上報,比如 report / beforeBuild / build / beforeSend 等等。

如何實現插件化

良好的生命周期是插件化的基礎, 基于這些生命周期我們就能實現各種各樣的插件。

舉個例子,我們需要為 Monitor 采集到的數據包裝事件發生時的上下文,可以通過這種方式:監聽 report ,劫持到數據,重新包裝,再傳遞給 Client 。

// 一個包裝上下文的插件
export const InjectEnvPlugin = (client: WebClient) => {
client('on', 'report', (ev: WebReportEvent) => {
return addEnvToSendEvent(ev)
})
}

// 應用此插件
InjectEnvPlugin(client)

再舉個例子,我們需要新監控一類數據,可以通過這種方式:監聽實例主體 Client 當前的狀態,在 Client ready 的時候(用戶配置完成時),開始收集數據。在收集到數據時,將數據傳回 Client 即可。

// 一個監聽數據的插件
export const MonitorXXPlugin = (client: WebClient) => {
client('on', 'init', () => {
const data = listenXX();
client('report', data)
})
}

在 SDK 內,  基本都是插件,常規的數據采集是一個個插件,其他的比如采樣、包裝上下文、異步加載等功能,也都是各自獨立的插件。

業務如何自行擴展

簡單的擴展,一般可以靠生命周期鉤子函數來完成,常見的需求就是在數據發送前做一些手動的過濾、安全脫敏等等。

舉個例子,我們想要在頁面地址包含  '/test'  時不上報任何數據,可以通過下面的代碼來實現。

import client from '@slardar/web'

client('on', 'beforeSend', (ev) => {
if (ev.common.url.includes('/test')) {
return false
}
return ev
})

但如果有高階的需求,比如想寫一個插件能提供給團隊的其他人用,上面的方式就不再適用。如果插件太復雜,其他人需要復制一大段代碼,用起來不太優雅。

基于這個需求, SDK 設計了一個自定義插件的傳遞協議,可以在初始化時將自定義插件傳遞給 Client , Client 將會在初始化時執行傳入的 setup 方法,在實例銷毀時執行傳入的 tearDown 方法來銷毀副作用。

export interface Integration<T extends AnyClient> {
name: string
setup: (client: T) => void
tearDown?: () => void
}

可以注意到,接口約定的實例類型是 AnyClient ,這個協議并不在意是什么類型的 Client ,實際的 Client 類型由 SDK 來定義,比如 Web SDK 拿到的是 WebClient , Electron SDK 拿到的是 ElectronClient 。

業務可以自行發布一個插件包,插件的實現可以是直接返回一個對象,或一個方法。允許用戶傳入一些配置,返回一個對象,只要這個對象滿足上面的 Integration 類型即可。

import client from '@slardar/web'
import CustomPlugin from 'xxx'

client('init', {
...
integrations: [CustomPlugin({ config: {} })]
...
})

如何按需加載

為了方便使用,默認情況下,我們會集成所有的監控功能。但這并不是所有業務都需要的,有的業務只關心 JS 錯誤,其他的功能都不想要,這應該怎么解決呢?

為此 SDK 導出了一個最小的實例,這個實例只引入通用的插件,但是不引入數據采集類的插件,而具體要采集哪些功能由用戶在 integrations 上按需配置。

import { createMinimalBrowserClient } from '@slardar/web'
import { jsErrorPlugin } from '@slardar/integrations/dist/jsError'

// 創建一個最小的實例
const client = createMinimalBrowserClient()

client('init',{
...
// 按需引入需要采集的監控功能
integrations: [jsErrorPlugin()],
...
})

如何保證原有業務的正確性

接入監控 SDK 的目的是為了發現問題,如果監控 SDK 的問題導致業務受到了影響,不免本末倒置。加上絕大部分前端業務都接入了這個 SDK ,如果出現問題,影響范圍和損失都很巨大。因此保證原有業務的正確性遠遠比監控本身更重要。

SDK 會首先將對業務有影響的 敏感代碼 使用 try catch 包裹起來,確保即使發生了錯誤也不影響業務,比如 hook 類的操作, hook XHR 和 Fetch 等等。這個操作要膽大心細,同時 try catch 的范圍能小則小。

其次是監控 SDK 自身的錯誤。我們也會將 SDK 自身的 關鍵代碼 包裹 try catch ,確保一個錯誤不會影響整個監控流程。單純的 try catch 將錯誤吞掉解決不了問題,這些錯誤可能導致某些監控數據沒有收集完全,影響監控的完整性。因此 SDK 實現了一個 ObserveSelfErrorPlugin ,用于收集 SDK 自身的錯誤并上報。

同時,我們會針對上報所有的上報數據進行清洗,帶有 SDK 自身堆棧的數據會統一消費一份到另一處,便于從宏觀上觀察 SDK 的出錯情況,及時發現問題。

這樣既確保了業務的正確性,也確保了監控 SDK 的正確性。

如何減少對業務的影響

絕大部分的業務都是使用監控 SDK 來自動上報性能數據以此來監控業務的性能,這也隱含著對監控 SDK 最基本的要求:不能帶來性能問題。

最重要的就是不能影響業務的首屏渲染,為此我們把 Monitor 類的插件分為兩類,一是需要立即監聽的,先加載;二是不需要的立即監聽的,延后加載。比如路由變化的監聽、請求的監聽,如果延后會導致數據遺漏,就屬于第一類;像靜態資源性能監控這樣晚一點執行也并不會遺漏的,就屬于第二類。

除此之外, SDK 本身的性能評估也非常重要。單個插件的執行耗時多少,插件帶來的副作用的耗時又是多少,這些都是基本的評估點。基于Maiev,我們編寫了完善的 Benchmark 性能測試,在代碼 MR 的時候會觸發相應的測試任務,另外也有固定周期來定時執行測試任務,任務異常時不能發版, SDK 的性能由此保證。

當然盡可能縮小 SDK 的體積也能直接減少對業務的影響,這塊內容涉及較廣,留作后續分說。

如何盡早開始監聽

監聽不遺漏的前提是事件發生在開始監控之后。但是一些超高優的事件,比如 JS 錯誤,發生時機可能超級靠前,等不到監控腳本加載完成。所以監控 SDK 針對 script 的接入方式會提供一個簡短的腳本,讓用戶內聯在頁面中。它的作用是提前開始監聽,保證高優的事件不被遺漏。

它還有另一個巧用:緩存調用命令。

監控腳本是異步加載的,因此會先掛載一個空函數,確保調用不報錯;同時把對實例主體 Client 的調用命令緩存下來,記錄下調用的時間和頁面地址,確保能正確組裝數據;等到監控腳本加載完成時再順序執行,以此確保調用不遺漏。示例如下:

window[globalName] = function (m) {
const onceArguments = [].slice.call(arguments)
onceArguments.push(Date.now(), location.href)
;window[globalName].precolletArguments.push(onceArguments)
}

window[globalName].precolletArguments = []

當然如果使用npm包接入的話,依然會有預收集的邏輯,因為npm包不會掛全局變量,所以邏輯稍微有一些不同,同時受限于引入的順序,執行的時機會稍晚一些。

如何保證 SDK 的質量

Slardar Web SDK 為絕大部分公司前端業務提供監控能力,上報數據的流量近千萬 QPS ,需要有嚴格的質量把控。

SDK 有完善的單元測試,每一個插件,每一個方法,都會單獨編寫測試用例。以及完善的自動化測試,對于整個 SDK 的所有默認行為以及各個配置項對應的行為有完整的用例覆蓋。每次變動都需要補充對應的相關用例,且每次 MR 都要測試通過才能合入預發布分支,這樣才能做到心中不慌。此外,會有預發布驗證環節,驗證改動的預期效果。如果改動的地方比較敏感,會找站點合作方灰度一段時間后發布正式版本。發布后的一段時間內我們也會密切的關注整體的流量情況,確認是否存在異常上漲和下降,是否有新增的 SDK 相關異常。

由此, SDK 的質量得以保證。

責任編輯:未麗燕 來源: 字節跳動技術團隊
相關推薦

2024-08-30 08:59:15

2020-04-21 12:09:47

JVM消化字節碼

2017-09-12 10:50:55

前端SDK開發

2022-01-11 14:25:46

前端監控SDK

2009-10-19 15:01:22

機房監控摩卡

2010-03-24 15:40:39

網管運維管理摩卡軟件

2020-10-27 07:34:41

基站手機蜂窩網絡

2015-06-30 11:52:30

2023-11-07 07:21:04

2020-01-02 15:01:27

NginxApache服務器

2022-10-28 13:41:51

字節SDK監控

2019-03-18 10:02:16

緩存更新數據

2023-03-03 11:12:34

Kubernetes控制器后端

2015-11-10 09:09:23

代碼程序員成長

2024-03-28 08:13:51

GPTsOpenAI人工智能

2011-11-25 09:48:04

天線無線

2013-08-19 16:17:48

CIO

2009-09-02 20:18:17

域名劫持域名安全

2015-09-06 09:09:13

2014-06-20 10:34:42

開源
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 久久99网 | 伊人伊成久久人综合网站 | 欧美日韩最新 | 国产在线观看一区二区三区 | 精品一区二区三区不卡 | 欧洲尺码日本国产精品 | 一区二区精品 | 日本大片在线播放 | 国产美女一区二区三区 | 鸳鸯谱在线观看高清 | 亚洲精品久久 | 久久影院一区 | 欧美一区二区 | 日本一区二区三区在线观看 | 911精品美国片911久久久 | 亚洲综合国产 | 秋霞在线一区 | 日韩av成人 | 永久网站| 中文字幕欧美一区二区 | xx性欧美肥妇精品久久久久久 | 日韩欧美精品在线播放 | 成人妇女免费播放久久久 | 超碰操 | av片在线观看 | 国产乱码精品1区2区3区 | 久久精选 | 日韩免费中文字幕 | 欧美1区2区 | 黄网站在线观看 | 国产成人免费视频网站高清观看视频 | 久久久久久久久久一区 | 精品一区二区三区av | 成人三级在线观看 | 一区二区三区免费 | 亚洲在线视频 | 日韩www | 午夜天堂精品久久久久 | 在线免费av电影 | 日韩最新网站 | 久久香焦|