Implantação zero-downtime de serviços Rust: como recarregar a aplicação sem perder clientes
Imagine a situação: você lançou uma nova versão do seu serviço Rust, mas no momento da reinicialização, 50 usuários receberam o erro 502. Alguém perdeu dados, alguém perdeu dinheiro e alguém perdeu a confiança no seu produto. Uma dor familiar? Hoje vamos analisar como configurar a implantação zero-downtime de serviços Rust para que as atualizações ocorram de forma imperceptível para os clientes.
Zero-downtime (ou implantação sem interrupções) não é mágica, mas um conjunto de técnicas: graceful shutdown, gerenciamento adequado de conexões e balanceamento de tráfego. Rust, com seu sistema de tipos e runtime assíncrono (Tokio), nos fornece ferramentas para resolver esse problema de forma elegante. Vamos nessa!
O problema: por que uma reinicialização comum mata a produção?
Quando você simplesmente mata o processo (kill -9) ou chama std::process::exit(), o sistema operacional fecha instantaneamente todos os sockets. Os clientes que estavam enviando requisições naquele momento recebem Connection reset ou Timeout. Para serviços HTTP, isso é um desastre: navegadores mostram erro, aplicativos móveis travam e tarefas em segundo plano perdem o estado.
Em Rust, podemos corrigir isso. Precisamos:
- Parar de aceitar novas requisições (mas finalizar as antigas).
- Aguardar a conclusão de todos os handlers ativos.
- Só então finalizar o processo.
Isso é chamado de graceful shutdown — a base da implantação zero-downtime de serviços Rust.
Passo 1. Graceful shutdown em Rust com Tokio
Tokio fornece mecanismos embutidos para graceful shutdown. Vamos considerar um exemplo de servidor HTTP com axum (framework web popular em Rust).
Servidor básico sem 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();}Se você pressionar Ctrl+C, o servidor simplesmente cai — todas as requisições ativas são interrompidas. Não serve.
Adicionando graceful shutdown
use axum::{Router, routing::get};use std::net::SocketAddr;use tokio::signal;
async fn shutdown_signal() { // Aguarda SIGTERM ou 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!("Sinal de término recebido, 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.");}Agora, ao receber SIGTERM ou Ctrl+C, o servidor:
- Para de aceitar novas conexões.
- Aguarda a conclusão de todas as requisições atuais (com timeout padrão).
- Finaliza o processo.
Este é o primeiro passo para a implantação zero-downtime de serviços Rust. Mas se você tem apenas um servidor, durante o tempo de espera pela conclusão das requisições, novos clientes não conseguirão se conectar. Preci