Sentry 監控 - 全棧開發人員的分散式跟蹤 101 系列教程(第一部分)

為少發表於2021-10-06

系列

歡迎來到我們關於全棧開發人員分散式跟蹤(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 requestWeb server 的完整生命週期)有時被稱為事務跨度(transaction spans),甚至只是事務。 我們將在本系列的第 2 部分中更多地討論事務與跨度(transactions vs. spans)。

跟蹤和跨度識別符號

到目前為止,我們已經確定了跟蹤的元件,但我們還沒有描述這些元件是如何連結在一起的。

首先,每個跟蹤都用跟蹤識別符號(trace identifier)唯一標識。 這是通過在根跨度(root span)中建立一個唯一的隨機生成值(即 UUID)來完成的——這是啟動整個跟蹤的初始操作。 在我們上面的示例中,根跨度出現在瀏覽器應用程式中。

其次,每個 span 首先需要被唯一標識。 這通過在跨度開始其操作時建立唯一的跨度識別符號(或 span_id)來完成。這個 span_id 建立應該發生在 trace 內發生的每個 span(或操作)處進行。

讓我們重新審視我們假設的跟蹤示例。 在上圖中,您會注意到跟蹤識別符號唯一地標識了跟蹤,並且該跟蹤中的每個跨度也擁有一個唯一的跨度識別符號。

然而,生成 trace_idspan_id 是不夠的。 要實際連線這些服務,您的應用程式必須在從一個服務向另一個服務發出請求時傳播所謂的跟蹤上下文(trace context)。

跟蹤上下文

跟蹤上下文(trace context)通常僅由兩個值組成:

  • 跟蹤識別符號(或 trace_id):在根跨度中生成的唯一識別符號,用於標識整個跟蹤。 這與我們在上一節中介紹的跟蹤識別符號相同;它以不變的方式傳播到每個下游服務。
  • 父識別符號(或 parent_id):產生當前操作的“父”跨度的 span_id

下圖顯示了在一個服務中啟動的請求如何將跟蹤上下文傳播到下游的下一個服務。 您會注意到 trace_id 保持不變,而 parent_id 在請求之間發生變化,指向啟動最新操作的父跨度。

有了這兩個值,對於任何給定的操作,就可以確定原始(root)服務,並按照導致當前操作的順序重建所有父/祖先(parent/ancestor)服務。

工作示例(程式碼演示)

示例原始碼:

為了更好地理解這一點,讓我們實際實現一個基本的跟蹤實現,其中瀏覽器應用程式是由跟蹤上下文連線的一系列分散式操作的發起者。

首先,瀏覽器應用程式呈現一個表單:就本示例而言,是一個“邀請使用者(invite user)”表單。表單有一個提交事件處理程式,它在表單提交時觸發。 讓我們將此提交處理程式視為我們的根跨度(root span),這意味著當呼叫處理程式時,會生成 trace_idspan_id

接下來,完成一些工作以從表單中收集使用者輸入的值,然後最後向我們的 Web 伺服器發出一個到 /inviteUser API 端點的 fetch 請求。作為此 fetch 請求的一部分,跟蹤上下文作為兩個自定義 HTTP header 傳遞:trace-idparent-id(即當前 spanspan_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” 階段。

在接收端,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 呼叫,該呼叫還發出當前 tracespanparent 識別符號。 在完美的同步世界中——每個服務都可以登入到同一個集中式 logging 工具——這些日誌語句中的每一個都會依次出現:

如果在這些操作過程中發生異常或錯誤行為,使用這些或額外的日誌語句來查明來源將相對簡單。但不幸的現實是,這些都是分散式服務,這意味著:

  • Web 伺服器通常處理許多併發請求。Web 伺服器可能正在執行歸因於其他請求的工作(併發出日誌記錄語句)。
  • 網路延遲會影響操作順序。 從上游服務發出的請求可能不會按照它們被觸發的順序到達目的地。
  • 後臺 worker 可能有排隊的 job。在到達此跟蹤中排隊的確切 job 之前,worker 可能必須先完成先前排隊的 job

在一個更現實的例子中,我們的日誌呼叫可能看起來像這樣,它反映了同時發生的多個操作:

如果不跟蹤 metadata,就不可能瞭解哪個動作呼叫哪個動作的拓撲結構。 但是通過在每次 logging 呼叫時發出跟蹤 meta 資訊,可以通過過濾 traceId 快速過濾跟蹤中的所有 logging 呼叫,並通過檢查 spanIdparentId 關係重建確切的順序。

這就是分散式跟蹤的威力:通過附加描述當前操作(span id)、產生它的父操作(parent id)和跟蹤識別符號(trace id)的後設資料,我們可以增加日誌記錄和遙測資料以更好地理解 分散式服務中發生的事件的確切順序。

在真實的分散式跟蹤環境中

在本文的過程中,我們一直在使用一個有點人為的示例。 在真正的分散式跟蹤環境中,您不會手動生成和傳遞所有的跨度和跟蹤識別符號。 您也不會依賴 console.log(或其他日誌記錄)呼叫來自己發出跟蹤後設資料。 您將使用適當的跟蹤庫來為您處理檢測和傳送跟蹤資料。

OpenTelemetry

OpenTelemetry 是一組開源工具、APISDK,用於檢測、生成和匯出正在執行的軟體中的遙測資料。 它為大多數流行的程式語言提供了特定於語言的實現,包括瀏覽器 JavaScriptNode.js

Sentry

Sentry 以多種方式使用這種遙測。例如,Sentry 的效能監控功能集使用跟蹤資料生成瀑布圖,說明跟蹤中分散式服務操作的端到端延遲。

Sentry 還使用跟蹤後設資料來增強它的錯誤監控功能,以瞭解在一個服務(如伺服器後端)中觸發的錯誤如何傳播到另一個服務(如前端)中的錯誤。

相關文章