Dans le développement logiciel, les mises à jour de code sont fréquentes. Pour les déploiements sur des serveurs de production sans recourir à des stratégies complexes comme le déploiement par canaris ou Kubernetes, il est crucial de garantir que les requêtes en cours soient traitées avant l'application des modifications. C'est là qu'intervient le concept de redémarrage à chaud (ou redémarrage gracieux).
Heureusement, les serveurs d'application WSGI couramment utilisés pour Python, tels que uWSGI et Gunicorn, intègrent des mécanismes pour réaliser ce type de redémarrage sans interruption de service.
Redémarrage à chaud avec uWSGI
Les expérimentations suivantes utilisent les versions suivantes :
- Python : 3.6.8
- Flask : 2.0.3
- uWSGI : 2.0.21
Application web simple
Créez un fichier main.py avec le contenu suivant :
import time
from flask import Flask
application = Flask(__name__)
@application.route("/")
def hello_world():
time.sleep(10) # Simule une requête longue
return "Bonjour, monde !"
if __name__ == "__main__":
application.run()
Configuration uWSGI
Créez un fichier uwsgi.ini avec la configuration suivante :
[uwsgi]
# Adresse et port d'écoute (protocole HTTP)
http = 0.0.0.0:8000
# Chemin vers le répertoire de l'application
chdir = ./
# Fichier Python contenant l'application WSGI
wsgi-file = main.py
# Nom de la variable d'application WSGI dans le fichier
callable = application
# Nombre de processus workers
processes = 4
# Nombre de threads par worker
threads = 2
# Activer le chargement différé des applications pour faciliter le redémarrage
lazy-apps = true
# Fichier à surveiller pour déclencher un redémarrage à chaud
# Le chemin doit être absolu ou relatif au répertoire de lancement de uWSGI.
# Lorsque ce fichier est touché, uWSGI initie un redémarrage gracieux.
touch-chain-reload = /chemin/vers/votre/fichier/reload_trigger.txt
Lancement de uWSGI
uwsgi --ini uwsgi.ini
Test du redémarrage à chaud
- Effectuez une requête vers
http://127.0.0.1:8000/. Vous remarquerez qu'elle prend 10 secondes à répondre à cause dutime.sleep(10). - Pendent que la première requête est en cours, modifiez le contenu retourné par la fonction
hello_worlddansmain.py(par exemple, en"Bonjour, mise à jour !"). - Dans le répertoire spécifié dans
touch-chain-reload(/chemin/vers/votre/fichier/), exécutez la commandetouch reload_trigger.txt. La modification du contenu de ce fichier déclenchera également le redémarrage. - La première requête devrait se terminer avec le message original
"Bonjour, monde !". - Effectuez une nouvelle requête vers
http://127.0.0.1:8000/. Vous devriez maintenant recevoir le message mis à jour"Bonjour, mise à jour !".
Ce comportement confirme que le redémarrage à chaud a été effectué avec succès. uWSGI a terminé de traiter les requêtes en cours avec les anciens workers avant de démarrer les nouveaux workers avec le code mis à jour.
Journalisation du redémarage à chaud
L'activation de touch-chain-reload entraîne un processus de redémarrage séquentiel des workers. Le journal uWSGI affichera des messages indiquant le remplacement progressif des processus. Vous verrez des indications comme "Gracefully killing worker X..." suivies de "Respawned uWSGI worker X...". La mention "chain reloading complete" marque la fin du processus.
Pendant le redémarrage, il est possible d'observer à la fois les anciens et les nouveaux processus uWSGI actifs. Il est important de noter que pendant la phase de redémarrage, il n'est pas garanti que toutes les nouvelles requêtes soient immédiatement dirigées vers les nouveaux processus.
Mise à jour en production avec uWSGI
- Déployez votre nouveau code sur le serveur.
- Identifiez les processus uWSGI en cours d'exécution (par exemple, avec
ps aux | grep uwsgi). - Vérifiez si le fichier de déclenchement existe. S'il existe, mettez à jour son horodatage avec
touch. Sinon, créez-le. - Surveillez les journaux uWSGI ou les processus pour confirmer que tous les workers ont été redémarrés et que le service est opérationnel avec le nouveau code.
Redémarrage à chaud avec Gunicorn
Application web simple (identique à l'exemple uWSGI)
Utilisez le même fichier main.py.
Configuration Gunicorn
Créez un fichier conf.py pour la configuration de Gunicorn :
# Niveau de verbosité des logs
loglevel = 'info'
# Adresse et port d'écoute
bind = "0.0.0.0:8888"
# Nombre de processus workers
workers = 2
# Nombre de threads par worker
threads = 2
# Délai d'attente avant de considérer une requête comme expirée
timeout = 600
# Chemin vers le fichier PID du processus maître
pidfile = "log/gunicorn.pid"
# Chemin vers le fichier de logs d'accès
accesslog = "log/access.log"
# Chemin vers le fichier de logs d'erreurs
errorlog = "log/debug.log"
# IMPORTANT : Ne pas activer 'preload_app = True' si vous souhaitez un redémarrage à chaud fonctionnel.
# Cette option charge l'application avant le démarrage des workers, ce qui empêche le redémarrage à chaud via HUP.
Lancement de Gunicorn
gunicorn -c conf.py main:application
Test du redémarrage à chaud
- Identifiez l'ID du processus maître de Gunicorn. Vous pouvez utiliser
pgrep gunicornoupstree -ap | grep gunicorn. - Envoyez un signal HUP (Hang Up) au processus maître :
kill -HUP <pid_du_maitre></pid_du_maitre>. - Exécutez à nouveau la commande pour identifier les processus Gunicorn. Vous devriez observer que les IDs des processus workers ont changé, indiquant qu'ils ont été remplacés par de nouveaux processus exécutant le code mis à jour. Vous pouvez également consulter les fichiers de log spécifiés dans
conf.pypour voir les messages relatifs à la réception du signal HUP et au redémarrage des workers.
Mise à jour en production avec Gunicorn
- Trouvez l'ID du processus maître Gunicorn à l'aide de
pgrep gunicorn. - Exécutez la commande
kill -HUP <pid_du_maitre></pid_du_maitre>pour déclencher le redémarrage. - Gunicorn gérera le remplacement des workers de manière graduelle, assurant ainsi la continuité du service pendant la mise à jour.