原文鏈接:https://fast.github.io/blog/fastrace-a-modern-approach-to-dis...
摘要
在微服務架構中,分佈式追蹤對於理解應用程序的行為至關重要。雖然 tokio-rs/tracing 在 Rust 中被廣泛使用,但它存在一些顯著的挑戰:生態系統碎片化、配置複雜以及高開銷。
Fastrace 提供了一個可用於生產環境的解決方案,具有無縫的生態系統集成、開箱即用的 OpenTelemetry 支持,以及更簡潔的 API,能夠自然地與現有的日誌基礎設施協同工作。
以下示例展示瞭如何使用 fastrace 對函數進行追蹤:
#[fastrace::trace]
pub fn send_request(req: HttpRequest) -> Result<(), Error> {
// ...
}
Fastrace 已在如 ScopeDB 等產品中投入生產使用,幫助追蹤和調試 PB 級的可觀測性數據工作負載。
為什麼分佈式追蹤很重要
在當今的微服務和分佈式系統中,瞭解應用程序內部發生的事情變得前所未有地困難。一個用户請求可能在完成之前涉及數十個服務,傳統的日誌記錄方法很快就會顯得不足。
考慮一個典型的請求流程:
用户 → API 網關 → 認證服務 → 用户服務 → 數據庫
當發生異常或應用程序性能不佳時,問題究竟發生在哪裏?單個服務的日誌只顯示了追蹤的片段,缺乏整個系統中請求流動的關鍵上下文。
這使得分佈式追蹤變得至關重要。追蹤創建了一個跨服務邊界的請求流程的連接視圖,使得能夠:
- 識別跨服務的性能瓶頸
- 調試組件之間的複雜交互
- 瞭解依賴關係和服務關係
- 分析延遲分佈和異常值
- 將日誌和指標與請求上下文相關聯
一個常見的方案:tokio-rs/tracing
對於一些 Rust 開發者來説,tokio-rs/tracing 是實現追蹤的首選解決方案。以下是一個典型例子:
fn main() {
// 初始化 tracing 訂閲者
// 省略複雜的配置代碼...
// 創建一個 span 並記錄一些數據
let span = tracing::info_span!("processing_request",
user_id = 42,
request_id = "abcd1234"
);
// 進入 span(為當前執行上下文激活它)
let _guard = span.enter();
// 在 span 上下文中記錄日誌
tracing::info!("Starting request processing");
process_data();
tracing::info!("Finished processing request");
}
tokio-rs/tracing 提供了過程宏簡化函數插樁:
#[tracing::instrument(skip(password), fields(user_id = user.id))]
async fn authenticate(user: &User, password: &str) -> Result<AuthToken, AuthError> {
tracing::info!("Authenticating user {}", user.id);
// ...更多代碼...
}
tokio-rs/tracing 的問題
根據我們的用户體驗,tokio-rs/tracing 存在幾個顯著的問題:
1. 生態系統碎片化
tokio-rs/tracing引入自己的日誌宏,與使用標準 log crate 的代碼產生了分歧:
// 使用 log crate
log::info!("Starting operation");
// 使用 tracing crate(不同的語法)
tracing::info!("Starting operation");
這種碎片化對庫作者尤其成問題。在創建庫時,作者將面臨一個困難的選擇:
- 使用 log crate,以兼容更廣泛的生態系統
- 使用 tokio-rs/tracing,以獲得更好的可觀測性功能
許多庫為了簡單選擇了第一種方式,但錯過了追蹤的好處。
雖然 tokio-rs/tracing 提供了一個 log 特性標誌,允許在使用 tokio-rs/tracing 的宏時向 log crate 發出日誌記錄,但庫作者必須手動啓用這個特性,以確保所有用户無論使用哪種日誌框架都能正確接收日誌記錄。這為庫維護者帶來了額外的配置複雜性。
此外,使用 tokio-rs/tracing 的應用程序還必須安裝和配置 tracing-log 橋接器,以正確接收使用 log crate 的庫的日誌記錄。這造成了一個需要顯式配置的雙向兼容性問題:
# Library's Cargo.toml
[dependencies]
tracing = { version = "0.1", features = ["log"] } # Emit log records for log compatibility
# Application's Cargo.toml
[dependencies]
tracing = "0.1"
tracing-log = "0.2" # Listen to log records for log compatibility
2. 對庫的性能影響
庫的作者對性能開銷特別敏感,因為他們的代碼可能會在循環或性能關鍵路徑中被調用。當使用 tokio-rs/tracing 進行檢測時,其開銷可能相當顯著,這帶來了一個兩難的選擇:
- 始終進行追蹤檢測 —— 這樣會對所有用户都帶來額外的性能開銷。
- 完全不進行檢測 —— 這樣會失去可觀測性。
- 創建一個額外的特性標誌系統 —— 增加維護成本和複雜度。
以下是使用 tokio-rs/tracing 的庫中常見的模式:
#[cfg_attr(feature = "tracing", tracing::instrument(skip(password), fields(user_id = user.id)))]
async fn authenticate(user: &User, password: &str) -> Result<AuthToken, AuthError> {
// ...更多代碼...
}
不同的庫可能會定義稍有差異的特性名稱,這使得最終的應用程序在配置這些標誌時變得十分複雜。
對於 tokio-rs/tracing 來説,目前並沒有一種乾淨的方式來實現“零成本的禁用”(zero-cost disabled)。這導致庫的作者不願意在性能敏感的代碼路徑中添加檢測邏輯。
3. 不支持上下文傳播
分佈式追蹤要求在服務邊界之間傳播上下文信息,但 tokio-rs/tracing 大部分情況下將這個任務留給了開發者來手動處理。例如,下面是 tonic 官方提供的 gRPC 服務追蹤示例:
Server::builder()
.trace_fn(|_| tracing::info_span!("grpc_server"))
.add_service(MyServiceServer::new(MyService::default()))
.serve(addr)
.await?;
上述示例僅僅創建了一個基礎的 span,但是並沒有從傳入的請求中提取追蹤上下文。
在分佈式系統中,缺乏上下文傳播會導致嚴重的後果。當由於上下文缺失而導致追蹤斷開時,你將無法看到完整的請求流,例如:
期望的完整追蹤流:
Trace #1: 前端 → API 網關 → 用户服務 → 數據庫 → 響應
實際看到的卻是斷開的片段:
Trace #1: 前端 → API 網關
Trace #2: 用户服務 → 數據庫
Trace #3: API 網關 → 響應
更糟糕的是,當多個請求交錯執行時,這些追蹤片段會變得混亂:
Trace #1: 前端 → API 網關
Trace #2: 前端 → API 網關
Trace #3: 前端 → API 網關
Trace #4: 用户服務 → 數據庫
Trace #6: API 網關 → 響應
Trace #5: 用户服務 → 數據庫
這種碎片化會極大地增加跟蹤請求流、隔離性能問題以及理解服務之間因果關係的難度,影響調試和優化的效率。
引入 fastrace:一個快速而完整的解決方案
1. 零成本抽象(Zero-cost Abstraction)
fastrace 設計時採用了真正的零成本抽象。當禁用追蹤時,所有的追蹤代碼會在編譯期間被完全移除,因此不會產生任何運行時開銷。這使得它非常適合對性能要求敏感的庫使用。
2. 生態系統兼容性(Ecosystem Compatibility)
fastrace 專注於分佈式追蹤,並通過可組合的設計與現有的 Rust 生態系統無縫集成,包括對標準 log crate 的支持。這種架構設計允許庫實現全面的追蹤功能,同時保留用户選擇其偏好的日誌設置的自由。
3. 簡潔優先(Simplicity First)
API 設計直觀且簡潔,減少了模板代碼的編寫,專注於最常見的使用場景,同時在需要時提供擴展能力。
4. 極致性能(Insanely Fast)
fastrace 為高性能應用而生,能夠處理大量的 span(追蹤片段),並且對 CPU 和內存的使用影響極小。
5. 應用與庫的雙重適配(Ergonomic for both Applications and Libraries)
fastrace 可以在不引入性能開銷的情況下被庫使用:
#[fastrace::trace] // 當未啓用 "enable" 特性時,是真正零成本的
pub fn process_data(data: &[u8]) -> Result<Vec<u8>, Error> {
// 庫內部使用標準 log crate
log::debug!("Processing {} bytes of data", data.len());
// ...更多代碼...
}
關鍵在於庫引入 fastrace 時,不需要開啓任何特性:
[dependencies]
fastrace = "0.7" # 不啓用 "enable" 特性
當應用程序使用該庫且沒有啓用 fastrace 的 “enable” 特性時:
- 所有追蹤代碼在編譯時會被完全優化掉
- 不會引入任何運行時開銷
- 對性能關鍵路徑沒有任何影響
而當應用程序啓用了 enable 特性時:
- 庫中的檢測邏輯會被激活
- span 會被收集並上報
- 應用能夠對庫的內部行為進行全面可視化
這種設計相較於傳統追蹤解決方案有顯著優勢,傳統方案通常會始終引入開銷,或者要求庫作者實現複雜的特性標誌系統。
6. 無縫的上下文傳播(Seamless Context Propagation)
fastrace 提供了多種主流框架的集成庫,能夠自動處理上下文傳播:
- HTTP 客户端(reqwest)
let response = client.get(&format!("https://user-service/users/{}", user_id))
.headers(fastrace_reqwest::traceparent_headers()) // 自動注入追蹤上下文
.send()
.await?;
- gRPC 服務端(tonic)
Server::builder()
.layer(fastrace_tonic::FastraceServerLayer) // 自動從請求中提取上下文
.add_service(MyServiceServer::new(MyService::default()))
.serve(addr);
- gRPC 客户端
let channel = ServiceBuilder::new()
.layer(fastrace_tonic::FastraceClientLayer) // 自動注入上下文到請求中
.service(channel);
- 數據訪問(Apache OpenDAL)
let op = Operator::new(services::Memory::default())?
.layer(opendal::layers::FastraceLayer) // 自動追蹤所有數據操作
.finish();
op.write("test", "0".repeat(16 * 1024 * 1024).into_bytes())
.await?;
通過這些集成,fastrace 實現了開箱即用的分佈式追蹤,無需手動處理上下文傳播,大幅簡化了開發者的工作量。
完整的解決方案:fastrace + log + logforth
fastrace 專注於做好一件事:分佈式追蹤。通過它的可組合設計以及對 Rust 生態的良好支持,與以下工具共同構建了一個強大的可觀測性解決方案:
- log:Rust 的標準日誌接口,用於基礎日誌記錄。
- logforth:具有工業級特性的靈活日誌實現,支持更復雜的日誌管理與調度。
- fastrace:高性能的分佈式追蹤,支持上下文傳播和跨服務鏈路跟蹤。
這種集成可以讓日誌自動關聯到追蹤 span,不需要額外切換不同的日誌宏:
log::info!("Processing started");
在你的日誌基礎設施中,你可以清楚地看到每個日誌條目對應的追蹤 ID 和 span,便於更高效的關聯和分析。
完整示例:構建一個具備完整可觀測性的微服務
以下是一個基於 fastrace、log 和 logforth 的簡潔微服務示例:
#[poem::handler]
#[fastrace::trace] // 自動創建並管理 span
async fn get_user(Path(user_id): Path<String>) -> Json<User> {
// 標準日誌會自動關聯到當前 span
log::info!("Fetching user {}", user_id);
let user_details = fetch_user_details(&user_id).await;
Json(User {
id: user_id,
name: user_details.name,
email: user_details.email,
})
}
子任務的追蹤:
#[fastrace::trace]
async fn fetch_user_details(user_id: &str) -> UserDetails {
let client = reqwest::Client::new();
let response = client.get(&format!("https://user-details-service/users/{}", user_id))
.headers(fastrace_reqwest::traceparent_headers()) // 自動傳播追蹤上下文
.send()
.await
.expect("Request failed");
response.json::<UserDetails>().await.expect("Failed to parse JSON")
}
主服務的配置和啓動:
#[tokio::main]
async fn main() {
// 配置日誌和追蹤
setup_observability("user-service");
let app = poem::Route::new()
.at("/users/:id", poem::get(get_user))
.with(fastrace_poem::FastraceMiddleware); // 自動提取追蹤上下文
poem::Server::new(poem::listener::TcpListener::bind("0.0.0.0:3000"))
.run(app)
.await
.unwrap();
fastrace::flush();
}
日誌與追蹤的初始化:
fn setup_observability(service_name: &str) {
// 配置 logforth 進行日誌管理
logforth::stderr()
.dispatch(|d| {
d.filter(log::LevelFilter::Info)
// 將追蹤 ID 附加到日誌
.diagnostic(logforth::diagnostic::FastraceDiagnostic::default())
// 將日誌附加到 span
.append(logforth::append::FastraceEvent::default())
})
.apply();
// 配置 fastrace 進行分佈式追蹤
fastrace::set_reporter(
fastrace_jaeger::JaegerReporter::new("127.0.0.1:6831".parse().unwrap(), service_name).unwrap(),
fastrace::collector::Config::default()
);
}
總結
fastrace 代表了 Rust 中分佈式追蹤的現代化解決方案,主要具備以下顯著優勢:
- 零運行時開銷(Zero Runtime Overhead When Disabled):
- 當應用未啓用追蹤時,庫中的檢測代碼會被完全優化掉,不會影響性能。
- 無生態鎖定(No Ecosystem Lock-In):
- 使用 fastrace 不會強制用户依賴某一特定日誌系統,可靈活適配 log、logforth 等多種實現。
- 簡單的 API 接口(Simple API Surface):
- 簡潔的 API 設計,讓開發者能夠輕鬆實現全面追蹤,而無需複雜的配置。
- 可預測的性能表現(Predictable Performance):
- 即使在高負載下,fastrace 的性能依舊穩定、可預測。
如果生態中的庫都能全面支持 fastrace,那麼應用程序將擁有前所未有的可觀測性,而不必擔心性能損耗或兼容性問題。
相關資源
- https://github.com/fast/fastrace
- https://crates.io/crates/fastrace-jaeger
- https://crates.io/crates/fastrace-opentelemetry
- https://crates.io/crates/fastrace-reqwest
- https://crates.io/crates/fastrace-poem
- https://crates.io/crates/fastrace-tonic
- https://crates.io/crates/logforth
這一整套生態的組合,能夠讓你快速搭建高性能、易擴展、且可全面觀測的分佈式系統。
觀測雲的思考
性能對比:零成本抽象帶來的優勢
與傳統的 tokio-rs/tracing 相比,Fastrace 的零成本抽象(Zero-cost Abstraction)設計在未啓用時完全移除追蹤代碼,不會對運行時產生任何性能開銷。而 tokio-rs/tracing 即使在未採集數據的情況下,仍會有一定的性能損耗。
此外,Fastrace 的上下文傳播是自動化且無縫的,而 tokio-rs/tracing 則需要手動處理上下文,增加了複雜度和潛在的錯誤風險。
一站式解決方案:Fastrace + 觀測雲
Fastrace 的強大分佈式追蹤能力不僅能幫助開發者高效追蹤微服務調用鏈,還能夠無縫對接到觀測雲平台,實現更加全面的可觀測性監控。通過將 Fastrace 的 OpenTelemetry 數據直接接入觀測雲,開發者可以在統一的平台上實時查看鏈路追蹤、性能瓶頸以及跨服務的調用關係,大幅提升問題排查和性能優化的效率。無論是調試複雜的微服務系統,還是在生產環境中快速定位故障,該組合都能以較低的接入成本帶來卓越的性能監控體驗,真正實現“全鏈路可觀測,一站式可視化”。