Java 后端新技能!極簡代碼通過 OpenTelemetry Traces 實現零代碼監控
在現代分布式系統中,全面掌握服務間的交互流程和性能瓶頸至關重要,而分布式追蹤技術正是為此而生。OpenTelemetry 是當前廣泛使用的一種開源工具鏈,能幫助開發者在多服務架構中輕松收集、分析和可視化系統運行數據。在本文中,我們將介紹如何使用 OpenTelemetry 的**零代碼采集(Zero-code Instrumentation)**特性,結合 Java Agent 和 Grafana Tempo,實現對服務調用鏈的全面追蹤。通過一系列配置和最佳實踐,您可以在保持代碼改動最小化的同時,快速上手分布式追蹤,并獲得對系統行為的深入洞察。
我們的目標:實現系統可觀測性
讓我們先來了解背景。作為開發人員,我們希望創建易于監控、評估和理解的軟件系統。這正是實現 OpenTelemetry 的目的:最大化系統的可觀測性。
傳統的監控方法
通常,我們通過手動記錄事件、指標和錯誤來獲取應用性能數據:
圖片
市場上有許多框架支持日志管理,相信大家都配置了用于收集、存儲和分析日志的系統。
我們已經全面配置了日志系統,因此未使用 OpenTelemetry 提供的日志功能。
另一種常見的監控方式是通過 指標(Metrics):
圖片
我們也已經配置了完整的指標收集與可視化系統,因此同樣未使用 OpenTelemetry 的指標功能。
不過,對于獲取和分析系統數據,追蹤(Traces) 是一種較少被使用的工具:
追蹤描述了請求在系統中的完整生命周期路徑,通常從系統接收到請求開始,到生成響應結束。追蹤由多個 跨度(Spans) 組成,每個跨度表示特定的工作單元,由開發人員或庫定義。這些跨度形成一個層次結構,有助于直觀了解系統如何處理請求。
本文我們將重點討論 OpenTelemetry 的追蹤功能。
OpenTelemetry 的背景介紹
OpenTelemetry 項目由 OpenTracing 和 OpenCensus 合并而來。它為多種編程語言提供基于標準的全面組件,定義了一套 API、SDK 和工具,旨在生成、收集、管理和導出數據。
需要注意的是,OpenTelemetry 本身并未提供數據存儲后端或可視化工具。
由于我們關注追蹤功能,我們探索了以下開源解決方案來存儲和可視化追蹤數據:
- Jaeger
- Zipkin
- Grafana Tempo
最終,我們選擇了 Grafana Tempo,因為它提供了優秀的可視化功能、快速的開發進展,以及與我們現有的 Grafana 指標可視化設置的無縫集成。統一工具帶來了極大的便利。
OpenTelemetry 的組件
讓我們進一步剖析 OpenTelemetry 的核心組件。
規范部分
- API:定義數據類型、操作和枚舉。
- SDK:規范的實現,提供不同語言的 API(各語言的 SDK 穩定性狀態可能不同,從 Alpha 到穩定版)。
- 數據協議(OTLP) 和 語義約定。
Java API 和 SDK
- 代碼自動化庫:用于自動插樁的工具。
- 導出器(Exporters):用于將生成的追蹤數據導出到后端。
- 跨服務傳播器(Cross Service Propagators):用于將執行上下文傳播到進程(JVM)外部。
此外,還有一個重要組件:OpenTelemetry 收集器(Collector),它是一個代理,用于接收、處理和轉發數據。接下來我們將詳細了解這一組件。
OpenTelemetry 收集器(Collector)
對于每秒處理數千個請求的高負載系統,管理數據量至關重要。追蹤數據的量級通常超過業務數據,因此需要優先考慮哪些數據需要收集和存儲。這就需要通過數據處理和過濾工具來確定重要數據,例如:
- 響應時間超過特定閾值的追蹤。
- 處理過程中出現錯誤的追蹤。
- 包含特定屬性的追蹤,例如經過某些微服務的請求。
- 一部分隨機選擇的普通追蹤,用于了解系統的正常行為并識別趨勢。
圖片
兩種主要采樣方法:
- 頭部采樣(Head Sampling):在追蹤開始時決定是否保留。
- 尾部采樣(Tail Sampling):在完整追蹤數據可用后才決定。這種方法適用于依賴后續數據的決策,例如包含錯誤跨度的情況。
OpenTelemetry 收集器幫助配置數據收集系統,確保只保存必要的數據。稍后我們將探討它的配置?,F在,讓我們看看需要進行哪些代碼更改來生成追蹤數據。
零代碼監控
通過零代碼實現跟蹤生成實際上只需要極少的編碼工作——只需使用 Java Agent 啟動應用程序,并指定配置文件:
-javaagent:/opentelemetry-javaagent-1.29.0.jar
-Dotel.javaagent.configuration-file=/otel-config.properties
OpenTelemetry 支持大量庫和框架,因此,在使用 Agent 啟動應用程序后,我們立即獲得了有關服務之間請求處理階段、數據庫管理系統(DBMS)等的跟蹤數據。
在我們的 Agent 配置中,我們禁用了不希望在跟蹤中看到的庫,同時為了獲取代碼運行的數據,我們使用注解標記代碼:
@WithSpan("acquire locks")
public CompletableFuture<Lock> acquire(SortedSet<Object> source) {
var traceLocks = source.stream().map(Object::toString).collect(joining(", "));
Span.current().setAttribute("locks", traceLocks);
return CompletableFuture.supplyAsync(() -> /* async job */);
}
在這個示例中,@WithSpan 注解被用于方法,表示需要為該方法創建一個名為“acquire locks”的新 Span。在方法主體中,還為創建的 Span 添加了一個名為“locks”的屬性。
當方法運行完成時,Span 會被關閉。對于異步代碼,這一點需要特別注意。如果需要獲取異步代碼中 lambda 表達式的運行數據,建議將這些 lambda 表達式分離到單獨的方法中,并標記上附加的注解。
我們的跟蹤數據收集配置
接下來,我們來討論如何配置完整的跟蹤數據收集系統。我們所有的 JVM 應用程序都使用 Java Agent 啟動,并將數據發送到 OpenTelemetry 收集器。
然而,單個收集器無法處理大量的數據流,因此該系統需要進行擴展。如果為每個 JVM 應用程序啟動一個單獨的收集器,尾部采樣(tail sampling)將無法正常工作,因為跟蹤分析必須在一個收集器上完成。如果請求跨越多個 JVM,單個跟蹤的不同 Span 將會被分散到不同的收集器上,無法進行統一分析。
在這種情況下,一個作為負載均衡器的收集器配置派上用場。
最終,我們獲得了如下系統架構:每個 JVM 應用程序將數據發送到同一個負載均衡收集器,該收集器的唯一任務是將來自不同應用程序但屬于同一跟蹤的數據分配到同一個收集器處理器(collector-processor)。然后,收集器處理器將數據發送到 Grafana Tempo。
圖片
接下來,我們詳細分析系統中各組件的配置。
負載均衡收集器
在負載均衡收集器的配置中,我們配置了以下主要部分:
receivers:
otlp:
protocols:
grpc:
exporters:
loadbalancing:
protocol:
otlp:
tls:
insecure:true
resolver:
static:
hostnames:
- collector-1.example.com:4317
- collector-2.example.com:4317
- collector-3.example.com:4317
service:
pipelines:
traces:
receivers:[otlp]
exporters:[loadbalancing]
- Receivers:配置收集器接收數據的方法。在這里我們僅配置了以 OTLP 格式接收數據(還可以通過多種協議接收數據,例如 Zipkin、Jaeger)。
- Exporters:配置數據負載均衡的部分。該部分會根據跟蹤標識符的哈希值,在指定的處理器收集器間分配數據。
- Service 部分:配置服務的工作方式,僅處理跟蹤數據,使用上方配置的 OTLP 接收器并以負載均衡模式傳輸數據,而不進行處理。
數據處理收集器
處理器收集器的配置更為復雜,讓我們詳細了解:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:14317
processors:
tail_sampling:
decision_wait: 10s
num_traces:100
expected_new_traces_per_sec:10
policies:
[
{
name: latency500-policy,
type: latency,
latency:{threshold_ms:500}
},
{
name: error-policy,
type: string_attribute,
string_attribute:{key: error,values:[true,True]}
},
{
name: probabilistic10-policy,
type: probabilistic,
probabilistic:{sampling_percentage:10}
}
]
resource/delete:
attributes:
-key: process.command_line
action: delete
-key: process.executable.path
action: delete
-key: process.pid
action: delete
-key: process.runtime.description
action: delete
-key: process.runtime.name
action: delete
-key: process.runtime.version
action: delete
exporters:
otlp:
endpoint: tempo:4317
tls:
insecure:true
service:
pipelines:
traces:
receivers:[otlp]
exporters:[otlp]
與負載均衡收集器類似,處理器收集器也包含 Receivers、Exporters 和 Service 部分。但這里重點是 Processors 部分,它定義了數據的處理方式。
- tail_sampling部分:配置了用于存儲和分析所需數據的篩選規則:
a.latency500-policy:篩選延遲超過 500 毫秒的跟蹤。
b.error-policy:篩選在處理過程中發生錯誤的跟蹤,查找 Span 中名為“error”的字符串屬性,其值為“true”或“True”。
c.probabilistic10-policy:隨機選擇 10% 的所有跟蹤,用于分析正常操作、錯誤和長時間請求的處理情況。
- resource/delete 部分:刪除不必要的屬性,以減少存儲和分析的冗余數據。
最終效果
通過 Grafana 的跟蹤搜索窗口,我們可以按各種條件篩選數據。例如,下圖顯示了來自處理游戲元數據的 Lobby 服務的跟蹤列表:
圖片
在跟蹤視圖窗口中,可以看到 Lobby 服務執行的時間軸,包括構成請求的各種 Span:
圖片
查看 Span 時,可以看到詳細屬性,例如數據庫查詢:
圖片
Grafana Tempo 的一個有趣功能是服務圖,它以圖形化方式展示導出跟蹤的所有服務、它們之間的連接、請求速率和延遲:
圖片
總結
通過本文的實踐,我們展示了如何利用 OpenTelemetry 的零代碼采集功能,實現對分布式系統中服務交互的高效追蹤。使用 Java Agent 和 Grafana Tempo,我們能夠輕松集成并可視化跨服務的調用鏈路。更重要的是,通過靈活的 Collector 配置,我們實現了高效的尾采樣策略,為后續的性能分析和問題排查提供了可靠的數據支持。
分布式追蹤不僅提升了系統的可觀測性,更為優化服務性能、增強系統可靠性奠定了基礎。借助 OpenTelemetry 和 Grafana Tempo 的可視化能力,開發團隊可以快速發現和解決性能瓶頸,使系統能夠更好地應對復雜業務場景的挑戰。未來,我們期待結合更多功能擴展,如日志關聯和指標分析,進一步完善系統的可觀測性工具鏈。