Skip to content

How to trace GreptimeDB

GreptimeDB 使用 Rust 的 tracing 框架进行代码埋点,tracing 的具体原理和使用方法参见 tracing 的官方文档。

通过将 trace_id 等信息在整个分布式数据链路上透传,使得我们能够记录整个分布式链路的函数调用链,知道每个被追踪函数的调用时间等相关信息,从而对整个系统进行诊断。

在 RPC 中定义 tracing 上下文

因为 tracing 框架并没有原生支持分布式追踪,我们需要手动将 trace_id 等信息在 RPC 消息中传递,从而正确的识别函数的调用关系。我们使用基于 w3c 的标准 将相关信息编码为 tracing_context ,将消息附在 RPC 的 header 中。主要定义在:

  • frontenddatanode 交互:tracing_context 定义在 RegionRequestHeader
  • frontendmetasrv 交互:tracing_context 定义在 RequestHeader
  • Client 与 frontend 交互:tracing_context 定义在 RequestHeader

在 RPC 调用中传递 tracing 上下文

我们构建了一个 TracingContext 结构体,封装了与 tracing 上下文有关的操作。相关代码

GreptimeDB 在使用 TracingContext::from_current_span() 获取当前 tracing 上下文,使用 to_w3c() 方法将 tracing 上下文编码为符合 w3c 的格式,并将其附在 RPC 消息中,从而使 tracing 上下文正确的在分布式组件之中传递。

下面的例子说明了如何获取当前 tracing 上下文,并在构造 RPC 消息时正确传递参数,从而使 tracing 上下文正确的在分布式组件之中传递。

rust
let request = RegionRequest {
    header: Some(RegionRequestHeader {
        tracing_context: TracingContext::from_current_span().to_w3c(),
        ..Default::default()
    }),
    body: Some(region_request::Body::Alter(request)),
};
let request = RegionRequest {
    header: Some(RegionRequestHeader {
        tracing_context: TracingContext::from_current_span().to_w3c(),
        ..Default::default()
    }),
    body: Some(region_request::Body::Alter(request)),
};

在 RPC 消息的接收方,需要将 tracing 上下文正确解码,并且使用该上下文构建第一个 span 对函数调用进行追踪。比如下面的代码就将接收到的 RPC 消息中的 tracing_context 使用 TracingContext::from_w3c 方法正确解码。并使用 attach 方法将新建的 info_span!("RegionServer::handle_read")  附上了上下文消息,从而能够跨分布式组件对调用进行追踪。

rust
...
let tracing_context = request
    .header
    .as_ref()
    .map(|h| TracingContext::from_w3c(&h.tracing_context))
    .unwrap_or_default();
let result = self
    .handle_read(request)
    .trace(tracing_context.attach(info_span!("RegionServer::handle_read")))
    .await?;
...
...
let tracing_context = request
    .header
    .as_ref()
    .map(|h| TracingContext::from_w3c(&h.tracing_context))
    .unwrap_or_default();
let result = self
    .handle_read(request)
    .trace(tracing_context.attach(info_span!("RegionServer::handle_read")))
    .await?;
...

使用 tracing::instrument 对监测代码进行埋点

我们使用 tracing 提供的 instrument 宏对代码进行埋点,只要将 instrument 宏标记在需要进行埋点的函数即可。 instrument 宏会每次将函数调用的参数以 Debug 的形式打印到 span 中。对于没有实现 Debug trait 的参数,或者结构体过大、参数过多,最后导致 span 过大,希望避免这些情况就需要使用 skip_all,跳过所有的参数打印。

rust
#[tracing::instrument(skip_all)]
async fn instrument_function(....) {
    ...
}
#[tracing::instrument(skip_all)]
async fn instrument_function(....) {
    ...
}

跨越 runtime 的代码埋点

Rust 的 tracing 库会自动处理埋点函数间的嵌套关系,但如果某个函数的调用跨越 runtime 的话,tracing 不能自动对这类调用进行追踪,我们需要手动跨越 runtime 去传递上下文。

rust
let tracing_context = TracingContext::from_current_span();
let handle = runtime.spawn(async move {
    handler
        .handle(query)
        .trace(tracing_context.attach(info_span!("xxxxx")))
    ...
});
let tracing_context = TracingContext::from_current_span();
let handle = runtime.spawn(async move {
    handler
        .handle(query)
        .trace(tracing_context.attach(info_span!("xxxxx")))
    ...
});

比如上面这段代码需要跨越 runtime 去进行 tracing,我们先通过 TracingContext::from_current_span() 获取当前 tracing 上下文,通过在另外一个 runtime 里新建一个 span,并将 span 附着在当前上下文中,我们就完成了跨越 runtime 的代码埋点,正确追踪到了调用链。