跳到主要内容

优雅关闭

问题

如何实现 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: 如何确保数据库连接池正确关闭?

答案

  1. 先停止接收新请求
  2. 等待所有进行中的请求完成(连接归还池中)
  3. 显式调用 pool.close().await(sqlx)等待所有连接关闭
  4. 在关闭顺序中,数据库连接池应该 最后关闭,因为其他组件可能还在用

相关链接