Despliegue Zero-downtime de servicios Rust: cómo recargar la aplicación sin perder clientes
Imagine la situación: ha lanzado una nueva versión de su servicio Rust, pero en el momento del reinicio, 50 usuarios recibieron un error 502. Algunos perdieron datos, otros perdieron dinero y otros perdieron la confianza en su producto. ¿Un dolor familiar? Hoy analizaremos cómo configurar el despliegue zero-downtime de servicios Rust para que las actualizaciones pasen desapercibidas para los clientes.
Zero-downtime (o despliegue sin tiempo de inactividad) no es magia, sino un conjunto de técnicas técnicas: graceful shutdown, gestión adecuada de conexiones y equilibrio de tráfico. Rust, con su sistema de tipos y su runtime asíncrono (Tokio), nos brinda las herramientas para resolver este problema de manera elegante. ¡Vamos allá!
El problema: ¿por qué un reinicio normal mata la producción?
Cuando simplemente mata el proceso (kill -9) o llama a std::process::exit(), el sistema operativo cierra instantáneamente todos los sockets. Los clientes que estaban enviando solicitudes en ese momento reciben Connection reset o Timeout. Para los servicios HTTP, esto es un desastre: los navegadores muestran un error, las aplicaciones móviles fallan y las tareas en segundo plano pierden su estado.
En Rust podemos solucionarlo. Necesitamos:
- Dejar de aceptar nuevas solicitudes (pero terminar las antiguas).
- Esperar a que finalicen todos los manejadores activos.
- Solo entonces finalizar el proceso.
Esto se llama graceful shutdown — la base del despliegue zero-downtime de servicios Rust.
Paso 1. Graceful shutdown en Rust con Tokio
Tokio proporciona mecanismos integrados para el graceful shutdown. Veamos un ejemplo de un servidor HTTP con axum (un popular framework web en Rust).
Servidor básico sin graceful shutdown
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();}Si presiona Ctrl+C, el servidor simplemente se cae — todas las solicitudes activas se interrumpen. No sirve.
Añadiendo graceful shutdown
use axum::{Router, routing::get};use std::net::SocketAddr;use tokio::signal;
async fn shutdown_signal() { // Esperamos SIGTERM o 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!("Señal de terminación recibida, iniciando graceful shutdown...");}
#[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!("Servidor finalizado.");}Ahora, al recibir SIGTERM o Ctrl+C, el servidor:
- Deja de aceptar nuevas conexiones.
- Espera a que finalicen todas las solicitudes actuales (con un tiempo de espera predeterminado).
- Finaliza el proceso.
Este es el primer paso hacia el despliegue zero-downtime de servicios Rust. Pero si tiene un solo servidor, durante el tiempo de espera para que finalicen las solicitudes, los nuevos clientes no podrán conectarse. Necesit