Déploiement sans interruption des services Rust : comment redémarrer une application sans perdre de clients
Imaginez la situation : vous déployez une nouvelle version de votre service Rust, mais au moment du redémarrage, 50 utilisateurs reçoivent une erreur 502. Certains ont perdu des données, d'autres de l'argent, et d'autres encore la confiance en votre produit. Une douleur familière ? Aujourd'hui, nous allons voir comment configurer un déploiement sans interruption des services Rust, afin que les mises à jour soient transparentes pour les clients.
Le déploiement sans interruption (ou déploiement sans temps d'arrêt) n'est pas de la magie, mais un ensemble de techniques : arrêt gracieux, gestion correcte des connexions et équilibrage du trafic. Rust, avec son système de types et son runtime asynchrone (Tokio), nous offre les outils pour résoudre ce problème avec élégance. C'est parti !
Le problème : pourquoi un redémarrage classique tue la production ?
Lorsque vous tuez simplement le processus (kill -9) ou appelez std::process::exit(), le système d'exploitation ferme immédiatement tous les sockets. Les clients qui envoyaient des requêtes à ce moment-là reçoivent une Réinitialisation de connexion ou un Délai d'attente. Pour les services HTTP, c'est une catastrophe : les navigateurs affichent une erreur, les applications mobiles plantent et les tâches en arrière-plan perdent leur état.
Avec Rust, nous pouvons corriger cela. Nous devons :
- Cesser d'accepter de nouvelles requêtes (mais terminer les anciennes).
- Attendre la fin de tous les gestionnaires actifs.
- Ensuite seulement, terminer le processus.
C'est ce qu'on appelle un arrêt gracieux — la base du déploiement sans interruption des services Rust.
Étape 1. Arrêt gracieux dans Rust avec Tokio
Tokio fournit des mécanismes intégrés pour l'arrêt gracieux. Prenons l'exemple d'un serveur HTTP avec axum (un framework web populaire en Rust).
Serveur de base sans arrêt gracieux
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 vous appuyez sur Ctrl+C, le serveur s'arrête simplement — toutes les requêtes actives sont interrompues. Pas acceptable.
Ajout de l'arrêt gracieux
use axum::{Router, routing::get};use std::net::SocketAddr;use tokio::signal;
async fn shutdown_signal() { // Attendre SIGTERM ou SIGINT (Ctrl+C) let ctrl_c = async { signal::ctrl_c() .await .expect("échec de l'installation du gestionnaire Ctrl+C"); };
#[cfg(unix)] let terminate = async { signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("échec de l'installation du gestionnaire de signal") .recv() .await; };
#[cfg(not(unix))] let terminate = std::future::pending::<()>();
tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, }
println!("Signal d'arrêt reçu, début de l'arrêt gracieux...");}
#[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!("Le serveur a terminé son travail.");}Maintenant, à la réception de SIGTERM ou Ctrl+C, le serveur :
- Cesse d'accepter les nouvelles connexions.
- Attend la fin de toutes les requêtes en cours (avec un délai d'attente par défaut).
- Termine le processus.
C'est la première étape vers un déploiement sans interruption des services Rust. Mais si vous avez un seul serveur, pendant le temps d'attente de la fin des requêtes, les nouveaux clients ne pourront pas se connecter. Il fau