Node.js Mehrprozess

Wir alle wissen, dass Node.js im Single-Threaded-Modus läuft, aber ereignisgesteuerte Verarbeitung verwendet, um Nebenläufigkeit zu handhaben, was uns hilft, mehrere Child-Prozesse auf Multi-Core-CPU-Systemen zu erstellen und dadurch die Leistung zu verbessern.

Jeder Child-Prozess hat immer drei Stream-Objekte: child.stdin, child.stdout und child.stderr. Sie können die stdio-Streams des Elternprozesses gemeinsam nutzen oder eigenständige Stream-Objekte sein, die gepipet werden.

Node stellt die Module child_process und cluster bereit, um Child-Prozesse zu erstellen.

child_processModul

  • exec- child_process.exec verwendet einen Child-Prozess, um einen Befehl auszuführen, puffert die Ausgabe des Child-Prozesses und gibt die Ausgabe des Child-Prozesses in Form von Parametern der Callback-Funktion zurück.

  • execFile- Führt eine ausführbare Datei direkt aus, etwas sicherer.

  • spawn- child_process.spawn erzeugt einen neuen Prozess mit angegebenen Kommandozeilenargumenten.

  • fork- child_process.fork ist eine spezielle Form von spawn() zum Ausführen von Modulen in einem Kindprozess, z. B. ist fork('./son.js') äquivalent zu spawn('node', ['./son.js']). Anders als die spawn-Methode erstellt fork einen Kommunikationskanal zwischen dem Elternprozess und dem Kindprozess für die Interprozesskommunikation.

clusterModul

  • Auf einem PortMehrere Worker-ProzesseGemeinsames Lauschen, wodurchein Prozess pro KernHTTP-Dienst realisiert wird.
  • Geeignet, um mehrere Kerne horizontal mit Webdiensten auszulasten.
  • Hinweis: Für neue Projekte wird offiziell empfohlen, sorgfältig abzuwägen (z. B. direkt einen Reverse-Proxy + mehrere Prozesse zu verwenden, oderworker_threadsfür CPU-intensive Aufgaben), aberclusteres bleibt weiterhin breit nutzbar.

exec()-Methode

child_process.exec verwendet einen Kindprozess, um einen Befehl auszuführen, puffert die Ausgabe des Kindprozesses und gibt die Ausgabe des Kindprozesses in Form von Callback-Funktionsparametern zurück.

Die Syntax lautet wie folgt:

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

Parameters

Die Parameterbeschreibungen lauten wie folgt:

command:String, der auszuführende Befehl, wobei die Parameter durch Leerzeichen getrennt werden

options: Object, kann sein:

  • cwd: String, das aktuelle Arbeitsverzeichnis des Kindprozesses
  • env: Object, Schlüssel-Wert-Paare für Umgebungsvariablen
  • encoding: String, Zeichenkodierung (Standard: 'utf8')
  • shell: String, die Shell zum Ausführen des Befehls (Standard: unter UNIX ist sie/bin/sh, unter Windows ist siecmd.exe, die Shell sollte erkennen-cSchalter unter UNIX, oder/s /cunter Windows. Unter Windows sollte die Befehlszeilenanalyse kompatibel sein mitcmd.exe)
  • timeout: Number, Timeout-Zeit (Standard: 0)
  • maxBuffer: Number, der maximale Puffer (binär), der in stdout oder stderr zulässig ist; wenn er überschritten wird, wird der Kindprozess beendet (Standard: 200*1024)
  • killSignal: String, Beendigungssignal (Standard: 'SIGTERM')
  • uid: Number, setzt die Benutzerprozess-ID
  • gid: Number, legt die Prozessgruppen-ID fest

callback :Callback-Funktion, die drei Parameter error, stdout und stderr enthält.

Die exec()-Methode gibt den größten Puffer zurück, wartet auf das Ende des Prozesses und gibt den Pufferinhalt auf einmal zurück.

Beispiel

Erstellen wir zwei JS-Dateien, support.js und master.js.

Code der Datei support.js:

console.log("Process" + process.argv[2] + "ausgeführt." );

Code der Datei 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('Fehlercode: '+error.code); console.log('Signal empfangen: '+error.signal); } console.log('stdout: ' + stdout); console.log('stderr: ' + stderr); }); workerProcess.on('exit', function (code) { console.log('Kindprozess wurde beendet, Exit-Code'+code); }); }

Führe den obigen Code aus, das Ausgabeergebnis ist:

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

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

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

