Node.js Multi-processus

Nous savons tous que Node.js fonctionne en mode monothread, mais il utilise un traitement piloté par les événements pour gérer la concurrence, ce qui nous aide à créer plusieurs processus enfants sur des systèmes à processeurs multicœurs, améliorant ainsi les performances.

Chaque processus enfant possède toujours trois objets de flux : child.stdin, child.stdout et child.stderr. Ils peuvent partager les flux stdio du processus parent, ou bien être des objets de flux indépendants et reliés par un pipe.

Node fournit les modules child_process et cluster pour créer des processus enfants.

child_processModule

  • exec- child_process.exec utilise un processus enfant pour exécuter une commande, met en mémoire tampon la sortie du processus enfant et renvoie la sortie du processus enfant sous la forme de paramètres de la fonction de rappel.

  • execFile- Exécute directement un fichier exécutable, légèrement plus sûr.

  • spawn- child_process.spawn crée un nouveau processus avec des arguments de ligne de commande spécifiés.

  • fork- child_process.fork est une forme spéciale de spawn() pour exécuter des modules dans un processus enfant, par ex., fork('./son.js') est équivalent à spawn('node', ['./son.js']). Contrairement à la méthode spawn, fork crée un canal de communication entre le processus parent et le processus enfant pour la communication inter-processus.

clusterModule

  • Sur un seul portPlusieurs processus de travailÉcoute partagée, réalisantun processus par cœurservice HTTP.
  • Convient pour remplir horizontalement plusieurs cœurs avec des services web.
  • Remarque : La recommandation officielle pour les nouveaux projets est d'évaluer attentivement (par ex., utiliser directement un proxy inverse + plusieurs processus, ouworker_threadspour les tâches intensives en CPU), maisclustercela reste largement utilisable.

méthode exec()

child_process.exec utilise un processus enfant pour exécuter une commande, met en mémoire tampon la sortie du processus enfant et renvoie la sortie du processus enfant sous la forme de paramètres de fonction de rappel.

La syntaxe est la suivante :

child_process.exec(command[, options], callback)

Parameters

Les descriptions des paramètres sont les suivantes :

command:String, la commande à exécuter, avec des paramètres séparés par des espaces

