头图

原文链接: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,那么应用程序将拥有前所未有的可观测性,而不必担心性能损耗或兼容性问题。

相关资源

这一整套生态的组合,能够让你快速搭建高性能、易扩展、且可全面观测的分布式系统。


观测云的思考

性能对比:零成本抽象带来的优势

与传统的 tokio-rs/tracing 相比,Fastrace 的零成本抽象(Zero-cost Abstraction)设计在未启用时完全移除追踪代码,不会对运行时产生任何性能开销。而 tokio-rs/tracing 即使在未采集数据的情况下,仍会有一定的性能损耗。

此外,Fastrace 的上下文传播是自动化且无缝的,而 tokio-rs/tracing 则需要手动处理上下文,增加了复杂度和潜在的错误风险。

一站式解决方案:Fastrace + 观测云

Fastrace 的强大分布式追踪能力不仅能帮助开发者高效追踪微服务调用链,还能够无缝对接到观测云平台,实现更加全面的可观测性监控。通过将 Fastrace 的 OpenTelemetry 数据直接接入观测云,开发者可以在统一的平台上实时查看链路追踪、性能瓶颈以及跨服务的调用关系,大幅提升问题排查和性能优化的效率。无论是调试复杂的微服务系统,还是在生产环境中快速定位故障,该组合都能以较低的接入成本带来卓越的性能监控体验,真正实现“全链路可观测,一站式可视化”。


观测云
21 声望85 粉丝

云时代的系统可观测平台