使用OpenTelemetry和Jaeger進行追蹤

Tracing(追踪)是一個關鍵組件,可以追踪複雜系統中的請求。這種可見性揭示了瓶頸和錯誤,從而實現更快的解決。在我們的Go Web服務系列的以前的文章中,我們探討了觀察性的重要性。今天,我們專注於追踪。Jaeger從分佈式系統中收集、存儲和可視化追踪。它提供了對服務之間請求流程的關鍵見解。通過將Jaeger與OpenTelemetry集成,開發人員可以統一他們的追踪方法,確保一致和全面的可見性。這種集成簡化了診斷性能問題並增強系統可靠性。在本文中,我們將設置Jaeger,在我們的應用程序中將其與OpenTelemetry集成,並探索可視化追踪以獲得更深入的見解。

動機

我們正在努力實現的是一個看起來像這樣的Jaeger儀表板:

當我們轉到應用程序的各個部分(在Onehub前端)時,收集各種請求的追踪(從命中grpc-gateway點)以及每個追踪的摘要。我們甚至可以深入研究一個追踪以獲得更細節的視圖。查看第一個POST請求(用於在主題中創建/發送消息):

在這裡,我們可以看到Create請求觸及的所有組件,以及它們的進入/退出時間和方法的時間。非常強大。

入門指南

簡而言之:要在操作中看到這一點並驗證博客的其餘部分:

  1. 源代碼存放在PART11_TRACING分支中。
  2. 建立所有必要的事物(当你检出分支後):
make build

  1. 我們已將docker-compose分成两部分(這将在下面進一步解釋),所以請確保你有兩個window在運行中。
  • 終端1:make updb dblogs
  • 終端2:make up logs
  1. 转到localhost:7080,就可以開始了。

高层次概览

我們系統目前:

使用OpenTelemetry進行 instrumentation,我們的系統將演變為:

如我們早先所提到的,每個服務都使用不同的客戶端來發送至特定的供應商是相当沉重的。 instead,我們可以確保所有(感興趣的)服務可以簡單地把度量/日志/追蹤發送至此收集器,然後此收集器可以把這些數據 Export到它們需要的後端 — 在這案例中,Jaeger用於追蹤。

讓我們開始吧。

設定OTel收集器

第一步是將我們運行在Docker環境中的OTel收集器添加到Jaeger中,以便他們可以存取。

注意:我們已將我們原有的docker-compose.yml配置文件拆分成两部分:

  1. db-docker-compose.yml:包含所有數據庫和基礎建設(非應用程序)相關组件,如數據庫(Postgres, Typesense)和監控服務(OTel收集器,Jaeger,Prometheus等)。
  2. docker-compose.yml: 包含了所有与应用相關的服务 (Nginx, gRPC Gateway, dbsync, frontend 等)。

這兩個 docker-compose 環境通過一個共享網絡 (onehubnetwork) 連接,讓這些環境中的服務能夠相互通信。透過這種分隔,我們在變動發生時只需要重新啟動服務的一部分,加快我們的開發速度。

回到我們的設定:在我們的 db-docker-compose.yml 中,新增以下服務:

YAML

 

這相當於在我們的 Docker 環境中設定兩個服務:

  1. otel-collector: 所有被監控服務發送的訊號 (metrics/logs/traces) 的汇聚點,它使用標準的 OTel 映像档,並搭配我們自訂的 OTel 設定 (如下) 來描述各種观测管道 (即讯号應該如何接收、處理和以各種方式匯出)。
  2. jaeger: 我們的 Jaeger 實例,將接收和儲存由 otel-collector 匯出的追蹤信息,此服務同時也是儲存和我們將要在 /jaeger HTTP 路徑前缀上出口的介面 (UI) 的主机。
  3. prometheus: 雖然這篇貼文不需要,我們還是會匯出度量值,以便它能被 Prometheus 截取。我們在這篇貼文中不會詳細讨论這個。

有幾件事值得注意:

  • 雖然這篇貼文不需要,我們還是將 POSTGRES 連接詳細資料 (作為環境變量) 傳送給 otel-collector,使它能夠抓取 Postgres 健康度量值。
  • Jaeger (從 v1.35 版本開始) 原生支援 OTLP
  • OTLP 的優點是 OTel 收集器可以串聯起來,形成一個 OTel 收集器/處理器/转发器/路由器等網絡。
  • OTLP 可以通过 GRPC 或 HTTP 端點提供服務(分別為端口 4317 和 4318)。
  • 默認情況下,OTLP 服務会在 localhost:4317/4318 開始。如果 Jaeger 是在與其所監控服務運行於同一主機/容器的同一个主機/容器上運行,這當然没問題。然而,由於 Jaeger 是在不同的容器上運行,它們必須绑定到外部地址(0.0.0.0)。這在文檔中不是很清楚,導致了大量的困擾。
  • COLLECTOR_OTLP_ENABLED: true 現在是默認值,不需要顯式指定。

