Java 后端新技能!極簡(jiǎn)代碼通過(guò) OpenTelemetry Traces 實(shí)現(xiàn)零代碼監(jiān)控
在現(xiàn)代分布式系統(tǒng)中,全面掌握服務(wù)間的交互流程和性能瓶頸至關(guān)重要,而分布式追蹤技術(shù)正是為此而生。OpenTelemetry 是當(dāng)前廣泛使用的一種開(kāi)源工具鏈,能幫助開(kāi)發(fā)者在多服務(wù)架構(gòu)中輕松收集、分析和可視化系統(tǒng)運(yùn)行數(shù)據(jù)。在本文中,我們將介紹如何使用 OpenTelemetry 的**零代碼采集(Zero-code Instrumentation)**特性,結(jié)合 Java Agent 和 Grafana Tempo,實(shí)現(xiàn)對(duì)服務(wù)調(diào)用鏈的全面追蹤。通過(guò)一系列配置和最佳實(shí)踐,您可以在保持代碼改動(dòng)最小化的同時(shí),快速上手分布式追蹤,并獲得對(duì)系統(tǒng)行為的深入洞察。
我們的目標(biāo):實(shí)現(xiàn)系統(tǒng)可觀測(cè)性
讓我們先來(lái)了解背景。作為開(kāi)發(fā)人員,我們希望創(chuàng)建易于監(jiān)控、評(píng)估和理解的軟件系統(tǒng)。這正是實(shí)現(xiàn) OpenTelemetry 的目的:最大化系統(tǒng)的可觀測(cè)性。
傳統(tǒng)的監(jiān)控方法
通常,我們通過(guò)手動(dòng)記錄事件、指標(biāo)和錯(cuò)誤來(lái)獲取應(yīng)用性能數(shù)據(jù):
圖片
市場(chǎng)上有許多框架支持日志管理,相信大家都配置了用于收集、存儲(chǔ)和分析日志的系統(tǒng)。
我們已經(jīng)全面配置了日志系統(tǒng),因此未使用 OpenTelemetry 提供的日志功能。
另一種常見(jiàn)的監(jiān)控方式是通過(guò) 指標(biāo)(Metrics):
圖片
我們也已經(jīng)配置了完整的指標(biāo)收集與可視化系統(tǒng),因此同樣未使用 OpenTelemetry 的指標(biāo)功能。
不過(guò),對(duì)于獲取和分析系統(tǒng)數(shù)據(jù),追蹤(Traces) 是一種較少被使用的工具:
追蹤描述了請(qǐng)求在系統(tǒng)中的完整生命周期路徑,通常從系統(tǒng)接收到請(qǐng)求開(kāi)始,到生成響應(yīng)結(jié)束。追蹤由多個(gè) 跨度(Spans) 組成,每個(gè)跨度表示特定的工作單元,由開(kāi)發(fā)人員或庫(kù)定義。這些跨度形成一個(gè)層次結(jié)構(gòu),有助于直觀了解系統(tǒng)如何處理請(qǐng)求。
本文我們將重點(diǎn)討論 OpenTelemetry 的追蹤功能。
OpenTelemetry 的背景介紹
OpenTelemetry 項(xiàng)目由 OpenTracing 和 OpenCensus 合并而來(lái)。它為多種編程語(yǔ)言提供基于標(biāo)準(zhǔn)的全面組件,定義了一套 API、SDK 和工具,旨在生成、收集、管理和導(dǎo)出數(shù)據(jù)。
需要注意的是,OpenTelemetry 本身并未提供數(shù)據(jù)存儲(chǔ)后端或可視化工具。
由于我們關(guān)注追蹤功能,我們探索了以下開(kāi)源解決方案來(lái)存儲(chǔ)和可視化追蹤數(shù)據(jù):
- Jaeger
- Zipkin
- Grafana Tempo
最終,我們選擇了 Grafana Tempo,因?yàn)樗峁┝藘?yōu)秀的可視化功能、快速的開(kāi)發(fā)進(jìn)展,以及與我們現(xiàn)有的 Grafana 指標(biāo)可視化設(shè)置的無(wú)縫集成。統(tǒng)一工具帶來(lái)了極大的便利。
OpenTelemetry 的組件
讓我們進(jìn)一步剖析 OpenTelemetry 的核心組件。
規(guī)范部分
- API:定義數(shù)據(jù)類(lèi)型、操作和枚舉。
- SDK:規(guī)范的實(shí)現(xiàn),提供不同語(yǔ)言的 API(各語(yǔ)言的 SDK 穩(wěn)定性狀態(tài)可能不同,從 Alpha 到穩(wěn)定版)。
- 數(shù)據(jù)協(xié)議(OTLP) 和 語(yǔ)義約定。
Java API 和 SDK
- 代碼自動(dòng)化庫(kù):用于自動(dòng)插樁的工具。
- 導(dǎo)出器(Exporters):用于將生成的追蹤數(shù)據(jù)導(dǎo)出到后端。
- 跨服務(wù)傳播器(Cross Service Propagators):用于將執(zhí)行上下文傳播到進(jìn)程(JVM)外部。
此外,還有一個(gè)重要組件:OpenTelemetry 收集器(Collector),它是一個(gè)代理,用于接收、處理和轉(zhuǎn)發(fā)數(shù)據(jù)。接下來(lái)我們將詳細(xì)了解這一組件。
OpenTelemetry 收集器(Collector)
對(duì)于每秒處理數(shù)千個(gè)請(qǐng)求的高負(fù)載系統(tǒng),管理數(shù)據(jù)量至關(guān)重要。追蹤數(shù)據(jù)的量級(jí)通常超過(guò)業(yè)務(wù)數(shù)據(jù),因此需要優(yōu)先考慮哪些數(shù)據(jù)需要收集和存儲(chǔ)。這就需要通過(guò)數(shù)據(jù)處理和過(guò)濾工具來(lái)確定重要數(shù)據(jù),例如:
- 響應(yīng)時(shí)間超過(guò)特定閾值的追蹤。
- 處理過(guò)程中出現(xiàn)錯(cuò)誤的追蹤。
- 包含特定屬性的追蹤,例如經(jīng)過(guò)某些微服務(wù)的請(qǐng)求。
- 一部分隨機(jī)選擇的普通追蹤,用于了解系統(tǒng)的正常行為并識(shí)別趨勢(shì)。
圖片
兩種主要采樣方法:
- 頭部采樣(Head Sampling):在追蹤開(kāi)始時(shí)決定是否保留。
- 尾部采樣(Tail Sampling):在完整追蹤數(shù)據(jù)可用后才決定。這種方法適用于依賴(lài)后續(xù)數(shù)據(jù)的決策,例如包含錯(cuò)誤跨度的情況。
OpenTelemetry 收集器幫助配置數(shù)據(jù)收集系統(tǒng),確保只保存必要的數(shù)據(jù)。稍后我們將探討它的配置?,F(xiàn)在,讓我們看看需要進(jìn)行哪些代碼更改來(lái)生成追蹤數(shù)據(jù)。
零代碼監(jiān)控
通過(guò)零代碼實(shí)現(xiàn)跟蹤生成實(shí)際上只需要極少的編碼工作——只需使用 Java Agent 啟動(dòng)應(yīng)用程序,并指定配置文件:
-javaagent:/opentelemetry-javaagent-1.29.0.jar
-Dotel.javaagent.configuration-file=/otel-config.properties
OpenTelemetry 支持大量庫(kù)和框架,因此,在使用 Agent 啟動(dòng)應(yīng)用程序后,我們立即獲得了有關(guān)服務(wù)之間請(qǐng)求處理階段、數(shù)據(jù)庫(kù)管理系統(tǒng)(DBMS)等的跟蹤數(shù)據(jù)。
在我們的 Agent 配置中,我們禁用了不希望在跟蹤中看到的庫(kù),同時(shí)為了獲取代碼運(yùn)行的數(shù)據(jù),我們使用注解標(biāo)記代碼:
@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 */);
}
在這個(gè)示例中,@WithSpan 注解被用于方法,表示需要為該方法創(chuàng)建一個(gè)名為“acquire locks”的新 Span。在方法主體中,還為創(chuàng)建的 Span 添加了一個(gè)名為“l(fā)ocks”的屬性。
當(dāng)方法運(yùn)行完成時(shí),Span 會(huì)被關(guān)閉。對(duì)于異步代碼,這一點(diǎn)需要特別注意。如果需要獲取異步代碼中 lambda 表達(dá)式的運(yùn)行數(shù)據(jù),建議將這些 lambda 表達(dá)式分離到單獨(dú)的方法中,并標(biāo)記上附加的注解。
我們的跟蹤數(shù)據(jù)收集配置
接下來(lái),我們來(lái)討論如何配置完整的跟蹤數(shù)據(jù)收集系統(tǒng)。我們所有的 JVM 應(yīng)用程序都使用 Java Agent 啟動(dòng),并將數(shù)據(jù)發(fā)送到 OpenTelemetry 收集器。
然而,單個(gè)收集器無(wú)法處理大量的數(shù)據(jù)流,因此該系統(tǒng)需要進(jìn)行擴(kuò)展。如果為每個(gè) JVM 應(yīng)用程序啟動(dòng)一個(gè)單獨(dú)的收集器,尾部采樣(tail sampling)將無(wú)法正常工作,因?yàn)楦櫡治霰仨氃谝粋€(gè)收集器上完成。如果請(qǐng)求跨越多個(gè) JVM,單個(gè)跟蹤的不同 Span 將會(huì)被分散到不同的收集器上,無(wú)法進(jìn)行統(tǒng)一分析。
在這種情況下,一個(gè)作為負(fù)載均衡器的收集器配置派上用場(chǎng)。
最終,我們獲得了如下系統(tǒng)架構(gòu):每個(gè) JVM 應(yīng)用程序?qū)?shù)據(jù)發(fā)送到同一個(gè)負(fù)載均衡收集器,該收集器的唯一任務(wù)是將來(lái)自不同應(yīng)用程序但屬于同一跟蹤的數(shù)據(jù)分配到同一個(gè)收集器處理器(collector-processor)。然后,收集器處理器將數(shù)據(jù)發(fā)送到 Grafana Tempo。
圖片
接下來(lái),我們?cè)敿?xì)分析系統(tǒng)中各組件的配置。
負(fù)載均衡收集器
在負(fù)載均衡收集器的配置中,我們配置了以下主要部分:
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:配置收集器接收數(shù)據(jù)的方法。在這里我們僅配置了以 OTLP 格式接收數(shù)據(jù)(還可以通過(guò)多種協(xié)議接收數(shù)據(jù),例如 Zipkin、Jaeger)。
- Exporters:配置數(shù)據(jù)負(fù)載均衡的部分。該部分會(huì)根據(jù)跟蹤標(biāo)識(shí)符的哈希值,在指定的處理器收集器間分配數(shù)據(jù)。
- Service 部分:配置服務(wù)的工作方式,僅處理跟蹤數(shù)據(jù),使用上方配置的 OTLP 接收器并以負(fù)載均衡模式傳輸數(shù)據(jù),而不進(jìn)行處理。
數(shù)據(jù)處理收集器
處理器收集器的配置更為復(fù)雜,讓我們?cè)敿?xì)了解:
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]
與負(fù)載均衡收集器類(lèi)似,處理器收集器也包含 Receivers、Exporters 和 Service 部分。但這里重點(diǎn)是 Processors 部分,它定義了數(shù)據(jù)的處理方式。
- tail_sampling部分:配置了用于存儲(chǔ)和分析所需數(shù)據(jù)的篩選規(guī)則:
a.latency500-policy:篩選延遲超過(guò) 500 毫秒的跟蹤。
b.error-policy:篩選在處理過(guò)程中發(fā)生錯(cuò)誤的跟蹤,查找 Span 中名為“error”的字符串屬性,其值為“true”或“True”。
c.probabilistic10-policy:隨機(jī)選擇 10% 的所有跟蹤,用于分析正常操作、錯(cuò)誤和長(zhǎng)時(shí)間請(qǐng)求的處理情況。
- resource/delete 部分:刪除不必要的屬性,以減少存儲(chǔ)和分析的冗余數(shù)據(jù)。
最終效果
通過(guò) Grafana 的跟蹤搜索窗口,我們可以按各種條件篩選數(shù)據(jù)。例如,下圖顯示了來(lái)自處理游戲元數(shù)據(jù)的 Lobby 服務(wù)的跟蹤列表:
圖片
在跟蹤視圖窗口中,可以看到 Lobby 服務(wù)執(zhí)行的時(shí)間軸,包括構(gòu)成請(qǐng)求的各種 Span:
圖片
查看 Span 時(shí),可以看到詳細(xì)屬性,例如數(shù)據(jù)庫(kù)查詢(xún):
圖片
Grafana Tempo 的一個(gè)有趣功能是服務(wù)圖,它以圖形化方式展示導(dǎo)出跟蹤的所有服務(wù)、它們之間的連接、請(qǐng)求速率和延遲:
圖片
總結(jié)
通過(guò)本文的實(shí)踐,我們展示了如何利用 OpenTelemetry 的零代碼采集功能,實(shí)現(xiàn)對(duì)分布式系統(tǒng)中服務(wù)交互的高效追蹤。使用 Java Agent 和 Grafana Tempo,我們能夠輕松集成并可視化跨服務(wù)的調(diào)用鏈路。更重要的是,通過(guò)靈活的 Collector 配置,我們實(shí)現(xiàn)了高效的尾采樣策略,為后續(xù)的性能分析和問(wèn)題排查提供了可靠的數(shù)據(jù)支持。
分布式追蹤不僅提升了系統(tǒng)的可觀測(cè)性,更為優(yōu)化服務(wù)性能、增強(qiáng)系統(tǒng)可靠性奠定了基礎(chǔ)。借助 OpenTelemetry 和 Grafana Tempo 的可視化能力,開(kāi)發(fā)團(tuán)隊(duì)可以快速發(fā)現(xiàn)和解決性能瓶頸,使系統(tǒng)能夠更好地應(yīng)對(duì)復(fù)雜業(yè)務(wù)場(chǎng)景的挑戰(zhàn)。未來(lái),我們期待結(jié)合更多功能擴(kuò)展,如日志關(guān)聯(lián)和指標(biāo)分析,進(jìn)一步完善系統(tǒng)的可觀測(cè)性工具鏈。