devops
Le mode cluster de PM2, et l'état qui casse quand tu l'actives
Le mode cluster de PM2, et l'état qui casse quand tu l'actives
Node exécute le JavaScript sur un seul thread. Les E/S sont déchargées vers un pool de fond pour que le thread principal ne soit pas bloqué, mais l'exécution réelle — parser une requête, faire tourner un handler, sérialiser une réponse — se passe sur ce seul thread, une opération à la fois. Un serveur 32 cœurs qui fait tourner un seul node dist/main.js obtient le même plafond mono-cœur qu'un portable 2 cœurs. Ce n'est pas un drapeau de config manquant ; c'est la conception du runtime.
Je l'ai trouvé de la façon concrète : un test de charge où docker stats pendant la campagne montrait un cœur collé près de 100 % et dix-neuf inactifs.
module.exports = {
apps: [{
script: "dist/src/main.js",
exec_mode: "cluster",
instances: 2,
autorestart: true,
max_restarts: 10,
max_memory_restart: "450M",
}],
};
PM2 fork N workers qui bindent tous le même port, l'OS répartissant la charge entre eux. En plus de ça, pour quasi zéro code supplémentaire : redémarrage auto au crash, rechargement progressif sans coupure, agrégation des logs dans un seul flux. Comparé au module cluster brut de Node, PM2 retire entièrement la comptabilité fork et maître-worker.
L'activer n'est pas automatiquement sûr
Dès qu'il y a plus d'un processus, tout ce qui est tenu dans la mémoire d'un processus cesse d'être cohérent — les requêtes d'un client peuvent atterrir sur des workers différents qui ne savent rien les uns des autres. Trois choses cassent silencieusement :
- Les limiteurs de débit en mémoire — chaque worker obtient son propre compteur, et un cluster de N workers multiplie silencieusement chaque limite par N.
- L'état de room Socket.io en mémoire — un client sur le worker 1 qui devrait recevoir un broadcast du worker 2 ne le recevra simplement pas.
- Les jobs de fond
setIntervalbruts protégés uniquement par un drapeau en processus — N workers signifie un traitement en double N fois à moins d'une vraie revendication atomique qui coordonne entre eux.
Le correctif pour les trois a la même forme : déplace l'état partagé quelque part que chaque processus peut voir. Redis est le défaut — INCR atomique pour les compteurs, un adaptateur pub/sub pour Socket.io, SET NX pour les verrous. Fais ça avant d'activer plusieurs instances. (Quand j'ai audité ça, deux de mes trois jobs de fond se sont révélés dépourvus de la revendication atomique.)
Pas Kubernetes
Le clustering PM2 monte en charge dans une seule machine, jusqu'à son nombre de cœurs. Kubernetes monte en charge entre machines, avec des déploiements progressifs et des pods auto-réparants en plus. Aller chercher Kubernetes quand la vraie contrainte est « un processus ne peut pas utiliser plus d'un cœur » résout un problème que tu n'as pas encore, au prix d'un plan de contrôle et de manifests que tu exploites maintenant. Le bon déclencheur pour Kubernetes est d'avoir saturé le clustering sur le meilleur serveur unique disponible et d'avoir quand même besoin de plus.
Le prouver
exec_mode: "cluster" ne prouve rien en soi. Ce qui l'a réellement prouvé : pm2 list montrant les deux workers indépendamment en ligne ; SIGKILL sur les deux PID de worker et regarder PM2 les respawn en quelques secondes ; et — celui qui valide l'hypothèse d'état partagé — faire tourner une limite de débit délibérément basse sur les deux workers et confirmer exactement N succès, pas 2N.