stderr: 

Analyse:

execwird3 Kindprozesse starten, die jeweils ausgeführt werden:

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

Jeder Kindprozess gibt sofort nach der Ausführung aus:

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

Dieser Teil ist dieStandardausgabe (stdout)des Kindprozesses; sie wird gepuffert und zusammen zurückgegeben, wenn der Kindprozess endetexecan den Callback.

Wenn der Kindprozess endet, löst er zuerst dasexitEreignis aus, dannexecwird der Callback ausgeführt (Hinweis: Die Node.js-Dokumentation besagt, dassexitdas Ereignis eintritt, wenn der Prozess endet, undexecder Callback wirdstdioausgelöst, nachdem der Stream geschlossen wurde; normalerweise wird exit vor dem Callback ausgegeben).

Die Ausgabereihenfolge ist unbestimmt, da die 3 Kindprozesse parallel ausgeführt werden und es vom Scheduler des Betriebssystems abhängt, welcher zuerst fertig wird.


spawn()-Methode

child_process.spawn erstellt einen neuen Prozess mit angegebenen Befehlszeilenargumenten; die Syntax lautet wie folgt:

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

Parameters

Die Parameterbeschreibungen lauten wie folgt:

command:Der auszuführende Befehl

args:Array von String-Argumenten

options Objekt

  • cwd String, das aktuelle Arbeitsverzeichnis des Kindprozesses
  • env Objekt, Schlüssel-Wert-Paare für Umgebungsvariablen
  • stdio Array|String, die stdio-Konfiguration des Kindprozesses
  • detached Boolean, dieser Kindprozess wird der Leiter einer Prozessgruppe
  • uid Number, legt die Benutzer-Prozess-ID fest
  • gid Number, legt die Prozessgruppen-ID fest

Die spawn()-Methode gibt Streams (stdout & stderr) zurück, die verwendet werden, wenn der Prozess eine große Datenmenge zurückgibt. Sobald der Prozess mit der Ausführung beginnt, beginnt spawn() Antworten zu empfangen.

Beispiel

Erstellen wir zwei JS-Dateien, support.js und master.js.

Code der Datei support.js:

console.log("Process" + process.argv[2] + "ausgeführt." );

Code der Datei 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('Kindprozess wurde beendet, Exit-Code'+code); }); }

Führe den obigen Code aus, das Ausgabeergebnis ist:

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

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

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

子进程已退出,退出码 0

fork-Methode

child_process.fork ist eine spezielle Form der spawn()-Methode, die zum Erstellen von Prozessen verwendet wird; die Syntax lautet wie folgt:

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

Parameters

Die Parameterbeschreibungen lauten wie folgt:

modulePath: String, das Modul, das im Kindprozess ausgeführt werden soll

args: Array von String-Argumenten

options:Object

  • cwd String, das aktuelle Arbeitsverzeichnis des Kindprozesses
  • env Objekt, Schlüssel-Wert-Paare für Umgebungsvariablen
  • execPath String, die ausführbare Datei, die zum Erstellen des Kindprozesses verwendet wird
  • execArgv Array, das String-Argument-Array der ausführbaren Datei des Kindprozesses (Standard: process.execArgv)
  • silent Boolean, wenn diestrue, des Kindprozessesstdin,stdoutundstderrwerden dem Elternprozess zugeordnet, andernfalls werden sie vom Elternprozess geerbt. (Standard:false)
  • uid Number, legt die Benutzer-Prozess-ID fest
  • gid Number, legt die Prozessgruppen-ID fest

Das zurückgegebene Objekt hat zusätzlich zu allen Methoden einer ChildProcess-Instanz auch einen integrierten Kommunikationskanal.

Beispiel

Erstellen wir zwei JS-Dateien, support.js und master.js.

Code der Datei support.js:

console.log("Process" + process.argv[2] + "Ausführen." );

Code der Datei 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('Kindprozess wurde beendet, Exit-Code' + code); }); }

Führen Sie den obigen Code aus, die Ausgabe ist:

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

cluster: Multiprozess-HTTP-Dienst auf einem Port

Minimal nutzbarer Multi-Core-HTTP-Dienst

Beispiel

