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