Rust 优雅关闭
处理信号、停止接新请求、排空进行中任务与超时强杀的服务关闭协议。
#concept
#growing
#dev
#rust
[!info] related notes
Rust 优雅关闭
这个概念为什么出现
kill -9 式退出会打断请求、丢消息。
优雅关闭:收信号 → 停止准入 → 等待飞行中工作 → 释放资源 → 退出。
[!abstract] 一句话理解 收到停止事件后停止准入并通知工作任务,等待进行中工作与清理完成;超过宽限期后按明确策略取消或退出,不能把直接丢弃服务 future 当作排空。
最小可运行示例
场景:协作通知、等待与超时取消
先执行 cargo add tokio@1 --features macros,rt-multi-thread,time,sync。示例不监听 OS 信号或接收真实请求,专门验证关闭协调协议:
use std::time::Duration;
use tokio::{sync::oneshot, time::{sleep, timeout}};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (stop_tx, stop_rx) = oneshot::channel::<()>();
let mut worker = tokio::spawn(async move {
// 示例只等待停止通知;真实服务在此停止准入并排空进行中工作。
let _ = stop_rx.await;
sleep(Duration::from_millis(10)).await; // 模拟异步清理
println!("drained");
});
println!("shutdown requested");
let _ = stop_tx.send(()); // 确定性触发,便于无需人工信号的测试
match timeout(Duration::from_secs(1), &mut worker).await {
Ok(result) => result?,
Err(_) => {
worker.abort();
let _ = worker.await; // 收割取消结果;不代表业务已排空
return Err("shutdown deadline exceeded".into());
}
}
println!("stopped");
Ok(())
}
cargo run 应依次输出 shutdown requested、drained、stopped。timeout 接收 &mut JoinHandle,超时后仍持有句柄才能 abort 并 await;若把句柄按值交给 timeout,超时丢弃它会分离任务。abort 依赖任务让出执行权,不能强杀阻塞线程,最终进程截止时间仍需外部监督。
真实服务可将停止事件接到 axum 的 with_graceful_shutdown。signal::ctrl_c() 不等于 Unix SIGTERM:容器停止路径需单独监听 tokio::signal::unix::SignalKind::terminate(启用 signal feature,按目标平台条件编译),并处理监听错误。SIGKILL 无法捕获。
结合场景再看三个关注点
- 核实编排器的停止信号和宽限期;Kubernetes 通常先 TERM,超时再 KILL,preStop 也消耗宽限期
- readiness 先失败再排空;路由传播和已有连接存在延迟,服务端仍须停止准入
- 超时防挂死
工程实践
- 统一停止事件;多任务广播可使用 tokio-util 的 CancellationToken,但 cancel 只是通知,仍需追踪并等待任务
- 工作者监听取消
- 关闭指标
- 本地用 ctrl-c 测
本节总结
- 关闭是协议
- 信号 + 排空 + 超时
- 与编排系统协作
自测题
- 为何 readiness 要在 drain 前变失败?
- 只有 Drop 够不够?
参考答案
- 停止新流量进入。
- 不够,需要协作式停止异步任务与监听端口。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| Tokio signal | 生态 | 信号 |
| axum graceful shutdown examples | 生态 | 框架集成 |