Sentry 開發(fā)者貢獻(xiàn)指南 - SDK 開發(fā)(性能監(jiān)控:Sentry SDK API 演進(jìn))
本文檔的目標(biāo)是將 Sentry SDK 中性能監(jiān)控功能的演變置于上下文中。我們首先總結(jié)了如何將性能監(jiān)控添加到 Sentry 和 SDK, 然后我們討論 identified issues(已確定的問題) 吸取的經(jīng)驗(yàn)教訓(xùn)以及解決這些問題的舉措。
介紹
早在 2019 年初,Sentry 就開始嘗試向 SDK 添加跟蹤功能。 Python 和 JavaScript SDK 是設(shè)計(jì)和開發(fā)第一個(gè)概念的測(cè)試平臺(tái)。概念驗(yàn)證于 2019 年 4 月 29 日 發(fā)布, 并于 2019 年 5 月 7 日交付給 Sentry。 Python 和 JavaScript 是顯而易見的選擇,因?yàn)樗鼈冊(cè)试S我們?cè)囼?yàn)檢測(cè) Sentry 自己的后端和前端。
- https://github.com/getsentry/sentry-python/pull/342
- https://github.com/getsentry/sentry-javascript/pull/1918
- https://github.com/getsentry/sentry-python/releases/tag/0.7.13
- https://github.com/getsentry/sentry/pull/12952
請(qǐng)注意,上述工作與 OpenCensus 和 OpenTracing 合并形成 OpenTelemetry 是同時(shí)代的。 Sentry 的 API 和 SDK 實(shí)現(xiàn)借鑒了 OpenTelemetry 1.0 之前版本的靈感,并結(jié)合了我們自己的想法。例如,我們的 Span 狀態(tài)列表與 2019 年底左右在 OpenTelemetry 規(guī)范中可以找到的匹配。
- https://medium.com/opentracing/a-roadmap-to-convergence-b074e5815289
- https://github.com/getsentry/relay/blob/55127c75d4eeebf787848a05a12150ee5c59acd9/relay-common/src/constants.rs#L179-L181
使用 API 后,性能監(jiān)控支持隨后擴(kuò)展到其他 SDK。Sentry 的性能監(jiān)控 解決方案于 2020 年 7 月普遍可用。 OpenTelemetry 的跟蹤規(guī)范 1.0 版于 2021 年 2 月發(fā)布。
- https://blog.sentry.io/2020/07/14/see-slow-faster-with-performance-monitoring
- https://medium.com/opentelemetry/opentelemetry-specification-v1-0-0-tracing-edition-72dd08936978
我們最初的實(shí)現(xiàn)重用了我們現(xiàn)有的錯(cuò)誤報(bào)告機(jī)制:
- Event type 擴(kuò)展了新字段。這意味著我們可以節(jié)省時(shí)間并快速開始向 Sentry 發(fā)送事件,而不是設(shè)計(jì)和實(shí)現(xiàn)全新的攝取管道,這一次,不是 error,而是一種新的 transaction 事件類型。
- https://develop.sentry.dev/sdk/event-payloads/
- 由于我們只是發(fā)送一種新型事件,因此也重用了 SDK 傳輸層。
- 由于我們共享攝取管道(ingestion pipeline),這意味著我們共享存儲(chǔ)以及發(fā)生在所有事件上的處理的許多部分。
我們的實(shí)現(xiàn)演變成明確強(qiáng)調(diào) Transaction 和 Span 之間的區(qū)別。部分原因是重用 Event 接口的副作用。
Transaction 與客戶產(chǎn)生了良好的共鳴。他們?cè)试S突出顯示代碼中的重要工作塊,例如瀏覽器頁面加載或 http 服務(wù)器請(qǐng)求。客戶可以查看和瀏覽 transaction 列表,而在 transaction 中,span 為更細(xì)粒度的工作單元提供詳細(xì)的時(shí)間安排。
在下一節(jié)中,我們將討論當(dāng)前模型的一些缺點(diǎn)。
已確定的問題
雖然統(tǒng)一 SDK 架構(gòu)(hub、client、scope) 和 transaction ingestion 模型的重用有其優(yōu)點(diǎn),但經(jīng)驗(yàn)揭示了一些我們將其分為兩類的問題。
- https://develop.sentry.dev/sdk/unified-api/
第一組與 scope 傳播有關(guān),本質(zhì)上是確定 當(dāng)前 scope 是什么的能力。用戶代碼中的手動(dòng)檢測(cè)以及 SDK 集成中的自動(dòng)檢測(cè)都需要此操作。
第二組是與用于將 transaction 數(shù)據(jù)從 SDK 發(fā)送到 Sentry 的 wire 格式相關(guān)的問題。
Scope 傳播
該問題由 getsentry/sentry-javascript#3751 跟蹤。
- https://github.com/getsentry/sentry-javascript/issues/3751
Unified SDK 架構(gòu) 基本上是基于每個(gè)并發(fā)單元存在一個(gè) hub,每個(gè) hub 有一堆 client 和 scope 對(duì)。
- https://develop.sentry.dev/sdk/unified-api/
Client 保存配置并負(fù)責(zé)通過 transport 向 Sentry 發(fā)送數(shù)據(jù),而 scope 保存附加到傳出事件(例如 tag 和 breadcrumb)的上下文數(shù)據(jù)。
每個(gè) hub 都知道當(dāng)前的 scope 是什么。它始終是堆棧頂部的 scope。困難的部分是 “per unit of concurrency(每單位并發(fā))” 有一個(gè) hub。
例如,JavaScript 是具有事件循環(huán)和異步代碼執(zhí)行的單線程。沒有標(biāo)準(zhǔn)的方法來承載跨異步調(diào)用工作的上下文數(shù)據(jù)。因此,對(duì)于 JavaScript 瀏覽器應(yīng)用程序,只有一個(gè)全局 hub 共享用于同步和異步代碼。
類似的情況出現(xiàn)在 Mobile SDK 上。用戶期望上下文數(shù)據(jù)(例如 tags、current user 是什么、 breadcrumbs 以及存儲(chǔ)在 scope 上的其他信息)可以從任何線程獲得和設(shè)置。因此,在這些 SDK 中,只有一個(gè)全局 hub。
在這兩種情況下,當(dāng) SDK 必須處理 reporting errors 時(shí),一切都相對(duì)較好。隨著跟蹤 transaction 和 span 的額外責(zé)任,scope 變得不適合存儲(chǔ)當(dāng)前的 span,因?yàn)樗拗屏瞬l(fā) span 的存在。
對(duì)于瀏覽器 JavaScript,一個(gè)可能的解決方案是使用 Zone.js,Angular 框架的一部分。主要挑戰(zhàn)是它增加了包的大小,并且可能會(huì)無意中影響最終用戶應(yīng)用程序,因?yàn)樗鼘?duì) JavaScript 運(yùn)行時(shí)引擎的關(guān)鍵部分進(jìn)行了猴子修補(bǔ)(monkey-patches)。
- https://github.com/angular/angular/blob/master/packages/zone.js/README.md
當(dāng)我們嘗試為手動(dòng)檢測(cè)創(chuàng)建更簡單的 API 時(shí),scope 傳播問題變得尤為明顯。這個(gè)想法是公開一個(gè) Sentry.trace 函數(shù),該函數(shù)將隱式傳播 tracing 和 scope 數(shù)據(jù), 并支持同步和異步代碼的深度嵌套。
舉個(gè)例子,假設(shè)有人想測(cè)量搜索 DOM 樹需要多長時(shí)間。Tracing(跟蹤) 此操作將如下所示:
- await Sentry.trace(
- {
- op: 'dom',
- description: 'Walk DOM Tree',
- },
- async () => await walkDomTree()
- );
使用 Sentry.trace 功能,用戶在添加計(jì)時(shí)數(shù)據(jù)時(shí)不必?fù)?dān)心保留對(duì)正確 transaction 或 span 的引用。用戶可以在 walkDomTree 函數(shù)中自由創(chuàng)建子 Span,Span 將在正確的層次結(jié)構(gòu)中排序。
實(shí)際 trace 函數(shù)的實(shí)現(xiàn)相對(duì)簡單 (參見具有示例實(shí)現(xiàn)的 PR)。然而,了解異步代碼和全局集成中的當(dāng)前 span 是一個(gè)尚未克服的挑戰(zhàn)。
- https://github.com/getsentry/sentry-javascript/pull/3697/files#diff-f5bf6e0cdf7709e5675fcdc3b4ff254dd68f3c9d1a399c8751e0fa1846fa85dbR158
以下兩個(gè)示例綜合了 scope 傳播問題。
無法確定當(dāng)前 Span
考慮一些需要獲取對(duì)當(dāng)前 span 的引用的自動(dòng)檢測(cè)代碼,在這種情況下,手動(dòng) scope 傳播不可用。
- // SDK code
- function fetchWrapper(/* ... */) {
- /*
- ... some code omitted for simplicity ...
- */
- const parent = getCurrentHub().getScope().getSpan(); // <1>
- const span = parent.startChild({
- data: { type: 'fetch' },
- description: `${method} ${url}`,
- op: 'http.client',
- });
- try {
- // ...
- // return fetch(...);
- } finally {
- span.finish();
- }
- }
- window.fetch = fetchWrapper;
- // User code
- async function f1() {
- const hub = getCurrentHub();
- let t = hub.startTransaction({ name: 't1' });
- hub.getScope().setSpan(t);
- try {
- await fetch('https://example.com/f1');
- } finally {
- t.finish();
- }
- }
- async function f2() {
- const hub = getCurrentHub();
- let t = hub.startTransaction({ name: 't2' });
- hub.getScope().setSpan(t);
- try {
- await fetch('https://example.com/f2');
- } finally {
- t.finish();
- }
- }
- Promise.all([f1(), f2()]); // run f1 and f2 concurrently
在上面的例子中,幾個(gè)并發(fā)的 fetch 請(qǐng)求觸發(fā)了 fetchWrapper helper 的執(zhí)行。行 <1> 必須能夠根據(jù)當(dāng)前的執(zhí)行流程觀察到不同的 span,導(dǎo)致如下兩個(gè) span 樹:
- t1
- \
- |- http.client GET https://example.com/f1
- t2
- \
- |- http.client GET https://example.com/f2
這意味著,當(dāng) f1 運(yùn)行時(shí),parent 必須引用 t1,而當(dāng) f2 運(yùn)行時(shí),parent 必須是 t2。不幸的是,上面的所有代碼都在爭先恐后地更新和讀取單個(gè) hub 實(shí)例,因此觀察到的 span 樹不是確定性的。例如,結(jié)果可能錯(cuò)誤地為:
- t1
- t2
- \
- |- http.client GET https://example.com/f1
- |- http.client GET https://example.com/f2
作為無法正確確定當(dāng)前 span 的副作用, fetch 集成的顯示實(shí)現(xiàn)(和其他)在JavaScript 瀏覽器 SDK 中選擇創(chuàng)建 flat transactions, 其中所有子 span 都是 transaction 的直接子代(而不是具有適當(dāng)?shù)亩嗉?jí)樹結(jié)構(gòu))。
- https://github.com/getsentry/sentry-javascript/blob/61eda62ed5df5654f93e34a4848fc9ae3fcac0f7/packages/tracing/src/browser/request.ts#L169-L178
請(qǐng)注意,其他跟蹤庫也面臨同樣的挑戰(zhàn)。在 OpenTelemetry for JavaScript 中有幾個(gè)(在開放時(shí))問題與確定父跨度和正確的上下文傳播(包括異步代碼)相關(guān):
- 如果使用多個(gè) TracerProvider 實(shí)例,則上下文泄漏 #1932
https://github.com/open-telemetry/opentelemetry-js/issues/1932
- 如何在不傳遞 parent 的情況下創(chuàng)建嵌套 span #1963
https://github.com/open-telemetry/opentelemetry-js/issues/1963
- 嵌套的子 span 沒有得到正確的父級(jí) #1940
https://github.com/open-telemetry/opentelemetry-js/issues/1940
- OpenTracing shim 不會(huì)改變上下文 #2016
https://github.com/open-telemetry/opentelemetry-js/issues/2016
- Http Span 未鏈接/未設(shè)置父 Span #2333
https://github.com/open-telemetry/opentelemetry-js/issues/2333
相互沖突的數(shù)據(jù)傳播預(yù)期
每當(dāng)我們添加前面討論過的 trace 函數(shù),或者只是嘗試使用 Zones 解決 scope 傳播時(shí),就會(huì)出現(xiàn)預(yù)期沖突。
當(dāng)前的 span 與 tags、breadcrumbs 等一起存儲(chǔ)在 scope 中的事實(shí)使數(shù)據(jù)傳播變得混亂, 因?yàn)?scope 的某些部分旨在僅傳播到內(nèi)部函數(shù)調(diào)用中(例如,tags), 而其他人預(yù)計(jì)會(huì)傳播回調(diào)用者(例如,breadcrumbs),尤其是在出現(xiàn) error 時(shí)。
這是一個(gè)例子:
- function a() {
- trace((span, scope) => {
- scope.setTag('func', 'a');
- scope.setTag('id', '123');
- scope.addBreadcrumb('was in a');
- try {
- b();
- } catch(e) {
- // How to report the SpanID from the span in b?
- } finally {
- captureMessage('hello from a');
- // tags: {func: 'a', id: '123'}
- // breadcrumbs: ['was in a', 'was in b']
- }
- })
- }
- function b() {
- trace((span, scope) => {
- const fail = Math.random() > 0.5;
- scope.setTag('func', 'b');
- scope.setTag('fail', fail.toString());
- scope.addBreadcrumb('was in b');
- captureMessage('hello from b');
- // tags: {func: 'b', id: '123', fail: ?}
- // breadcrumbs: ['was in a', 'was in b']
- if (fail) {
- throw Error('b failed');
- }
- });
- }
在上面的示例中,如果 error 在調(diào)用堆棧中冒泡,我們希望能夠報(bào)告 error 發(fā)生在哪個(gè) span(通過引用 SpanID)。我們希望有面包屑來描述發(fā)生的一切,無論哪個(gè) Zones 正在執(zhí)行, 我們希望在內(nèi)部 Zone 中設(shè)置一個(gè) tag 來覆蓋來自父 Zone 的同名 tag, 同時(shí)繼承來自父 Zone 的所有其他 tag。每個(gè) Zone 都有自己的 "current span"。
所有這些不同的期望使得很難以一種可以理解的方式重用當(dāng)前的 scope 概念、面包屑的記錄方式以及這些不同的概念如何相互作用。
最后,值得注意的是,在不破壞現(xiàn)有 SDK API 的情況下,重組 scope 管理的更改很可能無法完成。現(xiàn)有的 SDK 概念 — 如 hubs、scopes、breadcrumbs、user、tags 和 contexts — 都必須重新建模。
Span 攝取模型
考慮由以下 span 樹描述的跟蹤:
- F*
- ├─ B*
- │ ├─ B
- │ ├─ B
- │ ├─ B
- │ │ ├─ S*
- │ │ ├─ S*
- │ ├─ B
- │ ├─ B
- │ │ ├─ S*
- │ ├─ B
- │ ├─ B
- │ ├─ B
- │ │ ├─ S*
- where
- F: span created on frontend service
- B: span created on backend service
- S: span created on storage service
此跟蹤說明了 3 個(gè)被檢測(cè)的服務(wù),當(dāng)用戶單擊網(wǎng)頁上的按鈕 (F) 時(shí),后端 (B) 執(zhí)行一些工作,然后需要對(duì)存儲(chǔ)服務(wù) (S) 進(jìn)行多次查詢。位于給定服務(wù)入口點(diǎn)的 Span 標(biāo)有 * 以表示它們是 transaction。
我們可以通過這個(gè)例子來比較和理解 Sentry 的 span 攝取模型與 OpenTelemetry 和其他類似跟蹤系統(tǒng)使用的模型之間的區(qū)別。
在 Sentry 的 span 攝取模型中,屬于 transaction 的所有 span 必須在單個(gè)請(qǐng)求中一起發(fā)送。這意味著在整個(gè) B* transaction 期間,所有 B span 都必須保存在內(nèi)存中,包括在下游服務(wù)(示例中的存儲(chǔ)服務(wù))上花費(fèi)的時(shí)間。
在 OpenTelemetry 的模型中,span 在完成時(shí)被一起批處理,并且一旦 a) 批次中有一定數(shù)量的 span 或 b) 過了一定的時(shí)間就會(huì)發(fā)送批次。在我們的示例中,這可能意味著前 3 個(gè) B 跨度將一起批處理并發(fā)送, 而第一個(gè) S* 事務(wù)仍在存儲(chǔ)服務(wù)中進(jìn)行。隨后,其他 B span 將一起批處理并在完成時(shí)發(fā)送,直到最終 B* transaction span 也被發(fā)送。
雖然 transaction 作為將 span 組合在一起并探索 Sentry 中感興趣的操作的一種方式特別有用, 但它們目前存在的形式會(huì)帶來額外的認(rèn)知負(fù)擔(dān)。 SDK 維護(hù)人員和最終用戶在編寫檢測(cè)代碼時(shí)都必須了解并在 transaction 或 span 之間進(jìn)行選擇。
在當(dāng)前的攝取模型中已經(jīng)確定了接下來幾節(jié)中的問題,并且都與這種二分法有關(guān)。
事務(wù)的復(fù)雜 JSON 序列化
在 OpenTelemetry 的模型中, 所有跨度都遵循相同的邏輯格式。用戶和檢測(cè)庫可以通過將 key-value 屬性附加到任何 span 來為其提供更多含義。 wire 協(xié)議使用 span 列表將數(shù)據(jù)從一個(gè)系統(tǒng)發(fā)送到另一個(gè)系統(tǒng)。
- https://github.com/open-telemetry/opentelemetry-proto/blob/ebef7c999f4dea62b5b033e92a221411c49c0966/opentelemetry/proto/trace/v1/trace.proto#L56-L235
與 OpenTelemetry 不同,Sentry 的模型對(duì)兩種類型的 span 進(jìn)行了嚴(yán)格區(qū)分:transaction span(通常稱為 transactions)和 regular span。
在內(nèi)存中,transaction span 和 regular span 有一個(gè)區(qū)別:transaction span 有一個(gè)額外的屬性,即 transaction name。
但是,當(dāng)序列化為 JSON 時(shí),差異更大。 Sentry SDK 以直接類似于內(nèi)存中的 span 的格式將常規(guī) span 序列化為 JSON。相比之下,transaction span 的序列化需要將其 span 屬性映射到 Sentry Event (最初用于 report errors,擴(kuò)展為專門用于 transactions 的新字段),并將所有子 span 作為列表嵌入 Event 中。
Transaction Span 獲取 Event 屬性
當(dāng) transaction 從其內(nèi)存表示轉(zhuǎn)換為 Event 時(shí), 它會(huì)獲得更多無法分配給 regular span 的屬性, 例如 breadcrumbs, extra, contexts, event_id, fingerprint, release, environment, user 等。
生命周期鉤子
Sentry SDK 為 error 事件公開了一個(gè) BeforeSend hook,允許用戶在將事件發(fā)送到 Sentry 之前修改和/或丟棄事件。
當(dāng)引入新的 transaction 類型事件時(shí),很快就決定此類事件不會(huì)通過 BeforeSend hook,主要有兩個(gè)原因:
- 防止用戶代碼依賴 transaction 的雙重形式(有時(shí)看起來像一個(gè) span,有時(shí)像一個(gè) event,如前幾節(jié)所述);
- 為了防止現(xiàn)有的 BeforeSend 函數(shù)在編寫時(shí)只考慮到 error 而干擾 transaction,無論是意外地改變它們、完全丟棄它們,還是導(dǎo)致一些其他意想不到的副作用。
然而,也很明顯需要某種形式的 lifecycle hook,以允許用戶執(zhí)行諸如更新 transaction 名稱之類的操作。
我們最終達(dá)成了中間立場(chǎng),即通過使用 EventProcessor(一種更通用的 BeforeSend 形式)來允許更改/丟棄 transaction 事件。這通過在數(shù)據(jù)離開 SDK 之前讓用戶立即訪問他們的數(shù)據(jù)來解決問題,但它也有缺點(diǎn),它比 BeforeSend 使用起來更復(fù)雜,并且還暴露了從未打算泄漏的 transaction 二元性。
相比之下,在 OpenTelemetry 中,span 通過 span processor,這是兩個(gè)生命周期鉤子:一個(gè)是在 span 開始時(shí),一個(gè)是在它結(jié)束時(shí)。
嵌套事務(wù)
Sentry 的攝取模型不是為服務(wù)中的嵌套 transaction 而設(shè)計(jì)的。Transaction 旨在標(biāo)記服務(wù)轉(zhuǎn)換。
在實(shí)踐中,SDK 無法防止 transaction 嵌套。最終結(jié)果可能會(huì)讓用戶感到驚訝,因?yàn)槊抗P transaction 都會(huì)開始一棵新樹。關(guān)聯(lián)這些樹的唯一方法是通過 trace_id。
Sentry 的計(jì)費(fèi)模型是針對(duì)每個(gè)事件的,無論是 error 事件還是 transaction 事件。這意味著 transaction 中的 transaction 會(huì)生成兩個(gè)可計(jì)費(fèi)事件。
在 SDK 中,在 transaction 中進(jìn)行 transaction 將導(dǎo)致內(nèi)部 span 被圍繞它們的最內(nèi)層 transaction “吞噬”。在這些情況下,創(chuàng)建 span 的代碼只會(huì)將它們添加到兩個(gè) transaction 之一,從而導(dǎo)致另一個(gè) transaction 中的檢測(cè)間隙。
Sentry 的 UI 并非旨在以有用的方式處理嵌套 transaction。當(dāng)查看任何一個(gè) transaction 時(shí),就好像 transaction 中的所有其他 transaction 都不存在(樹視圖上沒有直接表示其他 transaction)。有一個(gè) trace view 功能來可視化共享一個(gè) trace_id 的所有 transaction, 但 trace view 僅通過顯示 transaction 而不是子 span 來提供跟蹤的概述。如果不先訪問某個(gè) transaction,就無法導(dǎo)航到 trace view。
對(duì)于這種情況(偽代碼),用戶對(duì) UI 中的期望也存在混淆:
- # if do_a_database_query returns 10 results, is the user
- # - seeing 11 transactions in the UI?
- # - billed for 11 transactions?
- # - see spans within create_thumbnail in the innermost transaction only?
- with transaction("index-page"):
- results = do_a_database_query()
- for result in results:
- if result["needs_thumbnail"]:
- with transaction("create-thumbnail", {"resource": result["id"]}):
- create_thumbnail(result)
跨度不能存在于事務(wù)之外
Sentry 的追蹤體驗(yàn)完全圍繞著存在于 transaction 中的 trace 部分。這意味著數(shù)據(jù)不能存在于 transaction 之外,即使它存在于 trace 中。
如果 SDK 沒有進(jìn)行 transaction,則由 instrumentation 創(chuàng)建的 regular span 將完全丟失。也就是說,這對(duì) Web server 來說不是什么問題,因?yàn)樽詣?dòng)檢測(cè)的 transaction 隨著每個(gè)傳入請(qǐng)求開始和結(jié)束。
Transaction 的要求在前端(瀏覽器、移動(dòng)和桌面應(yīng)用程序)上尤其具有挑戰(zhàn)性, 因?yàn)樵谶@些情況下,自動(dòng)檢測(cè)的 transaction 不太可靠地捕獲所有 span,因?yàn)樗鼈冊(cè)谧詣?dòng)完成之前只持續(xù)有限的時(shí)間。
在 trace 以僅作為 span 而不是 transaction 進(jìn)行檢測(cè)的操作開始的情況下,會(huì)出現(xiàn)另一個(gè)問題。在我們的 示例跟蹤中,產(chǎn)生 trace 的第一個(gè) span 是由于單擊按鈕。如果按鈕點(diǎn)擊 F* 被檢測(cè)為常規(guī)的 span 而不是 transaction,則很可能不會(huì)捕獲來自前端的數(shù)據(jù)。然而,仍會(huì)捕獲 B 和 S span,導(dǎo)致不完整的蹤跡。
在 Sentry 的模型中,如果一個(gè) span 不是一個(gè) transaction 并且沒有作為 transaction 的祖先 span,那么該 span 將不會(huì)被攝取。反過來,這意味著在很多情況下,跟蹤丟失了有助于調(diào)試問題的關(guān)鍵信息,特別是在前端,transaction 需要在某個(gè)時(shí)刻結(jié)束但執(zhí)行可能會(huì)繼續(xù)。
自動(dòng)和手動(dòng)檢測(cè)面臨著決定是開始 span 還是 transaction 的挑戰(zhàn),考慮到以下因素,決定尤其困難:
如果沒有 transaction,則 span 丟失。
如果已經(jīng)存在 transaction,則存在嵌套事務(wù)問題。
缺少 Web Vitals 測(cè)量
Sentry 的瀏覽器工具收集 Web Vitals 測(cè)量值。但是,因?yàn)檫@些測(cè)量值是使用自動(dòng)檢測(cè)的 transaction 作為載體發(fā)送到 Sentry 的,所以在自動(dòng) transaction 完成后由瀏覽器提供的測(cè)量值將丟失。
這會(huì)導(dǎo)致 transaction 丟失一些 Web Vitals 或?qū)?LCP 等指標(biāo)進(jìn)行非最終測(cè)量。
前端事務(wù)持續(xù)時(shí)間不可靠
因?yàn)樗械臄?shù)據(jù)都必須在一個(gè) transaction 中。Sentry 的瀏覽器 SDK 為每個(gè)頁面加載和每個(gè)導(dǎo)航創(chuàng)建一個(gè) transaction。這些 transaction 必須在某個(gè)時(shí)間結(jié)束。
如果在 transaction 完成之前關(guān)閉瀏覽器選項(xiàng)卡并將其發(fā)送到 Sentry,則所有收集的數(shù)據(jù)都會(huì)丟失。因此,SDK 需要平衡丟失所有數(shù)據(jù)的風(fēng)險(xiǎn)與收集不完整和可能不準(zhǔn)確的數(shù)據(jù)的風(fēng)險(xiǎn)。
在觀察到最后一個(gè)活動(dòng)(例如傳出的 HTTP 請(qǐng)求)后空閑了一段時(shí)間后,Transaction 就完成了。這意味著頁面加載或?qū)Ш?transaction 的持續(xù)時(shí)間是一個(gè)相當(dāng)隨意的值,不一定能改進(jìn)或與其他事務(wù)相比,因?yàn)樗荒軠?zhǔn)確代表任何具體和可理解的過程的持續(xù)時(shí)間。
我們通過將 LCP Web Vital 作為瀏覽器的默認(rèn)性能指標(biāo)來應(yīng)對(duì)這一限制。但是,如上所述,LCP 值可能會(huì)在最終確定之前發(fā)送,因此這不是理想的解決方案。
內(nèi)存緩沖影響服務(wù)器
如前所述,當(dāng)前的攝取模型需要 Sentry SDK 來觀察內(nèi)存中的完整 span 樹。以恒定的并發(fā) transaction 流運(yùn)行的應(yīng)用程序?qū)⑿枰罅康南到y(tǒng)資源來收集和處理跟蹤數(shù)據(jù)。Web 服務(wù)器是出現(xiàn)此問題的典型案例。
這意味著記錄 100% 的 span 和 100% 的 transaction 對(duì)于許多服務(wù)器端應(yīng)用程序來說是不可行的,因?yàn)樗a(chǎn)生的開銷太高了。
無法批處理事務(wù)
Sentry 的攝取模型不支持一次攝取多個(gè)事件。特別是,SDK 不能將多個(gè) transaction 批處理為一個(gè)請(qǐng)求。
因此,當(dāng)多筆 transaction 幾乎同時(shí)完成時(shí),SDK 需要為每個(gè) transaction 發(fā)出單獨(dú)的請(qǐng)求。這種行為在最好的情況下是非常低效的,在最壞的情況下是對(duì)資源(如網(wǎng)絡(luò)帶寬和CPU周期)的嚴(yán)重且有問題的消耗。
兼容性
Transaction Span 的特殊處理與 OpenTelemetry 不兼容。使用 OpenTelemetry SDK 檢測(cè)現(xiàn)有應(yīng)用程序的用戶無法輕松使用 Sentry 來獲取和分析他們的數(shù)據(jù)。
Sentry 確實(shí)為 OpenTelemetry Collector 提供了一個(gè) Sentry Exporter,但是,由于當(dāng)前的攝取模型,Sentry Exporter 有一個(gè)主要的正確性限制。
https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/sentryexporter#known-limitations
總結(jié)
通過在 Sentry 中構(gòu)建當(dāng)前的跟蹤實(shí)現(xiàn),我們學(xué)到了很多。本文檔試圖捕捉許多已知的限制,以作為未來改進(jìn)的基礎(chǔ)。
追蹤是一個(gè)復(fù)雜的主題,馴服這種復(fù)雜性并非易事。
第一組中的問題 - 與 scope propagation(作用域傳播) 相關(guān)的問題 - 是 SDK 及其設(shè)計(jì)方式獨(dú)有的問題。解決這些問題將需要對(duì)所有 SDK 進(jìn)行內(nèi)部架構(gòu)更改,包括重新設(shè)計(jì)面包屑等舊功能, 但進(jìn)行此類更改是實(shí)現(xiàn)簡單易用的 tracing helper(如可在任何上下文中工作并捕獲準(zhǔn)確可靠的性能數(shù)據(jù)的 trace 函數(shù))的先決條件。請(qǐng)注意,此類更改幾乎肯定意味著發(fā)布新的主要 SDK 版本,這會(huì)破壞與現(xiàn)有版本的兼容性。
第二組中的問題 - 與 span ingestion model(跨度攝取模型) 相關(guān)的問題要復(fù)雜得多,因?yàn)闉榻鉀Q這些問題所做的任何更改都會(huì)影響產(chǎn)品的更多部分,并且需要多個(gè)團(tuán)隊(duì)的協(xié)調(diào)努力。
盡管如此,對(duì) ingestion model 進(jìn)行更改將對(duì)產(chǎn)品產(chǎn)生不可估量的積極影響,因?yàn)檫@樣做會(huì)提高效率,使我們能夠收集更多數(shù)據(jù),并減少 instrumentation 的負(fù)擔(dān)。