OTel 配置

OTel 也需要用特定的接收器、處理器和导出器進行配置。我們會在 configs/otel-collector.yaml 里面这样做。

添加接收器

我們需要告訴 OTel 收集器要启用的接收器。這在 receivers 节段中指定:

YAML

 

此處激活了端口4317和4318上的OTLP接收器(分別為grpchttp)。可以啟動很多種類的接收器。作為一個例子,我們也添加了一個“postgresql”接收器,它將積極抓取Postgres的度量(儘管這對於本篇文章來說並不相關)。接收器也可以是拉取或推送基礎的。拉取基礎的接收器定期抓取特定的目標(例如,postgres),而推送基礎的接收器則收听並“接收”使用OTel客戶端SDK的應用程序發送的度量/日誌/跟踪。

就是这样。現在我們的收集器已經準備好接收(或抓取)適當的度量。

添加處理器

OTel中的處理器是一種方式,可以在匯出之前轉換、映射、批次、過濾和/或豐富收到的信號。例如,處理器可以采樣度量,過濾日誌,甚至為效率批次信號。預設情況下沒有添加處理器(使收集器成為直通)。我們現在會忽略這點。

添加匯出器

現在是確定信號要匯出到何處的時候:最适合各的信號的後端。就像接收器一樣,匯出器也可以是拉取或推送到基礎。推送到基礎的匯出器用於發送信號到另一個以推送模式運作的接收器。這些是外出。拉取基礎的匯出器暴露可以被其他以拉取基礎的接收器(例如,prometheus)抓取的端點。我們將添加每一種類型的匯出器:一個用於追蹤和一個用於從Prometheus抓取(雖然Prometheus不是本篇文章的主題):

YAML

 

這裡有一個對Jaeger運行OTLP收集器的匯出器,由otlp/jaeger指出。這個匯出器將定期將追蹤的信號推送到Jaeger。我們還為端口9090添加一個“抓取器”端點,Prometheus將從此端點定期抓取。

“debug”匯出器只是用於將信號倒在標準輸入/錯誤流中。

定義管線

接收器、處理器及匯出器部分只是定義收集器將启用的模塊。它們仍然未被呼叫。為了實際呼叫/啟用它們,它們必須被稱為“管線”。管線定義信號如何流經及被收集器處理。我們的管線定義(在services部分中)將說明這點:

YAML

 

這裡我們正在定義兩個管線。注意管線之間的類似性,但允許兩種不同的匯出模式(Jaeger和Prometheus)。現在我們可以看到OTel和在其中創建管線的威力。

  1. traces:

  • 從客戶端SDK接收信號
  • 無處理
  • 將追蹤資訊 Export 到控制台和 Jaeger
  1. 指標
  • 從客戶端 SDK 收到的信號
  • 不進行處理
  • 將指標 Export 到控制台和 Prometheus(通過暴露一個端點供其抓取)。

透過 Nginx 暴露控制板

Jaeger 提供了一個控制板用於視覺化所有我們的請求的追蹤數據。這可以在允許下列選項的我們的 Nginx 配置 中於瀏覽器中視覺化。同樣,這篇文章的內容並不涉及此點 – 我們也通過 nginx 在 HTTP 路徑前缀 /prometheus 暴露了 Prometheus UI。

YAML

 

在 Jaeger 中視覺化追蹤

Jaeger UI 非常 Comprehensive,且有許多可以探索的功能。在瀏覽器中導航到 Jaeger UI。您將看到一個全面的接口用於搜索和分析追蹤。繼續熟悉主要區域,包括搜索欄和追蹤列表。您可以根據搜索條件搜索各種追蹤,並通過服務、時間持續時間、部件等進行過濾。

分析不同請求中的追蹤時間線以了解操作序列。每個跨度代表一個工作單元,顯示開始和結束時間、持續時間和相關元數據。這個詳細的視圖對於者在追蹤內識別性能瓶頸和錯誤非常有幫助。

整合客戶端 SDK

到目前為止,我們已經設定系統來視覺化、消費信號等。然而,我們的服務仍然沒有更新到可以向 OTel 發送信號。這裡我們將在各個部分的程式碼中與 (Golang) 客戶端 SDK 整合。SDK 文档是熟悉一些概念的絕佳場所。

我們将要处理的關鍵概念在下文中有描述。

資源

資源是產生信號的實體。在我們的案例中,資源的範圍是托管服務的可执行文件。目前,我們整個 Onehub 服務只有一個資源,但這可以後續分割。

