⚙️ Circus – Gestion de Processus & Sockets (Python/ZMQ)
Guide complet IDEO-Lab sur le gestionnaire de processus de Mozilla (circusd, circusctl).
Concept : Gestionnaire Dynamique
Process manager (Python), Keep-Alive, par Mozilla.
Circus Python MozillaArchitecture (ZMQ)
circusd (Daemon), circusctl (CLI), Pub/Sub (ZeroMQ).
vs. Supervisor
Dynamique (Circus) vs Statique (Supervisor).
Supervisor ComparatifInstallation (pip)
pip install circus. Dépendances (pyzmq, tornado...).
Config : circus.ini (Structure)
[circus] (Global), [watcher:x], [socket:x], [plugin:x].
Config : Endpoints (ZMQ)
endpoint, pubsub_endpoint, stats_endpoint.
Config : [watcher:x]
Le "programme" (cmd, user, working_dir).
Watcher : Scaling (numprocesses)
numprocesses = N. (Gère N copies).
Watcher : HA (Restart)
autorestart, max_age (Restart par temps).
Config : [socket:x] (Concept)
Définition du socket (host, port, path).
Socket Activation (Héritage FD)
Binder (root), passer le FD, lancer le worker (non-root).
Socket Activation File DescriptorCas d'Usage : Gunicorn + Socket
cmd = gunicorn --fd $(CIRCUS_FD_web).
CLI : circusctl (Shell)
status, list, stats. (Connexion ZMQ).
CLI : Gestion (start/stop/restart)
start [watcher], stop [watcher], restart.
CLI : Commandes Dynamiques
addwatcher, rmwatcher, incr/decr (Scaling).
Outil : circus-web (UI)
Activer (pip install), Config ([plugin:httpd]).
Hooks (Callbacks Python)
before_start, after_stop. (Intégration avancée).
API Python (Client)
circus.Client (Contrôler circusd depuis Python).
Gestion des Logs
stdout_stream, stderr_stream (FileStream).
vs. PM2
Python/Généraliste (Circus) vs Node.js/Cluster (PM2).
PM2 ComparatifCheat-sheet
Résumé circusctl & circus.ini.
Qu'est-ce que Circus ?
Circus est un gestionnaire de processus (Process Manager) et un "watchdog" (chien de garde), écrit en Python (développé à l'origine par Mozilla).
Comme Supervisor (guide précédent), son objectif principal est de lancer, surveiller, et redémarrer (keep-alive) des processus (ex: des serveurs web, des workers).
Il est conçu pour être une alternative "moderne" à Supervisor, en mettant l'accent sur la flexibilité et la gestion dynamique.
Philosophie (Dynamique & Sockets)
Circus a été conçu pour résoudre certaines limitations de Supervisor :
- Gestion de Sockets (Socket Management) : (Sa force n°1) Peut ouvrir (bind) des sockets (ex: Port 80) au nom des processus (similaire à
systemd). - Gestion Dynamique : (Sa force n°2) Permet d'ajouter/supprimer des watchers (processus) "à chaud" (via
circusctl addwatcher) sans recharger la configuration. - Communication (ZMQ) : Utilise ZeroMQ (ZMQ) pour la communication (Pub/Sub) entre le daemon et le client, au lieu de XML-RPC (Supervisor).
- Web UI : Intègre une interface web (
circus-web).
circusd, circusctl, ZMQ)Circus utilise une architecture Client/Serveur, mais basée sur la messagerie ZeroMQ (ZMQ).
Composants
circusd: Le daemon (serveur) principal. Il gère les "Watchers" (processus enfants) et les "Sockets".circusctl: L'outil CLI (client). Il envoie des commandes (messages ZMQ) au daemoncircusd.circus-web: (Optionnel) Un plugin (pip install circus-web) qui fournit une interface web (viacircushttpd) pour monitorer/contrôler.
Communication (ZMQ Pub/Sub)
C'est le cœur de l'architecture "dynamique" de Circus.
circusd expose (par défaut) deux endpoints (sockets ZMQ) :
endpoint(ZMQ REQ/REP) : (Le "Contrôleur") Le socketREP(Reply) oùcircusdécoute les commandes (REQ- Request) decircusctl.pubsub_endpoint(ZMQ PUB/SUB) : (L'"Event Bus") Le socketPUB(Publish) oùcircusdpublie (broadcast) tous les événements (ex: "Processus 'A' a crashé", "Processus 'B' démarré"). Les clients (circusctl,circus-web) s'abonnent (SUB) à ce flux.
Avantage : Permet un monitoring "temps réel" (basé Push/Events) plutôt que "polling" (basé Pull/Request).
Les deux sont des gestionnaires de processus en Python, mais avec des philosophies différentes.
| Critère | Supervisor (Guide Précédent) | Circus (Mozilla) |
|---|---|---|
| Objectif | Stabilité (Gardien "simple"). | Dynamisme (Gestion "à chaud"). |
| Dépendances | Python (pur). | Python + ZMQ (Lib C). |
| Communication | XML-RPC (Client/Serveur synchrone). | ZeroMQ (Pub/Sub asynchrone). |
| Gestion Config | Statique (Nécessite reread + update). | Dynamique (Supporte addwatcher/rmwatcher à chaud). |
| Socket Activation | Non. | Oui ([socket:x]). (Peut binder Port 80 en root, et passer le FD au worker non-root). |
| Popularité | Extrêmement populaire (Standard de facto). | Moins populaire (Niche). |
| Usage | Gérer gunicorn, celery (simple). | Gérer gunicorn (scalé, binding socket), applications distribuées. |
Circus s'installe via pip (le gestionnaire de paquets Python). Il est recommandé de l'installer dans un environnement virtuel (venv) si vous l'utilisez pour un projet spécifique, ou globalement si c'est un service système.
Dépendances (ZMQ)
Circus dépend de pyzmq, qui lui-même dépend de libzmq (une librairie C). Vous devez souvent installer les dépendances de compilation avant.
# (Debian/Ubuntu) $ sudo apt-get update $ sudo apt-get install -y python3-dev python3-pip libzmq3-dev # (Installation de Circus) $ sudo pip3 install circus # (Optionnel : Installer la Web UI) $ sudo pip3 install circus-web # Vérifier $ circusd --version
circus.ini (Structure)La configuration de Circus (.ini) est très similaire à celle de Supervisor.
# Fichier: circus.ini # --- 1. Section Globale [circus] --- # (Définit les endpoints de communication ZMQ) [circus] endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 check_delay = 5 # --- 2. Section Watcher (Processus) --- # (Voir 3.1) [watcher:my_app] cmd = /path/to/gunicorn myapp:app numprocesses = 4 user = www-data # --- 3. Section Socket (Gestion) --- # (Voir 4.1) [socket:web] host = 127.0.0.1 port = 8080 # --- 4. Section Plugins (Web UI) --- # (Voir 6.1) [plugin:httpd] use = circusweb.circushttpd port = 8000
La section [circus] (globale) définit les endpoints (points de terminaison) ZMQ que le daemon circusd va ouvrir.
| Directive | Socket ZMQ | Rôle |
|---|---|---|
endpoint | REQ/REP | (Le "Contrôleur"). L'endpoint principal où circusctl envoie ses commandes (start, stop...). |
pubsub_endpoint | PUB/SUB | (L'"Event Bus"). L'endpoint où circusd publie (broadcast) tous les événements (start, crash...). |
stats_endpoint | PUB/SUB | (Optionnel) Endpoint (séparé) qui publie les statistiques (CPU/RAM). (Utilisé par circus-web). |
Format (Socket ZMQ)
- IPC (Local) : (Socket UNIX)
ipc:///tmp/circus.sock - TCP (Réseau) :
tcp://127.0.0.1:5555
[watcher:x] (Le Programme)Un Watcher (Observateur) est l'équivalent du [program:x] de Supervisor. C'est le processus à surveiller.
[watcher:my_gunicorn_app] # (La commande à lancer) cmd = /path/to/myenv/bin/gunicorn myapp:app --bind ... # (Le dossier de travail) working_dir = /srv/myapp # (Lancer en tant que cet utilisateur) user = www-data # (Scaling) Lancer 4 instances (workers) de cette commande numprocesses = 4 # (Keep-Alive) Redémarrer si ça crash autorestart = true # (Logs stdout/stderr) # (Par défaut, Circus gère les logs via ZMQ/API. # On peut les rediriger vers des fichiers :) stdout_stream.class = FileStream stdout_stream.filename = /var/log/myapp.out stderr_stream.class = FileStream stderr_stream.filename = /var/log/myapp.err
numprocesses)La directive numprocesses (dans [watcher:x]) contrôle le nombre d'instances (processus) de cette commande à lancer.
[watcher:my-worker] cmd = python worker.py # (Lance 8 copies de worker.py) numprocesses = 8
Différence vs PM2 : numprocesses est "stupide". Il lance N copies. Il n'inclut pas le "Load Balancer (TCP)" magique du cluster mode de PM2.
Si vous lancez numprocesses = 8 sur un serveur web (Gunicorn) sans [socket] (4.3), les 8 processus essaieront de binder le même Port 8000 et échoueront (sauf si Gunicorn gère lui-même le partage, ce qu'il peut faire).
Usage : Idéal pour les workers (Celery, RabbitMQ) qui n'écoutent pas sur un port (ils "pull" depuis une queue).
Directives (dans [watcher:x]) pour le "Keep-Alive".
| Directive | Défaut | Description (Équiv. Supervisor) |
|---|---|---|
autorestart | false | Si true, redémarre le processus s'il s'arrête (proprement ou crash). (Équiv. autorestart=true). |
max_retries | 5 | Nombre de tentatives (startretries). Si -1 = infini. |
warmup_delay | 0 | Temps (sec) avant de considérer le démarrage "réussi". (startsecs). |
max_age | -1 (Infini) | (Intéressant) Redémarre (Recycle) le worker s'il a tourné pendant X secondes (ex: pour contrer les fuites mémoire). |
graceful_timeout | 30 | Temps (sec) (stopwaitsecs) pour attendre (SIGTERM) avant de tuer (SIGKILL). |
[socket:x] (Concept)C'est la fonctionnalité la plus puissante de Circus (similaire à systemd Socket Activation).
La section [socket:NOM] définit un socket que circusd (le daemon) va créer et gérer.
[socket:web_socket] # (Nom du socket) # (Type de socket : 'tcp' ou 'unix') host = 0.0.0.0 port = 8080 # (ou 'path = /tmp/mysocket.sock') # (Permissions (si socket UNIX)) ; mode = 0770
Les [watcher] (3.1) peuvent ensuite "demander" (via use_sockets) à utiliser ce socket (voir 4.3).
Le Problème (Permissions)
Pour binder (écouter) le Port 80 (privilégié), Gunicorn (ou votre app) doit être lancé en root (dangereux). Si on le lance en www-data, il ne peut binder que des ports > 1024 (ex: 8000), et il faut un Nginx (Reverse Proxy) devant.
La Solution (Circus Socket - Héritage de File Descriptor)
Vous laissez circusd (qui tourne en root) créer le socket (Port 80) avant de lancer le worker.
Flux (Simplifié) : 1. [circusd (root)] démarre. 2. [circusd (root)] lit [socket:web] (port 80). 3. [circusd (root)] Ouvre (bind) le Port 80. (Le Kernel lui donne le File Descriptor (FD) 3). 4. [circusd (root)] lit [watcher:gunicorn] (user=www-data). 5. [circusd (root)] "droppe" ses privilèges (devient www-data). 6. [circusd (www-data)] lance (fork) 'gunicorn'. 7. [circusd (www-data)] passe (hérite) le FD 3 au processus 'gunicorn' (via env var). 8. [gunicorn (www-data)] démarre et écoute sur le FD 3. Résultat : L'application (gunicorn) tourne en non-root (www-data) mais écoute sur un port privilégié (80).
Exemple complet de configuration (circus.ini) pour Gunicorn (Python) utilisant la "Socket Activation".
[circus]
# ... (endpoints)
# 1. Définir le socket (nommé "web")
[socket:web]
host = 127.0.0.1
port = 8000
# (On peut mettre port = 80 si circusd est root)
# 2. Définir le watcher (Gunicorn)
[watcher:gunicorn_app]
# 3. La commande Gunicorn
# (Note: Gunicorn est compatible 'systemd'
# et lit '--fd [num]' pour l'héritage)
cmd = /path/to/myenv/bin/gunicorn myapp:app \
--workers 4 \
--fd $(CIRCUS_FD_web)
# 4. (Le 'FD' (File Descriptor) est passé via une
# variable d'environnement nommée $(CIRCUS_FD_[nom_socket]))
# 5. Spécifier d'utiliser (hériter) le socket
use_sockets = web
# (Le reste de la config)
user = www-data
working_dir = /srv/myapp
autorestart = true
circusctl (Shell)circusctl est l'équivalent de supervisorctl. C'est le client CLI qui se connecte au daemon circusd (via ZMQ).
Commandes (Mode Interactif ou Direct)
$ circusctl circusctl 0.17.1 circus> status circus: active my_app: active ├─ 1: active (pid 1234) └─ 2: active (pid 1235) # (Alias pour 'status') circus> list # (Statistiques (CPU/MEM) via ZMQ Stats) circus> stats ... # (Quitter) circus> quit
start/stop/restart)Commandes pour gérer le cycle de vie d'un watcher.
# (Syntaxe: circusctl [commande] [nom_watcher]) # Démarrer (si 'stopped') circus> start my_app # Arrêter (envoie SIGTERM, puis SIGKILL) circus> stop my_app # Redémarrer (Stop + Start) circus> restart my_app # (Toutes les commandes peuvent cibler un PID spécifique) circus> restart my_app:1 # Recharger la configuration (reload) circus> reloadconfig
La force de Circus (via ZMQ) est sa capacité à modifier la configuration à chaud (dynamiquement), sans reloadconfig.
addwatcher / rmwatcher
Ajoute/Supprime un nouveau processus (Watcher) au daemon en cours d'exécution.
# (Ajoute un nouveau watcher 'mon_script_temporaire') $ circusctl addwatcher mon_script_temporaire "python script.py" # (Le watcher est ajouté et démarré) $ circusctl status ... mon_script_temporaire: active ...
incr / decr (Scaling)
Augmente ou diminue (dynamiquement) le numprocesses d'un watcher.
(État: my_app (numprocesses=4)) # (Augmente de 2 -> 6 workers) $ circusctl incr my_app 2 # (Diminue de 1 -> 5 workers) $ circusctl decr my_app 1
circus-web (Web UI)circus-web (similaire à l'UI de Supervisor) est un plugin qui fournit une interface web simple (basée sur bottle.py) pour monitorer et gérer Circus.
1. Installation (Plugin)
$ pip install circus-web
2. Configuration (circus.ini)
Vous devez "dire" à circusd de charger ce plugin (via la section [plugin]).
[circus] # (Le endpoint ZMQ 'stats' doit être activé) stats_endpoint = tcp://127.0.0.1:5557 # ... # (Charger le plugin Web UI) [plugin:httpd] # (Le 'point d'entrée' python du plugin) use = circusweb.circushttpd host = 0.0.0.0 port = 8000 # (Optionnel : Auth) # username = admin # password = secret
Après redémarrage de circusd, l'UI est accessible sur http://[IP_Serveur]:8000.
C'est une fonctionnalité avancée de Circus (similaire aux "Events" de Supervisor, mais en Python natif).
Vous pouvez définir des "hooks" (callbacks) Python qui sont exécutés à différents moments du cycle de vie du watcher.
[watcher:my_app] cmd = ... # (Définit le hook 'before_start' # vers la fonction 'mon_hook' # dans le module 'hooks.py') hooks.before_start = myapp.hooks.mon_hook
# Fichier: myapp/hooks.py
# (Le hook reçoit le watcher et d'autres infos)
def mon_hook(watcher, hook_name, **kwargs):
print(f"Hook '{hook_name}' déclenché pour {watcher.name}!")
# (Ex: Envoyer une alerte Slack)
return True # (True = Continuer, False = Annuler l'action)
Hooks Courants
before_start/after_startbefore_stop/after_stopbefore_restart/after_restart
Puisque Circus utilise ZMQ, il est facile de le piloter (comme circusctl) depuis un script Python.
# (Nécessite 'pip install circus')
import circus
# 1. Se connecter au endpoint ZMQ (REQ/REP)
client = circus.Client(endpoint="tcp://127.0.0.1:5555")
try:
# 2. Envoyer une commande (Message)
# (Ex: 'list' = 'status')
response = client.call("list")
print(response['statuses'])
# {'my_app': 'active'}
# 3. Envoyer une commande avec args (ex: restart)
response = client.call("restart", name="my_app")
except Exception as e:
print(f"Erreur: {e}")
Usage : Intégration dans des outils d'automatisation (Ansible, Fabric) ou des interfaces web personnalisées.
La gestion des logs (stdout/stderr) dans Circus est basée sur des "Streams" (flux).
Par défaut (si non configuré), Circus envoie tout au stdout/stderr du daemon circusd lui-même (qui peut être géré par systemd/journald).
Configuration (circus.ini)
Vous pouvez définir des "streams" (pipelines) pour stdout et stderr.
[watcher:my_app] cmd = ... # --- Option 1 : Fichiers (FileStream) --- stdout_stream.class = FileStream stdout_stream.filename = /var/log/myapp.out stdout_stream.max_bytes = 10485760 # 10MB stdout_stream.backup_count = 5 stderr_stream.class = FileStream stderr_stream.filename = /var/log/myapp.err # --- Option 2 : Rediriger stdout vers stderr --- stdout_stream.class = RedirectStream stdout_stream.to = stderr # --- Option 3 : ZMQ (Pub/Sub) --- # (Envoyer les logs sur un endpoint ZMQ) stdout_stream.class = ZMQStream
| Critère | Circus (Python) | PM2 (Node.js) |
|---|---|---|
| Langage Cible | Généraliste (Python). (Gère Gunicorn, Celery...). | Spécialisé Node.js. |
| Clustering (LB Natif) | Non. (Lance N copies, mais sans LB L4). | Oui (Mode Cluster). (LB L4 Round-Robin natif). |
| Socket Activation | Oui ([socket], --fd). | Non (Nécessite Nginx/systemd). |
| Gestion Dynamique | Oui (addwatcher, incr). | Non (Nécessite reload de l'Ecosystem). |
| Startup (Boot) | Non (Manuel, via systemd/cron). | Oui (pm2 startup). |
| Reload (Zero-Downtime) | Non (restart brutal). | Oui (reload). |
| Verdict | Bon pour les workers Python, excellent pour le binding de sockets (Gunicorn). | Standard de facto pour les apps web Node.js (grâce au Cluster). |
circusctl & circus.iniCLI (circusctl)
# --- Démarrage (Daemon) --- # (Lance le daemon en arrière-plan) $ circusd circus.ini # (Lance en foreground) $ circusd --log-output - circus.ini # --- Client (circusctl) --- $ circusctl # (Statut) circus> status circus> list # (Gestion) circus> start [watcher] circus> stop [watcher] circus> restart [watcher] # (Scaling Dynamique) circus> incr [watcher] [nb] circus> decr [watcher] [nb] # (Config) circus> reloadconfig
Config (circus.ini)
[circus] endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 check_delay = 5 include = /etc/circus/conf.d/*.ini # (Gérer un socket) [socket:web] host = 0.0.0.0 port = 80 # (Gérer un watcher) [watcher:my_app] cmd = /path/to/gunicorn --fd $(CIRCUS_FD_web) user = www-data numprocesses = 4 autorestart = true use_sockets = web
