关于 tokio-troubleshooting
tokio-troubleshooting 是一项面向 Tokio 异步应用开发者的调试技能,提供运行时检查、死锁与挂起排查、内存问题分析和性能剖析方法。内容还涵盖常见错误模式、异步测试及排查清单。
主要功能
- 01使用 tokio-console 检查运行时
- 02排查死锁、挂起和超时
- 03识别任务泄漏与引用环
- 04用 tracing 分析性能
- 05处理常见异步错误
- 06编写带超时的异步测试
适用场景
- 01查找 Tokio 任务增长或轮询耗时问题
- 02诊断锁竞争和异步操作挂起
- 03调查任务泄漏或内存占用增长
- 04分析请求路径的性能瓶颈
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ geoffjay/ claude-plugins --skill tokio-troubleshooting安装后在 Claude Code 中输入/tokio-troubleshooting确认已加载。
详细介绍
Tokio 故障排查
本技能提供用于调试和排查基于 Tokio 构建的异步应用问题的技巧。
使用 tokio-console 检查运行时
实时监控异步运行时:
// In Cargo.toml
[dependencies]
console-subscriber = "0.2"
// In main.rs
fn main() {
console_subscriber::init();
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
run_application().await
});
}
在单独的终端中运行控制台:
tokio-console
需要监控的关键指标:
- 任务的生成速率和任务总数
- 每个任务的轮询时长
- 空闲时间与繁忙时间
- Waker 操作
- 资源利用率
问题识别:
- 轮询时长过长:在异步上下文中执行了 CPU 密集型工作
- Waker 数量过多:可能存在竞争或轮询效率低下
- 任务数量不断增加:任务泄漏或无界生成任务
- 空闲时间过长:工作量不足或存在阻塞操作
调试死锁和挂起
检测并解决死锁情况:
常见死锁模式
// BAD: Potential deadlock
async fn deadlock_example() {
let mutex1 = Arc::new(Mutex::new(()));
let mutex2 = Arc::new(Mutex::new(()));
let m1 = mutex1.clone();
let m2 = mutex2.clone();
tokio::spawn(async move {
let _g1 = m1.lock().await;
tokio::time::sleep(Duration::from_millis(10)).await;
let _g2 = m2.lock().await; // May deadlock
});
let _g2 = mutex2.lock().await;
tokio::time::sleep(Duration::from_millis(10)).await;
let _g1 = mutex1.lock().await; // May deadlock
}
// GOOD: Consistent lock ordering
async fn no_deadlock_example() {
let mutex1 = Arc::new(Mutex::new(()));
let mutex2 = Arc::new(Mutex::new(()));
// Always acquire locks in same order
let _g1 = mutex1.lock().await;
let _g2 = mutex2.lock().await;
}
// BETTER: Avoid nested locks
async fn best_example() {
// Use message passing instead
let (tx, mut rx) = mpsc::channel(10);
tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
process_message(msg).await;
}
});
tx.send(message).await.unwrap();
}
使用超时检测挂起
use tokio::time::{timeout, Duration};
async fn detect_hang() {
match timeout(Duration::from_secs(5), potentially_hanging_operation()).await {
Ok(result) => println!("Completed: {:?}", result),
Err(_) => {
eprintln!("Operation timed out - potential hang detected");
// Log stack traces, metrics, etc.
}
}
}
使用 try_lock 检测死锁
use tokio::sync::Mutex;
async fn try_with_timeout(mutex: &Mutex<State>) -> Option<State> {
for _ in 0..10 {
if let Ok(guard) = mutex.try_lock() {
return Some(guard.clone());
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
eprintln!("Failed to acquire lock - possible deadlock");
None
}
检测内存泄漏
识别并修复内存泄漏:
任务泄漏
// BAD: Tasks never complete
async fn leaking_tasks() {
loop {
tokio::spawn(async {
loop {
// Never exits
tokio::time::sleep(Duration::from_secs(1)).await;
}
});
}
}
// GOOD: Tasks have exit condition
async fn proper_tasks(shutdown: broadcast::Receiver<()>) {
loop {
let mut shutdown_rx = shutdown.resubscribe();
tokio::spawn(async move {
loop {
tokio::select! {
_ = shutdown_rx.recv() => break,
_ = tokio::time::sleep(Duration::from_secs(1)) => {
// Work
}
}
}
});
}
}
Arc 循环引用
// BAD: Reference cycle
struct Node {
next: Option<Arc<Mutex<Node>>>,
prev: Option<Arc<Mutex<Node>>>, // Creates cycle!
}
// GOOD: Use weak references
use std::sync::Weak;
struct Node {
next: Option<Arc<Mutex<Node>>>,
prev: Option<Weak<Mutex<Node>>>, // Weak reference breaks cycle
}
监控内存使用情况
use sysinfo::{System, SystemExt};
pub async fn memory_monitor() {
let mut system = System::new_all();
let mut interval = tokio::time::interval(Duration::from_secs(60));
loop {
interval.tick().await;
system.refresh_memory();
let used = system.used_memory();
let total = system.total_memory();
let percent = (used as f64 / total as f64) * 100.0;
tracing::info!(
used_mb = used / 1024 / 1024,
total_mb = total / 1024 / 1024,
percent = %.2 percent,
"Memory usage"
);
if percent > 80.0 {
tracing::warn!("High memory usage detected");
}
}
}
使用 Tracing 进行性能分析
为代码添加检测,以便进行性能分析:
use tracing::{info, instrument, span, Level};
#[instrument]
async fn process_request(id: u64) -> Result<Response, Error> {
let span = span!(Level::INFO, "database_query");
let _enter = span.enter();
let data = fetch_from_database(id).await?;
drop(_enter);
let span = span!(Level::INFO, "transformation");
let _enter = span.enter();
let result = transform_data(data).await?;
Ok(Response { result })
}
// Configure subscriber for flame graphs
use tracing_subscriber::layer::SubscriberExt;
fn init_tracing() {
let fmt_layer = tracing_subscriber::fmt::layer();
let filter_layer = tracing_subscriber::EnvFilter::from_default_env();
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.init();
}
理解 Panic 消息
常见的异步 Panic 模式:
生成的任务中的 Panic
// Panic is isolated to the task
tokio::spawn(async {
panic!("This won't crash the program");
});
// To catch panics
let handle = tokio::spawn(async {
// Work that might panic
});
match handle.await {
Ok(result) => println!("Success: {:?}", result),
Err(e) if e.is_panic() => {
eprintln!("Task panicked: {:?}", e);
// Handle panic
}
Err(e) => eprintln!("Task cancelled: {:?}", e),
}
Send + 'static 错误
// ERROR: future cannot be sent between threads
async fn bad_example() {
let rc = Rc::new(5); // Rc is !Send
tokio::spawn(async move {
println!("{}", rc); // Error!
});
}
// FIX: Use Arc instead
async fn good_example() {
let rc = Arc::new(5); // Arc is Send
tokio::spawn(async move {
println!("{}", rc); // OK
});
}
// ERROR: borrowed value does not live long enough
async fn lifetime_error() {
let data = String::from("hello");
tokio::spawn(async {
println!("{}", data); // Error: data might not live long enough
});
}
// FIX: Move ownership
async fn lifetime_fixed() {
let data = String::from("hello");
tokio::spawn(async move {
println!("{}", data); // OK: data is moved
});
}
常见错误模式及解决方案
在异步上下文中执行阻塞操作
// PROBLEM: Detected with tokio-console (long poll time)
async fn blocking_example() {
std::thread::sleep(Duration::from_secs(1)); // Blocks thread!
}
// SOLUTION
async fn non_blocking_example() {
tokio::time::sleep(Duration::from_secs(1)).await; // Yields control
}
// For unavoidable blocking
async fn necessary_blocking() {
tokio::task::spawn_blocking(|| {
expensive_cpu_work()
}).await.unwrap();
}
通道已关闭错误
// PROBLEM: SendError because receiver dropped
async fn send_error_example() {
let (tx, rx) = mpsc::channel(10);
drop(rx); // Receiver dropped
match tx.send(42).await {
Ok(_) => println!("Sent"),
Err(e) => eprintln!("Send failed: {}", e), // Channel closed
}
}
// SOLUTION: Check if receiver exists
async fn handle_closed_channel() {
let (tx, rx) = mpsc::channel(10);
tokio::spawn(async move {
// Receiver keeps channel open
while let Some(msg) = rx.recv().await {
process(msg).await;
}
});
// Or handle the error
if let Err(e) = tx.send(42).await {
tracing::warn!("Channel closed: {}", e);
// Cleanup or alternative action
}
}
任务取消
// PROBLEM: Task cancelled unexpectedly
let handle = tokio::spawn(async {
// Long-running work
});
handle.abort(); // Cancels task
// SOLUTION: Handle cancellation gracefully
let handle = tokio::spawn(async {
let result = tokio::select! {
result = do_work() => result,
_ = tokio::signal::ctrl_c() => {
cleanup().await;
return Err(Error::Cancelled);
}
};
result
});
有效测试异步代码
编写可靠的异步测试:
#[tokio::test]
async fn test_with_timeout() {
tokio::time::timeout(
Duration::from_secs(5),
async {
let result = my_async_function().await;
assert!(result.is_ok());
}
)
.await
.expect("Test timed out");
}
#[tokio::test]
async fn test_concurrent_access() {
let shared = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..10)
.map(|_| {
let shared = shared.clone();
tokio::spawn(async move {
let mut lock = shared.lock().await;
*lock += 1;
})
})
.collect();
for handle in handles {
handle.await.unwrap();
}
assert_eq!(*shared.lock().await, 10);
}
// Test with mocked time
#[tokio::test(start_paused = true)]
async fn test_with_time_control() {
let start = tokio::time::Instant::now();
tokio::time::sleep(Duration::from_secs(100)).await;
// Time is mocked, so this completes instantly
assert!(start.elapsed() < Duration::from_secs(1));
}
调试检查清单
排查异步问题时:
- 使用 tokio-console 监控运行时行为
- 使用 tracing 检查是否存在阻塞操作
- 确认所有锁都已正确释放
- 检查是否存在任务泄漏(任务数量不断增加)
- 持续监控内存使用情况
- 添加超时以检测挂起
- 检查是否存在通道关闭错误
- 确认满足 Send + 'static 约束
- 使用 try_lock 检测潜在死锁
- 使用 tracing 分析性能瓶颈
- 使用 tokio-test 测试基于时间的代码
- 使用弱引用检查是否存在 Arc 循环引用
实用工具
- tokio-console:实时监控异步运行时
- tracing:结构化日志记录和性能分析
- cargo-flamegraph:生成火焰图
- valgrind/heaptrack:内存性能分析
- perf:在 Linux 上进行 CPU 性能分析
- Instruments:在 macOS 上进行性能分析
最佳实践
- 始终在开发过程中使用 tokio-console
- 在关键代码路径中添加 tracing span
- 尽可能多地使用超时来检测挂起
- 监控任务数量以发现泄漏
- 先分析性能再优化——先进行测量
- 使用真实并发进行测试——不要只测试顺利路径
- 妥善处理所有任务中的取消
- 使用结构化日志记录进行调试
- 避免嵌套锁——优先使用消息传递
- 必要时记录锁的获取顺序
评论
支持 Markdown