這在 cmd/backend/obs.go 中定義。請注意,客戶端 SDK 並不需要我們显式進入資源定義的詳細信息。標準助手(sdktrace.WithResource)讓我們通過推論最有用的部分(如程序名, Pod 名等)在運行時創建資源定義。

我們只需要覆寫一件事:在 docker-compose.yml 中的 onehub 服務的 OTEL_RESOURCE_ATTRIBUTES: service.name=onehub.backend,service.version=0.0.1 環境變量。

上下文传播

上下文传播 是可观察性中非常重要的一个主题。当我们可以将来自各个支柱的信号关联起来以识别系统问题时,各个支柱的功能会呈指数级增长。可以将上下文视为可以绑定到信号的额外数据:即可以以某种独特的方式“连接”以将各种信号与特定组(例如请求)关联起来。

提供者/导出器

对于每个信号,OTel 提供了一个提供者接口(例如,用于导出跨度/跟踪的 TracerProvider,用于导出度量的 MeterProvider,用于导出日志的 LoggerProvider 等)。对于这些接口中的每一个,都可以有多种实现,例如,用于发送到 stdout/err 流的调试提供者,用于导出到另一个 OTel 终端点(在链中)的 OTel 提供者,甚至直接通过 各种导出器。但是,在我们的例子中,我们希望将任何供应商的选择推迟到我们的服务之外,而是将所有信号发送到环境中运行的 OTel 收集器。

为了抽象这一点,我们将创建一个“OTELSetup”类型,跟踪我们可能想要使用或替换的各种提供者。在 cmd/backend/obs.go 中,我们有:

Go

 

这是一个簡單的包装器,用於追踪OTel SDK所需的一般方面。這裡有提供者(Logger、Tracer和Metric)以及提供上下文(用於追蹤)的方法。所有提供者使用的上是這裡指定的覆蓋資源。關閉功能很有趣。這些是由提供者在底層導出器終止(順利或由於退出)時調用的功能。該包装器本身接受一個通用型別,以便此 Setup 的特定實例可以使用自己的自定義數據。

回购庫中包含此的两个實現:

我們將在我的應用程序中實例化第二個。我們不會详细了解特定實現,因為它們已經從SDK中的示例中取得了,並進行了小幅修復和重构。具體來說,請查看otel-collector示例以獲取靈感。

初始化OTel 提供者。

服務中啟用搜集器的核心,是所有「進入」點都会开始一种与OTel相关的「上下文」。如果这个上下文在开始时创建,那么它会被发送到此处调用的所有目标,随后会被传播(只要我们做对了事)。

以简单的ListTopics API调用(api/vi/topics)为例,我们的请求会按照以下路径来回复:

[ Browser ] ---> [ Nginx ] ---> [ gRPC Gateway ] ---> [ gRPC Service ] ---> [ Database ]

在我们这个案例中,入口点是在gRPC Gateway接收到来自Nginx的API请求时(我们本可以从HTTP请求接触到Nginx的点开始追踪,以突出Nginx的延迟,但我们会稍后再推迟)。

所需的是:

  • gRPC Gateway接收请求。
  • 它创建一个「自定义」OTel特定的context.Context实例。
  • 创建一个到相应gRPC服务的自定义连接(例如,TopicService),传递这个上下文而不是默认的上下文
  • 相应的服务在发出追踪时会使用这个上下文。

让我们一步一步来看。

初始化并准备OTel SDK使用

main.go中,首先初始化搜集器连接:

Go

 

  • 8-16行: 我們建立與在 Docker 環境中運行的 otel-collector 的連接。
  • 17-21行: 然後我們設置 OTel 設定,並為追蹤器和度量提供者設定,使我們的收集器現在可以推送所有追蹤器和度量(記得早先我們在 OTel 配置中定義了接收器)。
  • 23-25行: 設定最終清理器,在關閉時清除 OTel 連接/提供者等。
  • 27行: 如同以前一樣設定我們的數據庫和連接。
  • 29+行: 過去我們只是將 GRPC 和网关服務在后台启动,就这样。我們并没有太关心它们的返回或退出状态。对于一个更健壮的系统,深入了解我们所启动服务的生命周期是非常重要的。所以现在我们为每个要启动的服务传递了“回调”通道。当服务器退出时,各自的方法将会在这些通道上回调,表示它们已经优雅地退出了。我们的整个二进制文件将在这些服务中的任何一个退出时退出。

作为一个例子,让我们看看我们的网关服务是如何利用这个通道的。

而不是像这样启动 HTTP 服务器(用于 grpc-gateway):

1    http.ListenAndServe(gw_addr, mux)

现在我们有:

Go

 

