掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
作者:為少 2021-10-06 05:04:47
安全
應(yīng)用安全
分布式 在 Web 的早期,編寫 Web 應(yīng)用程序很簡(jiǎn)單。開發(fā)人員使用 PHP 等語言在服務(wù)器上生成 HTML,與 MySQL 等單一關(guān)系數(shù)據(jù)庫(kù)進(jìn)行通信,大多數(shù)交互性由靜態(tài) HTML 表單組件驅(qū)動(dòng)。雖然調(diào)試工具很原始,但理解代碼的執(zhí)行流程很簡(jiǎn)單。

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

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流