Rust 语言的全链路追踪库 tracing
在一个应用程序或库的开发过程中,除了其自身的逻辑以外,开发人员还须要做很多额定的工作,以保障编写的代码能够正确的运行,或者在出错时能够疾速定位到谬误的地位以及起因,这就须要引入一些额定的工具,trace
就是其中特地好用的一种,下文我将会简略介绍 trace
,并以 Rust
为例,演示 trace
在 Rust
中的应用办法。
可观测性
Logs
、Metrics
和 Traces
并称为可观测性三大支柱,通过剖析它们输入的数据,开发人员可能更好的观测到零碎的运行状况,更快的定位问题,从而进步零碎的可靠性。
日志 (Logs)
日志作为最罕用的可观测性数据源之一,置信少数开发者都比拟相熟。其本质上就是一种带有工夫戳的离散事件记录,通常用于记录零碎的运行状态,日志的应用非常简略,只须要在代码中须要报告信息的点增加一行代码,就能够将这些信息输入到控制台或文件中,然而日志也有很大的毛病,它的输入是离散的,这意味着在记录的时候,无奈将日志信息互相关联,也无奈晓得日志信息的上下文,尤其是在多线程的环境下,最终输入的信息比拟凌乱,不便于检索和剖析。
指标 (Metrics)
指标是一种定量掂量,例如平均值、比率和百分比等。其值始终为数字而非文本,能够通过数学方法统计和剖析,其次要用于形容零碎运行状态的数据,比方 CPU 的使用率、内存的使用率、磁盘的使用率等,这些数据能够用来监控零碎的运行状态,也能够用来预警。
追踪 (Traces)
追踪是一种用于记录零碎中一次申请的残缺生命周期的数据,它能够记录下一个申请从开始到完结的所有信息,包含申请的发起者、接收者、申请的门路、申请的状态、申请的耗时、申请的错误信息等,这些信息能够用来剖析零碎的性能瓶颈,也能够用来剖析零碎的谬误。追踪实质上也是一种日志,他与日志的数据结构十分相似,然而它可能提供比日志更丰盛的信息。特地是在分布式系统中,追踪可能逾越多个服务,汇总出一次申请的残缺信息,让开发人员可能更不便的找到零碎中的问题。
Rust 中的 Trace
Rust
社区中比拟有名的 trace
实现有三个:
tracing
由 tokio 团队保护,目前应用最宽泛,生态也比较完善rustracing
应用人数绝对较少minitrace
tikv 团队打造,性能最好
接下来就以 tracing
为例,介绍一下 trace 的外围概念以及应用办法:
Span
Span
能够说是 trace
中最要害的概念之一,它示意的是一个过程,也就是一段时间内产生的所有事件的汇合,其数据结构中蕴含着 Span
的开始工夫和完结工夫,在剖析数据是能够借助工具直观的看到某次申请或操作的耗时状况。在同一个 trace
流程中的所有 span
都共享这雷同的 Trace Id
,每个 Span
也有着本人的 Span Id
,并且 Span
还反对嵌套,嵌套的 Span
中也会保留着相应的父子关系,最终能够靠这些信息,将申请的残缺生命周期串联起来,并且不会与雷同时间段内的其余申请产生烦扰。
use tracing::{span, Level};
fn main() {let span = span!(Level::INFO, "span");
let _enter = span.enter();
// enter 后进入该 span 的上下文
// 能够记录信息到 span 中
} // 来到作用域后,_enter 被 drop,对应的 span 在此完结
以上代码是创立并应用一个 Span
最简略的形式,除此以外还有几种不同的形式
#[instrument] // tracing 会为以后函数主动创立 span,该 span 名与函数雷同,并且整个函数都在该 span 的上下文内
fn do_something() {
// some event
let span = span!(Level::INFO, "external function");
span.in_scope(|| some_external_function()); // 对于无奈增加 #[instrument] 的内部函数,也能够应用 in_scope 办法让其在 span 的上下文中执行
}
#[instrument] // 此办法同样对异步函数实用
async fn do_something_async() {
let future = async {// some async code};
let span = span!(Level::INFO, "future");
future.instrument(span).await; // 也能够在 future .await 之前将 span 附加给 future
}
// async 代码中要防止以下状况
async fn some_async_code() {let span = span!(Level::INFO, "span");
let _enter = span.enter();
// 此处进入 span 的上下文,直到 _enter 被 drop 后才会完结
async_fn().await; // .await 时,task 可能会让出以后线程的执行权,而此时 _enter 还没有 drop,因而可能会谬误的记录到其余 task 的 enent.}
Event
Event
与日志相似,示意的是某一个工夫点产生的事件,但与日志不同的是,Event
能够将信息记录到 Span
的上下文中,这样在剖析数据时,能够间接查看 Span
中产生的所有事件。
use tracing::{event, info, span, Level};
fn main() {event!(Level::INFO, "event"); // 在 span 的上下文之外记录一个 Leval 为 INFO 的 event
let span = span!(Level::INFO, "span");
let _enter = span.enter();
event!(Level::INFO, "event"); // 在 span 的上下文内记录 event
info!("something with info level"); // 也能够应用和 log 雷同的模式记录 event
}
Collector
以上的示例不会有任何可见的输入,因为咱们还没有配置 Collector
,tracing
中所有的 Span
和 Event
都是通过 Collector
来收集的,Collector
会将 Span
和 Event
以肯定的格局输入到指定的中央,比方 stdout
、stderr
、文件、网络等。tracing-subscriber
的 fmt 模块提供了一个 Collector
,能够不便的输入事件信息。
use tracing::info;
use tracing_subscriber;
fn main() {
// 初始化全局 Collector
tracing_subscriber::fmt::init();
info!("Hello, world!");
}
运行下面这段代码,能够在终端中看到一条 INFO 级别的事件,如果须要将 Trace
信息发送到其余中央,就要用到其余的 Collector
实现,比方 tracing-appender
这个 crate,能够将 Trace
信息输入到文件中。
在 Rust 中应用 tracing 的残缺示例
use std::{thread::sleep, time::Duration};
use tracing::{debug, info, info_span, instrument};
#[instrument]
fn expensive_work(secs: u64) {debug!("doing expensive work");
sleep(Duration::from_secs(secs));
debug!("done with expensive work");
}
fn main() {tracing_subscriber::fmt()
// enable everything
.with_max_level(tracing::Level::TRACE)
// sets this to be the default, global collector for this application.
.init();
let span = info_span!("root");
let _enter = span.enter();
info!("some info in the root span");
expensive_work(1);
}
运行以上代码将会的到以下输入
2022-12-01T02:50:59.425475Z INFO root: tracing_example: some info in the root span
2022-12-01T02:50:59.425518Z DEBUG root:expensive_work{secs=1}: tracing_example: doing expensive work
2022-12-01T02:51:00.425722Z DEBUG root:expensive_work{secs=1}: tracing_example: done with expensive work
每个事件都已雷同的格局输入,此输入模式下,与 log
的输入十分相似,但 tracing
输入的内容多出了 span
相干的信息。由 instrument
生成的 span
还主动增加了函数的参数信息。上面介绍的 OpenTelemetry
和 Jaeger
,还能够让咱们更加直观的查看 Span
之间的工夫关系。
Trace 的标准化
想要让 Trace
逾越多个服务,集成到多种不同的语言,那就必须要规定大家互相调用的标准,要恪守一套雷同的协定,能力让 Trace
的数据在不同的零碎中都可能失常传递,Trace
晚期诞生了两种标准,别离是 OpenTracing
和 OpenCensus
,起初为了标准的对立,OpenTracing
和 OpenCensus
合并成了 OpenTelemetry
,当初曾经成为了 Trace
的事实标准。OpenTelemetry
提供了不同语言的 SDK,能够不便的集成到不同的零碎中,对于 Rust
,它提供了一系列相干的 crate 用于集成。tracing
也提供了 tracing-OpenTelemetry
用来将其收集到的信息发送到兼容 OpenTelemetry
的分布式追踪零碎中。
Trace 数据的可视化剖析
Jaeger
是受到 Dapper
和 OpenZipkin
启发的开源分布式跟踪零碎,由 Uber 开发,现已捐献给 CNCF。Jaeger
通过收集 Trace
数据,将其可视化展现,不便开发者剖析零碎的问题。下图为 Jaeger
部署的示例。
要将 Trace
数据发送给 Jaeger
,须要在咱们的利用中增加 jaeger-client
。OpenTelemetry
提供的 crate 中,就包含了响应的 jaeger-clinet
实现: opentelemetry-jaeger
。它会将 Span
信息以 UDP 包的模式发送到 jaeger-agent
,jaeger-agent
将一段时间内的数据打包分批发送到 jaeger-collector
,再由 jaeger-collector
把数据存入数据库内,咱们在 jaeger
的 UI 中就能够查问到这些数据。
OpenTelemetry
的仓库中也提供了以上流程的示例,咱们能够间接运行这个示例,而后在 jaeger
的前端咱们就能够失去下图的内容:
有了这些数据,开发人员就可能疾速定位到申请的次要耗时局部,也可能通过其中蕴含的事件获取到申请内的音讯记录。
总结
对于大多数同步程序,用 Log
就可能满足需要,并且应用起来也足够简略,然而一旦波及到异步程序或其余的一些简单产隔膜,Log
就会变得不那么好用了,一段时间内的 Log
信息可能来自于多个不同的解决流程,难以疾速不便的获取咱们须要的信息,而 Trace
则可能很好的解决这个问题。