// 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(`Hauptprozess ${process.pid}, startet ${cpuCount}Worker-Prozesse`);
  for (let i = 0; i < cpuCount; i++) cluster.fork();

  cluster.on('exit', (worker, code) => {
    console.warn(`Worker-Prozess ${worker.process.pid}beendet (Code=${Code}), wird neu gestartet...`);
    cluster.fork();
  });
} else {
  const server = http.createServer((req, res) => {
    // Simuliere CPU-intensive Berechnung
    pbkdf2Sync('password', 'salt', 100_000, 64, 'sha512');
    res.writeHead(200, {'content-type':'text/plain; charset=utf-8'});
    res.end(`Von Worker bearbeitet ${process.pid}\n`);
  });

  server.listen(3000, () => {
    console.log(`Worker-Prozess ${process.pid}lauscht3000`);
  });
}

Ausführen:node server-cluster.js, mehrere Anfragen anhttp://localhost:3000werden unterschiedlichePID sehenAntworten.

Hinweis:Mehrere Online-Instanzen haben normalerweise einen Reverse-Proxy/Load-Balancer (Nginx, Envoy, K8s Service) vorgeschaltet oder verwenden Cluster, um auf einem Port zu bündeln.

WebSocket/Sticky-Sessions erfordern eine "Sticky Session" (derselbe Client landet beim selben Worker). Du kannst Source-Address-Hashing im L4-Load-Balancing verwenden oder Stickiness selbst in der Anwendungsschicht implementieren (z. B. Dispatch anhand von req.socket.remoteAddress).

Sanfter Neustart (Zero-Downtime-Ansatz)

Ansatz:Hauptprozess empfängt Reload-Signal → zuerst neuen Worker forken und warten, bis er lauscht → dann alten Worker trennen, warten, bis Anfragen abgeschlossen sind, bevor beendet wird.

Beispiel

// Beispielzusammenfassung (im cluster.isPrimary-Zweig)
process.on('SIGUSR2', async () => {
  console.log('SIGUSR2 empfangen, starte geordneten Neustart');
  const workers = Object.values(cluster.workers ?? {});
  // 1) Neue starten
  const fresh = cluster.fork();
  await new Promise(r => fresh.once('listening', r));
  // 2) Alte nacheinander herunterfahren
  for (const w of workers) {
    w?.disconnect();
    // Erzwungenes Beenden, falls Timeout nicht beendet wurde
    setTimeout(() => w?.process.kill('SIGKILL'), 5000);
  }
});

Windows hat kein SIGUSR2, du kannst die HTTP-/RPC-Verwaltungsschnittstelle verwenden, um ein Neuladen auszulösen.

Baue deinen eigenen "Prozesspool": Steuere die Nebenläufigkeit und verwende Kindprozesse wieder

Wenn du eine große Anzahl unabhängiger CPU-Aufgaben hast (wie Batch-Komprimierung, Verschlüsselung, Crawler-Parsing), sind häufige fork/spawn-Kosten hoch; dann benötigst du einen Prozesspool, um einige Kindprozesse wiederzuverwenden, wie einen "Thread-Pool", um die Nebenläufigkeit zu begrenzen.

Datei:pool/worker.js

Beispiel

// pool/worker.js
process.on('message', async (msg) => {
  if (msg.type === 'task') {
    const { id, payload } = msg;
    // Simuliere aufwändige Aufgabe (Fibonacci)
    const fib = (n) => (n <= 1 ? n : fib(n-1) + fib(n-2));
    const result = fib(payload.n);
    process.send({ type: 'done', id, result });
  }
});

Datei:pool/index.js

Beispiel

// 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', () => {
      // Pool automatisch auffüllen
      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();
  }
}

Datei:pool/demo.js

Beispiel

// 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('Ergebnis:', results);
console.log('Verstrichene Zeit(ms):', Date.now() - t0);

await new Promise(r => setTimeout(r, 100)); // Warten, bis Nachrichten geleert sind
pool.close();

Erklärung:

  • Verpacke "tasks" als Nachrichten, der Prozesspool ist für die Zuweisung und Wiederverwendung von Workern verantwortlich.
  • In der realen Geschäftswelt können SieParsing/Transcoding/Kompressioneinkapseln inworker.js.
  • HinweisTask-Timeout、Retry、IdempotenzundBackpressure(Warteschlangenlängenlimit).

Praktisches Beispiel: HTTP-Schnittstelle lagert schwere Aufgaben an einen Prozesspool aus

Beispiel

// 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('Fehler');
    }
  } else {
    res.writeHead(404).end('Nicht gefunden');
  }
});

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

Aufrufen: /fib?n=38usw., der Hauptprozess wird nicht blockiert, die Aufgabe wird im Kindprozess berechnet.

Weitere Erweiterungen