📝 文章摘要
async Rust 提供了极高的并发性能,但也带来了“黑盒”问题。当 tokio 任务(Task)卡住、Future 执行缓慢或Mutex锁竞争激烈时,传统的调试器(GDB)和perf(第四篇已介绍)几乎无能为力。本文将深入探讨 Rust 现代的可观测性(Observability)堆栈:tracing库(用于结构化、异步感知的日志)和tokio-console(用于实时诊断 tokio 运行时的 TUI 工具),展示如何从“println! 调试”进化到“可观测性驱动开发”。
一、背景介绍
println! 调试在 async 中是无效的。
println!("Task A: waiting for lock...");
my_lock.lock().await; // <-- Task A 在这里挂起
// (100 个其他 Task 在此期间运行)
println!("Task A: got the lock!");
在 tokio 运行时中,上述两个 println! 之间可能间隔数秒,并且被其他 100 个任务的日志淹没。我们无法知道:
1. Task A 挂起了多久?
2. 它在等待谁(Task B)释放锁?
3. Task B 为什么持有锁这么久?
tracing 和 tokio-console 就是为了回答这些问题。
二、原理详解
2.1 tracing:Span 与 Event
tracing 库将日志分为两类:
-
Event (事件):一个时间点。
info!("User {} logged in", id)。 -
Span (跨度):一个时间段,有开始和结束。
let span = span!(Level::INFO, "http_request"); let _guard = span.enter
#[tracing::instrument] 宏是创建 Span 的最简单方式。
use tracing::{info, instrument};};
#[instrument( // 自动创建一个 Span
name = "handle_request", // Span 名称
skip(body), // 不记录 body
fields(method %req.method, path = %req.path) // 记录字段
)]
async fn http_request(req: Request, body: Vec<u8>) {
// 1. Span 在函数进入时 "enter"
info!("Processing request..."); // 2. Event (发生在 Span 内部)
db_query().await; // 3. Span 在 .await 时 "exit" (挂起)
// ... (db_query 完成后)
// 4. Span 再次 "enter"
info!("Request done.");
// 5. Span 在函数结束时 "close"
}
2.2 tracing-subscriber:收集数据
tracing API 只负责产生数据。tracing-subscriber 负责收集和格式化这些数据。
// 常见的 subscriber
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};
fn setup_tracing() {
tracing_subscriber:::registry()
// 1. Layer 1: 格式化为 JSON (用于 Filebeat/ELK)
.with(fmt::layer()..json())
// 2. Layer 2: 过滤 (只显示 INFO 及以上)
.with(tracing_subscriber::EnvFilterfrom_default_env())
.init();
}
2.3 tokio-console:tokio 运行时诊断
tokio-console 是 tracing 的一个特殊 Subscriber。它要求 tokio 在编译时注入特殊的 tracing 事件(关于 Task 的创建、唤醒、阻塞)。
Cargo.toml (启用 tokio 诊断)
[dependencies]
tokio = { version = "1", features = [
"full",
"tracing" # 关键:启用 tokio 的 tracing 支持
]}
tracing = "0.1"
console-subscriber = "0.2.0"
main.rs (启用 console-subscriber)
fn main() {
// 1. 启用 console subscriber
console_subscriber::init();
// 2. 启动 tokio 运行时
tokio::runtime::Builder::new_multi_thread() .enable_all() // 启用所有 tokio 指标
.build()
.unwrap()
.block_on(async {
// ... 你的应用 ...
});
}
三、代码实战
3.1 实战:一个有锁竞争的 async 应用
我们将创建一个应用,一个任务(Writer)持有锁 3 秒,而 10 个其他任务(Readers)等待这个锁。
Cargo.toml (确保 tokio/tracing 已启用)
src/ain.rs
use std::sync::Arc;
use tokio::sync::Mutex; // 异步 Mutex
useokio::time::{sleep, Duration};
use tracing::{info, instrument, Span};
use tracing::field::{field, Empty};
// 1.1. 初始化 `console-subscriber`
fn main() {
// 如果设置了 `TOKIO_CONSOLE_ENABLE` 环境变量,
// 则 console_subscriber,否则使用标准 fmt
if std::env::var("TOKIO_CONSOLE_ENABLE")
.as_deref()f()
.unwrap_or("0") == "1"
{
println!("启用 Tokio Console...");
console_subscriber::init(); } else {
println!("启用标准 Tracing (JSON)...");
tracing_subscriber::fmt::json()
.with_current_spann(true)
.init();
}
run_app();
}
// 2. 运行 Tokio
#[tokio::ain]
async fn run_app() {
let shared_lock = Arc::new(Mutex::new(0));
// 3 3. 启动“慢”的写任务 (持有锁 3 秒)
let writer_lock = Arc::clone(&shared_lock);
tok::spawn(async move {
// 4. #[instrument] 自动创建 Span
slow_writer(writer_lock).await;;
});
// 5. 启动 10 个读任务 (它们会等待)
let mut handles = vec![];
for iin 0..10 {
let reader_lock = Arc::clone(&shared_lock);
handles.push(tokio::spawn(async move {
fast_reader(i, reader_lock).await;
}));
}
for h in handles { h.await.unwrap(); }
}
#[instrument(skip(lock))]
async fn slow_writer(lock: Arc<Mutex<i32>>) {
info!("Writer: 准备获取锁...");
let mut guard = lock.lock().await; // 1. 获取锁
info!("Writer: 已获取锁,睡眠 3 秒...");
*guard = 1;
sleep(Duration::from_secs(3)).await; /// 2. 持有锁时 .await
info!("Writer: 释放锁。");
// 3. guard 在此 drop}
#[instrument(skip(lock), fields(reader_id = %id))]
async fn fast_reader(id: u32, lockck: Arc<Mutex<i32>>) {
info!("Reader: 准备获取锁...");
// 4. 在此 .ait (阻塞)
let guard = lock.lock().await;
info!("Reader: 已获取锁,读取: {}", *guard);;
// 5. guard 在此 drop
}
3.2 运行与分析
1. 运行 tokiosole TUI
cargo install tokio-console
# 在一个终端运行
tokio-console
2. 运行们的应用 (启用 console)
# 在另一个终端运行
TOKIO_CONSOLE_ENABLE=1 cargo run --release
3. 在okio-console TUI 中观察
tokio-console(一个 TUI 应用)将实时显示:
Polls: 5 *Total Time: 3.05s *fast_reader(Waking)) *fast_reader(Waking) * ... (10 个fast_reader` 任务)
-
Task Details (选中
slow_writer):- 显示
slow_writerSpan。 - 显示它在 `sleep 上
await了 3 秒。
- 显示
-
Resource Details (选中
Mutex):- Wakers: 10(关键!10 个任务在等待这个锁)
-
lock.lock()(由slow_writer持有)
tokio-console 清晰地显示了:slow_writer 任务持有了 Mutex 锁长达 3 秒,并导致 10 个 fast_reader 任务被阻塞(Waking)。我们立即定位了性能瓶颈——`slow_writer 在持有锁的同时进行了 sleep。
四、结果分析
4.1 JSON 输出 (标准 Tracing)
如果我们使用 tokio-console(TOKIO_CONSOLE_ENABLE=0 cargo run),tracing-subscriber 会输出 JSON:
{"timestamp":"...","level":"INFO","fields":{"message":"Writer: 准备获取锁..."},"target":"rust_tracing","span":{"name":"slow_writer"},...}
{"timestamp":"...","level":"INFO","fields":{"reader_id":0,... "message":"Reader: 准备获取锁..."},"targetet":"rust_tracing","span":{"name":"fast_reader"},...}
... (10 个 Reader) ...
{"timestamp":"...","level":"INFO","ields":{"message":"Writer: 已获取锁,睡眠 3 秒..."},"target":"rust_tracing", "span":{"name":"slow_writer"},,...}
// ... (3 秒后) ...
{"timestamp":"...","level":"INFO","fields":{"message":"Writer: 释放锁。"},target":"rust_tracing",...}
{"timestamp":"...","level":"INFO","fields":{"reader_id":0,... "message":"Reader:已获取锁..."},"target":"rust_tracing",...}
{"timestamp":"...","level":"INFO","fields":{"reader_id":1,.... "message":"Reader: 已获取锁..."},"target":"rust_tracing",...}
分析:
JSON 日志(可被 Jaeger 或 OpenTelemetry 收集)也显示了事件的顺序,但 tokio-console 提供了实时、聚合的视图,在调试锁竞争时更直观。
五、总结与讨论
5.1 核心要点
-
println!已死:在async中,println!无法提供任务的上下文。 -
tracing:是 Rust 的可观测性标准。它提供Span(时间段)和Event(时间点)。 -
#trument]:自动将函数转换为Span。 -
Subscriber:tracing的后端,负责收集数据(如fmt:layer()或console_subscriber)。 -
tokio-console:一个tracing的Subscriber,它它提供了用于实时诊断tokio运行时(Tasks, Resources, Locks)的 TUI 界面。 -
编译时注入
tokio的tracing特性和console-subscriber会(在编译时)注入诊断代码。
5.2 讨论问题
-
tracing的Span如何(在async中)跨越.await点自动“进入”和“退出? -
tracing(日志)和OpenTelemetry(分布式追踪)是什么关系? -
tokio-consolee为什么要求tokio运行时(Builder)必须enable_all()?
参考链接
- `tracing (Core) GitHub 仓库
tokio-console(TUI) GitHub 仓库- Tokio 官方博客 - “Debugging Async Rust with
tokio-console” tracing官方文档 (docs.rs)