options: Object, peut être :

  • cwd: String, le répertoire de travail actuel du processus enfant
  • env: Object, paires clé-valeur des variables d'environnement
  • encoding: String, encodage des caractères (par défaut : 'utf8')
  • shell: String, le Shell pour exécuter la commande (par défaut : sous UNIX c'est/bin/sh, sous Windows c'estcmd.exe, le Shell doit reconnaître-coptions sous UNIX, ou/s /csous Windows. Sous Windows, l'analyse de la ligne de commande doit être compatible aveccmd.exe)
  • timeout: Number, délai d'attente (par défaut : 0)
  • maxBuffer: Number, la taille maximale du tampon (binaire) autorisée dans stdout ou stderr ; si elle est dépassée, le processus enfant sera tué (par défaut : 200*1024)
  • killSignal: String, signal de terminaison (par défaut : 'SIGTERM')
  • uid: Number, définit l'ID du processus utilisateur
  • gid: Number, définit l'ID du groupe de processus

callback :fonction de rappel, contenant trois paramètres error, stdout et stderr.

La méthode exec() renvoie le plus grand buffer, attend la fin du processus et renvoie le contenu du buffer en une seule fois.

Exemple

Créons deux fichiers js, support.js et master.js.

code du fichier support.js :

console.log("Process" + process.argv[2] + "exécuté." );

code du fichier master.js :

const fs = require('fs'); const child_process = require('child_process'); for(var i=0; i<3; i++) { var workerProcess = child_process.exec('node support.js '+i, function (error, stdout, stderr) { if (error) { console.log(error.stack); console.log('Code d'erreur : '+error.code); console.log('Signal reçu : '+error.signal); } console.log('stdout: ' + stdout); console.log('stderr: ' + stderr); }); workerProcess.on('exit', function (code) { console.log('Le processus enfant s'est terminé, code de sortie'+code); }); }

Exécutez le code ci-dessus, le résultat de sortie est :

$ node master.js 
子进程已退出,退出码 0
stdout: 进程 1 执行。

stderr: 
子进程已退出,退出码 0
stdout: 进程 0 执行。

stderr: 
子进程已退出,退出码 0
stdout: 进程 2 执行。

stderr: 

Analyse :

execva démarrer3 processus enfants, exécutant respectivement :

  • node support.js 0
  • node support.js 1
  • node support.js 2

Chaque processus enfant produit une sortie immédiatement après son exécution :

进程 0 执行。
进程 1 执行。
进程 2 执行。

Cette partie est lasortie standard (stdout)du processus enfant, elle sera mise en mémoire tampon et renvoyée en même temps lorsque le processus enfant se termineexecau callback.

Lorsque le processus enfant se termine, il déclenche d'abord l'exitévénement, puisexecle callback s'exécutera (Remarque : la documentation Node.js indique queexitl'événement se produit lorsque le processus se termine, etexecle callback eststdiodéclenché après la fermeture du flux ; généralement exit s'affiche avant le callback).

L'ordre de sortie est indéterminé, car les 3 processus enfants s'exécutent en parallèle, et celui qui se termine en premier dépend de l'ordonnanceur du système d'exploitation.


méthode spawn()

child_process.spawn crée un nouveau processus avec les arguments de ligne de commande spécifiés, la syntaxe est la suivante :

child_process.spawn(command[, args][, options])

Parameters

Les descriptions des paramètres sont les suivantes :

command:La commande à exécuter

args:Array d'arguments de type chaîne

options Object

  • cwd String, le répertoire de travail actuel du processus enfant
  • env Object, paires clé-valeur des variables d'environnement
  • stdio Array|String, configuration stdio du processus enfant
  • detached Boolean, ce processus enfant deviendra le chef d'un groupe de processus
  • uid Number, définit l'ID du processus utilisateur
  • gid Number, définit l'ID du groupe de processus

La méthode spawn() renvoie des flux (stdout & stderr), utilisée lorsque le processus renvoie une grande quantité de données. Dès que le processus commence à s'exécuter, spawn() commence à recevoir les réponses.

Exemple

Créons deux fichiers js, support.js et master.js.

code du fichier support.js :

console.log("Process" + process.argv[2] + "exécuté." );

code du fichier master.js :

const fs = require('fs'); const child_process = require('child_process'); for(var i=0; i<3; i++) { var workerProcess = child_process.spawn('node', ['support.js', i]); workerProcess.stdout.on('data', function (data) { console.log('stdout: ' + data); }); workerProcess.stderr.on('data', function (data) { console.log('stderr: ' + data); }); workerProcess.on('close', function (code) { console.log('Le processus enfant s'est terminé, code de sortie'+code); }); }

Exécutez le code ci-dessus, le résultat de sortie est :

$ node master.js stdout: 进程 0 执行。

子进程已退出,退出码 0
stdout: 进程 1 执行。

子进程已退出,退出码 0
stdout: 进程 2 执行。

子进程已退出,退出码 0

méthode fork

child_process.fork est une forme spéciale de la méthode spawn(), utilisée pour créer des processus, la syntaxe est la suivante :

child_process.fork(modulePath[, args][, options])

Parameters

Les descriptions des paramètres sont les suivantes :

modulePath: String, le module à exécuter dans le processus enfant

args: Tableau d’arguments de type chaîne

options:Object

  • cwd String, le répertoire de travail actuel du processus enfant
  • env Object, paires clé-valeur des variables d'environnement
  • execPath String, le fichier exécutable utilisé pour créer le processus enfant
  • execArgv Array, le tableau d'arguments de chaîne du fichier exécutable du processus enfant (par défaut : process.execArgv)
  • silent Boolean, s'il esttrue, du processus enfantstdin,stdoutetstderrseront associés au processus parent, sinon, ils seront hérités du processus parent. (par défaut :false)
  • uid Number, définit l'ID du processus utilisateur
  • gid Number, définit l'ID du groupe de processus

L'objet retourné, en plus de toutes les méthodes d'une instance ChildProcess, possède également un canal de communication intégré.

Exemple

Créons deux fichiers js, support.js et master.js.

code du fichier support.js :

console.log("Process" + process.argv[2] + "Exécuter." );

code du fichier master.js :

const fs = require('fs'); const child_process = require('child_process'); for(var i=0; i<3; i++) { var worker_process = child_process.fork("support.js", [i]); worker_process.on('close', function (code) { console.log('Le processus enfant s'est terminé, code de sortie' + code); }); }

Exécutez le code ci-dessus, la sortie est :

$ node master.js 
进程 0 执行。
子进程已退出,退出码 0
进程 1 执行。
子进程已退出,退出码 0
进程 2 执行。
子进程已退出,退出码 0

cluster : service HTTP multi-processus sur un seul port

Service HTTP multi-cœur minimal utilisable

Exemple

// server-cluster.js
import cluster from 'node:cluster';
import os from 'node:os';
import http from 'node:http';
import { pbkdf2Sync } from 'node:crypto';

if (cluster.isPrimary) {
  const cpuCount = Math.max(1, os.cpus().length);
  console.log(`Processus principal ${process.pid}, démarrage de ${cpuCount}processus worker`);
  for (let i = 0; i < cpuCount; i++) cluster.fork();

  cluster.on('exit', (worker, code) => {
    console.warn(`Processus worker ${worker.process.pid}quitté (code=${code}), redémarrage...`);
    cluster.fork();
  });
} else {
  const server = http.createServer((req, res) => {
    // Simuler un calcul intensif du CPU
    pbkdf2Sync('password', 'salt', 100_000, 64, 'sha512');
    res.writeHead(200, {'content-type':'text/plain; charset=utf-8'});
    res.end(`Traité par le worker ${process.pid}\n`);
  });

  server.listen(3000, () => {
    console.log(`Processus worker ${process.pid}à l'écoute3000`);
  });
}

Exécutez :node server-cluster.js, plusieurs requêteshttp://localhost:3000verront différentsPIDréponses.

Remarque :Plusieurs instances en ligne ont généralement un proxy inverse/répartiteur de charge (Nginx, Envoy, K8s Service) devant, ou utilisent le cluster pour agréger sur un seul port.

Les sessions WebSocket/sticky nécessitent une « sticky session » (le même client atterrit sur le même worker). Vous pouvez utiliser le hachage d'adresse source dans l'équilibrage de charge L4, ou implémenter la persistance vous-même au niveau de la couche application (par exemple, répartir selon req.socket.remoteAddress).

Redémarrage gracieux (approche zéro interruption)

Approche :Le processus principal reçoit un signal de rechargement → il fork d'abord un nouveau worker et attend qu'il soit à l'écoute → puis il déconnecte l'ancien worker, attend que les requêtes se terminent avant de quitter.

Exemple

// Exemple récapitulatif (dans la branche cluster.isPrimary)
process.on('SIGUSR2', async () => {
  console.log('SIGUSR2 reçu, démarrage du redémarrage gracieux');
  const workers = Object.values(cluster.workers ?? {});
  // 1) Démarrer de nouveaux workers
  const fresh = cluster.fork();
  await new Promise(r => fresh.once('listening', r));
  // 2) Arrêter les anciens un par un
  for (const w of workers) {
    w?.disconnect();
    // Forcer l'arrêt si le processus n'a pas quitté dans le délai
    setTimeout(() => w?.process.kill('SIGKILL'), 5000);
  }
});

Windows n'a pas SIGUSR2, vous pouvez utiliser l'interface de gestion HTTP / RPC pour déclencher le rechargement.

Construisez votre propre « process pool » : contrôlez la concurrence et réutilisez les processus enfants

Lorsque vous avez un grand nombre de tâches CPU indépendantes (comme la compression par lots, le chiffrement, l'analyse de crawler), les coûts fréquents de fork/spawn sont élevés ; à ce moment-là, vous avez besoin d'un pool de processus pour réutiliser quelques processus enfants, comme un « pool de threads » pour limiter la concurrence.

Fichier :pool/worker.js

Exemple

// pool/worker.js
process.on('message', async (msg) => {
  if (msg.type === 'task') {
    const { id, payload } = msg;
    // Simuler une tâche lourde (Fibonacci)
    const fib = (n) => (n <= 1 ? n : fib(n-1) + fib(n-2));
    const result = fib(payload.n);
    process.send({ type: 'done', id, result });
  }
});

Fichier :pool/index.js

Exemple

// pool/index.js
import { fork } from 'node:child_process';
import os from 'node:os';

export class ProcessPool {
  constructor({ file, size = Math.max(1, os.cpus().length - 1) } = {}) {
    this.file = file;
    this.size = size;
    this.idle = [];
    this.busy = new Map();  // worker -> taskId
    this.queue = [];
    for (let i = 0; i < size; i++) this._spawn();
  }

  _spawn() {
    const w = fork(this.file);
    w.on('message', (m) => {
      if (m?.type === 'done') {
        const cb = this.callbacks.get(m.id);
        if (cb) cb.resolve(m.result);
        this.callbacks.delete(m.id);
        this._markIdle(w);
        this._drain();
      }
    });
    w.on('exit', () => {
      // Remplir automatiquement le pool
      this.busy.delete(w);
      const idx = this.idle.indexOf(w);
      if (idx >= 0) this.idle.splice(idx, 1);
      this._spawn();
    });
    if (!this.callbacks) this.callbacks = new Map();
    this.idle.push(w);
  }

  _markIdle(w) {
    this.busy.delete(w);
    if (!this.idle.includes(w)) this.idle.push(w);
  }

  _acquire() {
    return this.idle.length ? this.idle.shift() : null;
  }

  _drain() {
    while (this.queue.length && this.idle.length) {
      const { id, payload, resolve, reject } = this.queue.shift();
      const w = this._acquire();
      this.busy.set(w, id);
      this.callbacks.set(id, { resolve, reject });
      w.send({ type: 'task', id, payload });
    }
  }

  runTask(payload) {
    const id = Math.random().toString(36).slice(2);
    return new Promise((resolve, reject) => {
      this.queue.push({ id, payload, resolve, reject });
      this._drain();
    });
  }

  close() {
    for (const w of this.idle) w.kill();
    for (const w of this.busy.keys()) w.kill();
  }
}

Fichier :pool/demo.js

Exemple

// pool/demo.js
import { ProcessPool } from './index.js';

const pool = new ProcessPool({ file: './pool/worker.js', size: 4 });

const tasks = Array.from({ length: 10 }, (_, i) => pool.runTask({ n: 35 + (i % 3) }));
const t0 = Date.now();
const results = await Promise.all(tasks);
console.log('Résultat:', results);
console.log('Temps écoulé(ms):', Date.now() - t0);

await new Promise(r => setTimeout(r, 100)); // Attendre que les messages soient vidés
pool.close();

Explication :

  • Encapsulez "tasks" sous forme de messages ; le pool de processus est chargé d'attribuer et de réutiliser les workers.
  • Dans un contexte professionnel réel, vous pouvezanalyse syntaxique/transcodage/compressionencapsuler dansworker.js.
  • RemarqueDélai d'attente de tâche、réessai、idempotenceetcontre-pression(limite de longueur de la file d'attente).

Exemple pratique : une interface HTTP déleste les tâches lourdes vers un pool de processus

Exemple

// app.js
import http from 'node:http';
import { ProcessPool } from './pool/index.js';

const pool = new ProcessPool({ file: './pool/worker.js', size: 4 });
const server = http.createServer(async (req, res) => {
  if (req.url?.startsWith('/fib?')) {
    const url = new URL(req.url, 'http://localhost');
    const n = Number(url.searchParams.get('n') || 35);
    try {
      const result = await pool.runTask({ n });
      res.writeHead(200, {'content-type': 'application/json'});
      res.end(JSON.stringify({ pid: process.pid, n, result }));
    } catch (e) {
      res.writeHead(500); res.end('erreur');
    }
  } else {
    res.writeHead(404).end('Non trouvé');
  }
});

server.listen(3000, () => console.log('http://localhost:3000'));

Visitez : /fib?n=38etc., le processus principal n'est pas bloqué, la tâche est calculée dans le processus enfant.

Autres extensions