Sentry 監控 - 面向全棧開發人員的分布式跟蹤
歡迎來到我們關于全棧開發人員分布式跟蹤(Distributed Tracing)的系列的第 1 部分。在本系列中,我們將學習分布式跟蹤的細節,以及它如何幫助您監控全棧應用程序日益復雜的需求。
在 Web 的早期,編寫 Web 應用程序很簡單。開發人員使用 PHP 等語言在服務器上生成 HTML,與 MySQL 等單一關系數據庫進行通信,大多數交互性由靜態 HTML 表單組件驅動。雖然調試工具很原始,但理解代碼的執行流程很簡單。
在今天的現代 web 棧中,它什么都不是。全棧開發人員需要編寫在瀏覽器中執行的 JavaScript,與多種數據庫技術互操作,并在不同的服務器架構(例如:serverless)上部署服務器端代碼。如果沒有合適的工具,了解瀏覽器中的用戶交互如何關聯到服務器堆棧深處的 500 server error 幾乎是不可能的。Enter:分布式跟蹤。
我試圖解釋 2021 年我的 web 堆棧中的瓶頸。
分布式跟蹤(Distributed tracing)是一種監控技術,它將多個服務之間發生的操作和請求聯系起來。這允許開發人員在端到端請求從一個服務移動到另一個服務時“跟蹤(trace)”它的路徑,讓他們能夠查明對整個系統產生負面影響的單個服務中的錯誤或性能瓶頸。
在這篇文章中,我們將了解有關分布式跟蹤概念的更多信息,在代碼中查看端到端(end-to-end)跟蹤示例,并了解如何使用跟蹤元數據為您的日志記錄和監控工具添加有價值的上下文。完成后,您不僅會了解分布式跟蹤的基礎知識,還會了解如何應用跟蹤技術來更有效地調試全棧 Web 應用程序。
但首先,讓我們回到開頭:什么是分布式追蹤?
分布式追蹤基礎
分布式跟蹤是一種記錄多個服務的連接操作的方法。通常,這些操作是由從一個服務到另一個服務的請求發起的,其中“請求(request)”可以是實際的 HTTP 請求,也可以是通過任務隊列或其他一些異步方式調用的工作。
跟蹤由兩個基本組件組成:
- Span 描述發生在服務上的操作或 “work”。Span 可以描述廣泛的操作——例如,響應 HTTP 請求的 web 服務器的操作——也可以描述單個函數的調用。
- trace 描述了一個或多個連接 span 的端到端(end-to-end)旅程。如果 trace 連接在多個服務上執行的 span(“work”),則該 trace 被認為是分布式跟蹤。
讓我們看一個假設的分布式跟蹤示例。
上圖說明了 trace 如何從一個服務(一個在瀏覽器上運行的 React 應用程序)開始,并通過調用 API Web Server 繼續,甚至進一步調用后臺任務 worker。此圖中的 span 是在每個服務中執行的 work,每個 span 都可以“追溯到(traced)”由瀏覽器應用程序啟動的初始工作(initial work)。最后,由于這些操作發生在不同的服務上,因此該跟蹤被認為是分布式的。
描述廣泛操作的跨度(例如:響應 HTTP request 的 Web server 的完整生命周期)有時被稱為事務跨度(transaction spans),甚至只是事務。我們將在本系列的第 2 部分中更多地討論事務與跨度(transactions vs. spans)。
跟蹤和跨度標識符
到目前為止,我們已經確定了跟蹤的組件,但我們還沒有描述這些組件是如何鏈接在一起的。
首先,每個跟蹤都用跟蹤標識符(trace identifier)唯一標識。這是通過在根跨度(root span)中創建一個唯一的隨機生成值(即 UUID)來完成的——這是啟動整個跟蹤的初始操作。在我們上面的示例中,根跨度出現在瀏覽器應用程序中。
其次,每個 span 首先需要被唯一標識。這通過在跨度開始其操作時創建唯一的跨度標識符(或 span_id)來完成。這個 span_id 創建應該發生在 trace 內發生的每個 span(或操作)處進行。
讓我們重新審視我們假設的跟蹤示例。在上圖中,您會注意到跟蹤標識符唯一地標識了跟蹤,并且該跟蹤中的每個跨度也擁有一個唯一的跨度標識符。
然而,生成 trace_id 和 span_id 是不夠的。要實際連接這些服務,您的應用程序必須在從一個服務向另一個服務發出請求時傳播所謂的跟蹤上下文(trace context)。
跟蹤上下文
跟蹤上下文(trace context)通常僅由兩個值組成:
- 跟蹤標識符(或 trace_id):在根跨度中生成的唯一標識符,用于標識整個跟蹤。這與我們在上一節中介紹的跟蹤標識符相同;它以不變的方式傳播到每個下游服務。
- 父標識符(或 parent_id):產生當前操作的“父”跨度的 span_id。
下圖顯示了在一個服務中啟動的請求如何將跟蹤上下文傳播到下游的下一個服務。您會注意到 trace_id 保持不變,而 parent_id 在請求之間發生變化,指向啟動最新操作的父跨度。
有了這兩個值,對于任何給定的操作,就可以確定原始(root)服務,并按照導致當前操作的順序重建所有父/祖先(parent/ancestor)服務。
工作示例(代碼演示)
示例源碼:
- https://github.com/getsentry/distributed-tracing-examples
為了更好地理解這一點,讓我們實際實現一個基本的跟蹤實現,其中瀏覽器應用程序是由跟蹤上下文連接的一系列分布式操作的發起者。
首先,瀏覽器應用程序呈現一個表單:就本示例而言,是一個“邀請用戶(invite user)”表單。表單有一個提交事件處理程序,它在表單提交時觸發。讓我們將此提交處理程序視為我們的根跨度(root span),這意味著當調用處理程序時,會生成 trace_id 和 span_id。
接下來,完成一些工作以從表單中收集用戶輸入的值,然后最后向我們的 Web 服務器發出一個到 /inviteUser API 端點的 fetch 請求。作為此 fetch 請求的一部分,跟蹤上下文作為兩個自定義 HTTP header 傳遞:trace-id 和 parent-id(即當前 span 的 span_id)。
- // browser app (JavaScript)
- import uuid from 'uuid';
- const traceId = uuid.v4();
- const spanId = uuid.v4();
- console.log('Initiate inviteUser POST request', `traceId: ${traceId}`);
- fetch('/api/v1/inviteUser?email=' + encodeURIComponent(email), {
- method: 'POST',
- headers: {
- 'trace-id': traceId,
- 'parent-id': spanId,
- }
- }).then((data) => {
- console.log('Success!');
- }).catch((err) => {
- console.log('Something bad happened', `traceId: ${traceId}`);
- });
請注意,這些是用于說明目的的非標準 HTTP header。作為 W3C traceparent 規范的一部分,正在積極努力標準化 tracing HTTP header,該規范仍處于 “Recommendation” 階段。
- https://www.w3.org/TR/trace-context/
在接收端,API web server 處理請求并從 HTTP 請求中提取跟蹤元數據(tracing metadata)。然后它會排隊一個 job 以向用戶發送電子郵件,并將跟蹤上下文作為 job 描述中“meta”字段的一部分附加。最后,它返回一個帶有 200 狀態 code 的響應,表明該方法成功。
請注意,雖然服務器返回了成功的響應,但實際的“工作”直到后臺任務 worker 拿起新排隊的 job 并實際發送電子郵件后才完成。
在某個點上,隊列處理器開始處理排隊的電子郵件作業。再一次,跟蹤(trace)和父標識符(parent identifier)被提取出來,就像它們在 web server 中的早些時候一樣。
- // API Web Server
- const Queue = require('bull');
- const emailQueue = new Queue('email');
- const uuid = require('uuid');
- app.post("/api/v1/inviteUser", (req, res) => {
- const spanId = uuid.v4(),
- traceId = req.headers["trace-id"],
- parentId = req.headers["parent-id"];
- console.log(
- "Adding job to email queue",
- `[traceId: ${traceId},`,
- `parentId: ${parentId},`,
- `spanId: ${spanId}]`
- );
- emailQueue.add({
- title: "Welcome to our product",
- to: req.params.email,
- meta: {
- traceId: traceId,
- // the downstream span's parent_id is this span's span_id
- parentId: spanId,
- },
- });
- res.status(200).send("ok");
- });
- // Background Task Worker
- emailQueue.process((job, done) => {
- const spanId = uuid.v4();
- const { traceId, parentId } = job.data.meta;
- console.log(
- "Sending email",
- `[traceId: ${traceId},`,
- `parentId: ${parentId},`,
- `spanId: ${spanId}]`
- );
- // actually send the email
- // ...
- done();
- });
分布式系統 Logging
您會注意到,在我們示例的每個階段,都會使用 console.log 進行 logging 調用,該調用還發出當前 trace、span 和 parent 標識符。在完美的同步世界中——每個服務都可以登錄到同一個集中式 logging 工具——這些日志語句中的每一個都會依次出現:
如果在這些操作過程中發生異常或錯誤行為,使用這些或額外的日志語句來查明來源將相對簡單。但不幸的現實是,這些都是分布式服務,這意味著:
Web 服務器通常處理許多并發請求。Web 服務器可能正在執行歸因于其他請求的工作(并發出日志記錄語句)。
網絡延遲會影響操作順序。從上游服務發出的請求可能不會按照它們被觸發的順序到達目的地。
后臺 worker 可能有排隊的 job。在到達此跟蹤中排隊的確切 job 之前,worker 可能必須先完成先前排隊的 job。
在一個更現實的例子中,我們的日志調用可能看起來像這樣,它反映了同時發生的多個操作:
如果不跟蹤 metadata,就不可能了解哪個動作調用哪個動作的拓撲結構。但是通過在每次 logging 調用時發出跟蹤 meta 信息,可以通過過濾 traceId 快速過濾跟蹤中的所有 logging 調用,并通過檢查 spanId 和 parentId 關系重建確切的順序。
這就是分布式跟蹤的威力:通過附加描述當前操作(span id)、產生它的父操作(parent id)和跟蹤標識符(trace id)的元數據,我們可以增加日志記錄和遙測數據以更好地理解 分布式服務中發生的事件的確切順序。
在真實的分布式跟蹤環境中
在本文的過程中,我們一直在使用一個有點人為的示例。在真正的分布式跟蹤環境中,您不會手動生成和傳遞所有的跨度和跟蹤標識符。您也不會依賴 console.log(或其他日志記錄)調用來自己發出跟蹤元數據。您將使用適當的跟蹤庫來為您處理檢測和發送跟蹤數據。
OpenTelemetry
OpenTelemetry 是一組開源工具、API 和 SDK,用于檢測、生成和導出正在運行的軟件中的遙測數據。它為大多數流行的編程語言提供了特定于語言的實現,包括瀏覽器 JavaScript 和 Node.js。
- https://opentelemetry.io/
- https://github.com/open-telemetry/opentelemetry-js
Sentry
Sentry 以多種方式使用這種遙測。例如,Sentry 的性能監控功能集使用跟蹤數據生成瀑布圖,說明跟蹤中分布式服務操作的端到端延遲。
Sentry 還使用跟蹤元數據來增強它的錯誤監控功能,以了解在一個服務(如服務器后端)中觸發的錯誤如何傳播到另一個服務(如前端)中的錯誤。