零停机部署 Rust 服务:如何在不丢失客户的情况下重新加载应用程序
想象一下这种情况:您推出了 Rust 服务的新版本,但在重启的那一刻,50 个用户遇到了 502 错误。有人丢失了数据,有人损失了金钱,还有人失去了对您产品的信任。是不是很熟悉?今天我们将讨论如何配置零停机部署 Rust 服务,使更新对客户透明。
零停机(或无中断部署)不是魔法,而是一套技术方法:优雅关闭、正确的连接管理和流量负载均衡。Rust 及其类型系统和异步运行时(Tokio)为我们提供了优雅解决这个问题的工具。开始吧!
问题:为什么普通重启会破坏生产环境?
当您简单地杀死进程(kill -9)或调用 std::process::exit() 时,操作系统会立即关闭所有套接字。此时正在发送请求的客户端会收到 Connection reset 或 Timeout。对于 HTTP 服务来说,这是一场灾难:浏览器显示错误,移动应用程序崩溃,后台任务丢失状态。
在 Rust 中,我们可以解决这个问题。我们需要:
- 停止接受新请求(但完成旧请求)。
- 等待所有活动处理程序完成。
- 然后才结束进程。
这被称为优雅关闭——零停机部署 Rust 服务的基础。
步骤 1. 使用 Tokio 在 Rust 中实现优雅关闭
Tokio 提供了用于优雅关闭的内置机制。让我们看一个基于 axum(Rust 中流行的 Web 框架)的 HTTP 服务器示例。
没有优雅关闭的基本服务器
use axum::{Router, routing::get};use std::net::SocketAddr;
#[tokio::main]async fn main() { let app = Router::new().route("/", get(|| async { "Hello, World!" }));
let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap();}如果按下 Ctrl+C,服务器会直接崩溃——所有活动请求都会中断。这不行。
添加优雅关闭
use axum::{Router, routing::get};use std::net::SocketAddr;use tokio::signal;
async fn shutdown_signal() { // 等待 SIGTERM 或 SIGINT (Ctrl+C) let ctrl_c = async { signal::ctrl_c() .await .expect("failed to install Ctrl+C handler"); };
#[cfg(unix)] let terminate = async { signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("failed to install signal handler") .recv() .await; };
#[cfg(not(unix))] let terminate = std::future::pending::<()>();
tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, }
println!("收到终止信号,开始优雅关闭...");}
#[tokio::main]async fn main() { let app = Router::new().route("/", get(|| async { "Hello, World!" }));
let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); axum::Server::bind(&addr) .serve(app.into_make_service()) .with_graceful_shutdown(shutdown_signal()) .await .unwrap();
println!("服务器已完成工作。");}现在,当收到 SIGTERM 或 Ctrl+C 时,服务器:
- 停止接受新连接。
- 等待所有当前请求完成(使用默认超时)。
- 结束进程。
这是迈向零停机部署 Rust 服务的第一步。但是,如果您只有一台服务器,那么在等待请求完成期间,新客户端将无法连接。需要