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, oder
worker_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:
Code der Datei master.js:
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 0node support.js 1node 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:
Code der Datei master.js:
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 dies
true, 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:
Code der Datei master.js:
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
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
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
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
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
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 in
worker.js. - HinweisTask-Timeout、Retry、IdempotenzundBackpressure(Warteschlangenlängenlimit).
Praktisches Beispiel: HTTP-Schnittstelle lagert schwere Aufgaben an einen Prozesspool aus
Beispiel
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