请关注 9-14行 中,其中在单独的 goroutine 中监视服务器的关闭,15行 如果在服务器退出时出现错误,它将通过作为参数传递给这个方法的“通知”通道发送回去。

現在我們服務的各个方面都擁有活躍的OTLP連接,可在 Signals 傳送時使用。

OTel Middleware for gRPC Gateway

上方,用於啟動gRPC Gateway的http.Server實例使用自訂處理器:OTel HTTP包中的http.Handler。此處理器取用既有的http.Handler實例,用OTel上下文裝飾它,並確保其傳遞至任何其他被呼叫的下游。

Go

 

我們的HTTP處理器很简单:

  • 行4:我們建立一個新的OTel專用包装器來處理HTTP請求。
  • 行5:我們設定SpanFormatter選項,以便追蹤可以通过方法和HTTP請求路徑唯一标识。如果没有SpanNameFormatter,网关中的默认“名称”简单地是"gateway",导致所有追踪看起来像这样:

使用OTel包装网关以进行gRPC调用

默认情况下,gRPC Gateway库在创建/管理到底层GRPC服务的连接时创建一个“普通”上下文。毕竟,网关不知道任何关于OTel的东西。在这种模式下,用户/浏览器到gRPC Gateway的连接和从网关到gRPC服务的连接将被视为两条不同的追踪。

因此,将创建gRPC连接的责任从网关中移除很重要,并提供一个已经认识OTel的连接。我们现在来做这件事。

在OTel整合之前,我們是用以下方式為我們的gRPC註冊 Gateway 處理器的:

Go

 

現在傳遞不同的連接變得簡單:

Go

 

我們所做的第一步是創建一個 client第6行),它作為我們gRPC服務器的連接工廠。這個客戶端相当簡單。只使用了gRPC的ClientHandlerotelgrpc.NewClientHandler)來創建連接。這確保了在新区HTTP請求上开始的當前追踪 contexts現在通過這個處理器傳送到gRPC服務器。

就是这样。現在我們應該可以看到Gateway上新請求以及Gateway->gRPC請求作為一個集中的追踪,而不是作為兩個不同的追踪。

開始和結束跨度

我們差不多完成了。到目前为止:

  • 我們已經啟用了OTel收集器和Jaeger以接收和存儲追踪(跨度)數據(在docker-compose中)。
  • 我們建立了基本的OTel收集器(作為一個獨立的pod運行)作為我們的“提供者”來提供追踪器、度和日志(即,我們應用程序的OTel整合將使用這個端點來存放所有信號)。
  • 我們將Gateway的HTTP處理器包裹為OTel啟用,以便创建和傳播踪迹和它們的上下文。
  • 我們覆写了Gateway中的(gRPC)客戶端,使其現在包裹從我們的OTel設定中取得的OTel上下文,而不是使用默認的上下文。
  • 我們創建了全局的追踪器/度量衡器/記錄器實例,這樣我們可以使用它們來傳送實際的信號。

現在我們需要為我們程式碼中所有「有趣」的地方產生跨度。以 ListTopics 方法為例(在 services/topics.go 中):

Go

 

我們呼叫數據庫來獲取主题並返回它們。與數據庫訪問方法(在 datastore/topicds.go 中)相似:

Go

 

這裡我們主要關乎這些方法中每一個花了多少時間。我們在這些方法中簡單地創建跨度,這樣就完成了。我們對服務和數據存儲方法的增強(分別)是:

  • services/topics.go:
Go

 

  • datastore/topicds.go:
Go

 

一般的模式是:

1. 創建跨度:

ctx, span := Tracer.Start(ctx, "<span name>")

在此,給定的上下文(ctx)被「包裹」並返回一個新的上下文。我們可以(也應該)將這個新的包裹上下文傳遞給進一步的方法。當我們呼叫數據存儲 ListTopics 方法時,我們就是这样做的。

2. 結束跨度:

defer span.End()

在方法返回時結束跨度,確保記錄了正確的結束時間/代碼等。如果需要,我們還可以進行其他事情,例如向此添加标签和状态,以提供更多有助於調試的信息。

就是这样。您可以在Jaeger中看到您美麗的追蹤,並獲得越来越多關於您的請求端到端性能的洞見!

結論

本篇文章涵蓋了很多內容,但對OTel和追蹤背后的所有詳細信息來說,只是略見一斑。为了避免使這篇文章(已經非常滿)過於臃腫,我們將在下次的文章中介紹新的概念和複雜的詳細信息。現在,不妨在你的服務中試一試,並嘗試在otel-contrib倉庫中與其他 Export 者和接收器一起玩耍。

Source:
https://dzone.com/articles/tracing-with-opentelemetry-and-jaeger