优雅关闭
问题
如何实现 Rust 服务的优雅关闭(Graceful Shutdown)?
答案
为什么需要优雅关闭
| 场景 | 无优雅关闭 | 有优雅关闭 |
|---|---|---|
| 进行中的请求 | 被强制中断,客户端收到错误 | 等待完成后再关闭 |
| 数据库事务 | 可能处于不一致状态 | 事务提交或回滚 |
| 正在写入的文件 | 文件损坏 | 正确 flush 并关闭 |
| 消息队列消费 | 消息可能丢失 | 确认或重新入队 |
基础实现
use tokio::signal;
use tokio::sync::broadcast;
#[tokio::main]
async fn main() {
// 创建关闭通知通道
let (shutdown_tx, _) = broadcast::channel::<()>(1);
// 启动 HTTP 服务器
let server_shutdown = shutdown_tx.subscribe();
let server = tokio::spawn(run_server(server_shutdown));
// 启动后台任务
let worker_shutdown = shutdown_tx.subscribe();
let worker = tokio::spawn(run_background_worker(worker_shutdown));
// 等待关闭信号
shutdown_signal().await;
tracing::info!("收到关闭信号,开始优雅关闭...");
// 通知所有组件
let _ = shutdown_tx.send(());
// 等待所有组件完成,设置超时
let timeout = tokio::time::Duration::from_secs(30);
if tokio::time::timeout(timeout, async {
let _ = server.await;
let _ = worker.await;
}).await.is_err() {
tracing::warn!("优雅关闭超时,强制退出");
}
tracing::info!("关闭完成");
}
async fn shutdown_signal() {
let ctrl_c = signal::ctrl_c();
let mut sigterm = signal::unix::signal(
signal::unix::SignalKind::terminate()
).unwrap();
tokio::select! {
_ = ctrl_c => {}
_ = sigterm.recv() => {}
}
}
Axum 优雅关闭
use axum::{Router, routing::get};
async fn run_server(mut shutdown: broadcast::Receiver<()>) {
let app = Router::new().route("/", get(|| async { "hello" }));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
.await
.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(async move {
let _ = shutdown.recv().await;
tracing::info!("HTTP 服务器开始优雅关闭");
})
.await
.unwrap();
tracing::info!("HTTP 服务器已关闭");
}
后台任务优雅关闭
async fn run_background_worker(mut shutdown: broadcast::Receiver<()>) {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
loop {
tokio::select! {
// 优先检查关闭信号
_ = shutdown.recv() => {
tracing::info!("后台任务收到关闭信号");
break;
}
_ = interval.tick() => {
// 执行定时任务
if let Err(e) = do_work().await {
tracing::error!(?e, "任务执行失败");
}
}
}
}
// 清理资源
flush_buffers().await;
tracing::info!("后台任务已安全关闭");
}
关闭顺序
超时保护
必须设置关闭超时(通常 30 秒)。如果某个组件 hang 住,到超时后强制退出,避免进程永远无法关闭。Kubernetes 的 terminationGracePeriodSeconds 默认为 30 秒,之后会发 SIGKILL。
常见面试问题
Q1: CancellationToken 和 broadcast channel 哪个更适合优雅关闭?
答案:
tokio_util::sync::CancellationToken:更轻量,专为「关闭」场景设计,支持子 token(child_token()),推荐用于纯关闭信号broadcast::channel:可以携带数据,适合需要通知不同关闭原因的场景- 简单场景用 CancellationToken,复杂场景用 broadcast
Q2: 如何确保数据库连接池正确关闭?
答案:
- 先停止接收新请求
- 等待所有进行中的请求完成(连接归还池中)
- 显式调用
pool.close().await(sqlx)等待所有连接关闭 - 在关闭顺序中,数据库连接池应该 最后关闭,因为其他组件可能还在用