Node.js multiprocesos
Todos sabemos que Node.js se ejecuta en modo de un solo hilo, pero utiliza la concurrencia impulsada por eventos, lo que nos ayuda a crear múltiples procesos hijos en sistemas con CPU multinúcleo, mejorando así el rendimiento.
Cada proceso hijo siempre tiene tres objetos de flujo: child.stdin, child.stdout y child.stderr. Pueden compartir los flujos stdio del proceso padre, o también pueden ser objetos de flujo independientes y redirigidos.
Node proporciona los módulos child_process y cluster para crear procesos hijos.
child_processMódulo
exec- child_process.exec ejecuta un comando utilizando un proceso hijo, almacena en caché la salida del proceso hijo y devuelve la salida del proceso hijo como parámetros de una función de devolución de llamada.
execFileEjecuta directamente archivos ejecutables, un poco más seguro.
spawn- child_process.spawn crea un nuevo proceso utilizando los parámetros de línea de comandos especificados.
fork- child_process.fork es una forma especial de spawn(), utilizada para módulos que se ejecutan en procesos hijos, como fork('./son.js') que equivale a spawn('node', ['./son.js']). A diferencia del método spawn, fork establece un canal de comunicación entre el proceso padre y el hijo, para la comunicación entre procesos.
clusterMódulo
- En un puertomúltiples procesos de trabajocomparten la escucha, implementandoun proceso por núcleodel servicio HTTP.
- Es adecuado para hacer que los servicios web aprovechen al máximo los múltiples núcleos de forma horizontal.
- Nota: los funcionarios recomiendan que los nuevos proyectos evalúen cuidadosamente (por ejemplo, usar directamente proxy inverso + multiprocesos, o
worker_threadstareas de uso intensivo de CPU), peroclustersigue siendo ampliamente utilizable.
Método exec()
child_process.exec ejecuta un comando utilizando un proceso hijo, almacena en caché la salida del proceso hijo y devuelve la salida del proceso hijo como parámetros de una función de devolución de llamada.
La sintaxis es la siguiente:
child_process.exec(command[, options], callback)
Parámetros
Los parámetros se explican a continuación:
command:Cadena: el comando que se ejecutará, con los parámetros separados por espacios.
options: objeto, que puede ser:
- cwd: cadena, directorio de trabajo actual del proceso hijo
- env: objeto, pares clave-valor de variables de entorno
- encoding: cadena, codificación de caracteres (predeterminado: 'utf8')
- shell: cadena, el shell que ejecutará el comando (por defecto: en UNIX es
/bin/sh, en Windows escmd.exe, el shell debería poder reconocer-cel conmutador en UNIX, o/s /cen Windows. En Windows, el análisis de la línea de comandos debería ser compatiblecmd.exe) - timeout: número, tiempo de espera (predeterminado: 0)
- maxBuffer: número, búfer máximo permitido en stdout o stderr (binario), si se excede, el proceso hijo será eliminado (predeterminado: 200*1024)
- killSignal: cadena, señal de terminación (predeterminado: 'SIGTERM')
- uid: número, establece el ID del proceso de usuario
- gid: número, establece el ID del grupo de procesos
callback :función de devolución de llamada, contiene tres parámetros: error, stdout y stderr.
El método exec() devuelve el búfer máximo y espera a que el proceso termine, devolviendo el contenido del búfer de una sola vez.
Ejemplo
Creemos dos archivos js: support.js y master.js.
Código del archivo support.js:
Código del archivo master.js:
Ejecuta el código anterior, el resultado es:
$ node master.js 子进程已退出,退出码 0 stdout: 进程 1 执行。 stderr: 子进程已退出,退出码 0 stdout: 进程 0 执行。 stderr: 子进程已退出,退出码 0 stdout: 进程 2 执行。 stderr:
Análisis:
execSe iniciarán3 procesos hijos, que se ejecutan respectivamente:
node support.js 0node support.js 1node support.js 2
Cada proceso hijo, después de ejecutarse, genera inmediatamente:
进程 0 执行。 进程 1 执行。 进程 2 执行。
Esta parte es lasalida estándar (stdout), se almacenará en búfer y, cuando el proceso hijo finalice, se devolverá aexecla devolución de llamada.
Cuando el proceso hijo sale, primero se activaexitel evento, luegoexecla función de devolución de llamada se ejecutará (nota: la documentación de Node.js indica queexitel evento ocurre cuando el proceso finaliza, mientras queexecla devolución de llamada esstdiose dispara después de que el flujo se cierre, generalmente exit se imprime antes que la devolución de llamada).
El orden de salida es indeterminado, porque los 3 procesos hijos se ejecutan en paralelo, quién termina primero depende de la programación del sistema operativo.
Método spawn()
child_process.spawn crea un nuevo proceso con los parámetros de línea de comandos especificados. El formato de sintaxis es el siguiente:
child_process.spawn(command[, args][, options])
Parámetros
Los parámetros se explican a continuación:
command:El comando que se ejecutará
args:Array: matriz de parámetros de cadena
options Object
- cwd: String, directorio de trabajo actual del proceso hijo
- env: Object, pares clave-valor de variables de entorno
- stdio: Array|String, configuración de stdio del proceso hijo
- detached: Boolean, este proceso hijo se convertirá en líder del grupo de procesos
- uid: Number, establece el ID del proceso de usuario
- gid: Number, establece el ID del grupo de procesos
El método spawn() devuelve flujos (stdout y stderr), se utiliza cuando el proceso devuelve una gran cantidad de datos. Una vez que el proceso comienza a ejecutarse, spawn() comienza a recibir la respuesta.
Ejemplo
Creemos dos archivos js: support.js y master.js.
Código del archivo support.js:
Código del archivo master.js:
Ejecuta el código anterior, el resultado es:
$ node master.js stdout: 进程 0 执行。 子进程已退出,退出码 0 stdout: 进程 1 执行。 子进程已退出,退出码 0 stdout: 进程 2 执行。 子进程已退出,退出码 0
Método fork
child_process.fork es una forma especial del método spawn(), utilizada para crear procesos. El formato de sintaxis es el siguiente:
child_process.fork(modulePath[, args][, options])
Parámetros
Los parámetros se explican a continuación:
modulePath: String, el módulo que se ejecutará en el proceso hijo
args: Array, matriz de parámetros de cadena
options:Object
- cwd: String, directorio de trabajo actual del proceso hijo
- env: Object, pares clave-valor de variables de entorno
- execPath: String, archivo ejecutable para crear el proceso hijo
- execArgv: Array, matriz de parámetros de cadena para el ejecutable del proceso hijo (predeterminado: process.execArgv)
- silent: Boolean, si es
truedel proceso hijostdin,stdoutystderrse asociarán al proceso padre; de lo contrario, heredarán del proceso padre. (Por defecto:false) - uid: Number, establece el ID del proceso de usuario
- gid: Number, establece el ID del grupo de procesos
El objeto devuelto, además de tener todos los métodos de una instancia de ChildProcess, también tiene un canal de comunicación incorporado.
Ejemplo
Creemos dos archivos js: support.js y master.js.
Código del archivo support.js:
Código del archivo master.js:
Al ejecutar el código anterior, el resultado es:
$ node master.js 进程 0 执行。 子进程已退出,退出码 0 进程 1 执行。 子进程已退出,退出码 0 进程 2 执行。 子进程已退出,退出码 0
cluster: servicio HTTP multiproceso en un solo puerto
El servicio HTTP multinúcleo mínimo utilizable
Ejemplo
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(`Proceso principal ${process.pid}, inicia ${cpuCount}procesos de trabajo`);
for (let i = 0; i < cpuCount; i++) cluster.fork();
cluster.on('exit', (worker, code) => {
console.warn(`Proceso de trabajo ${worker.process.pid}salió (código=${code}), reiniciando...`);
cluster.fork();
});
} else {
const server = http.createServer((req, res) => {
// Simular cálculo intensivo de CPU
pbkdf2Sync('password', 'salt', 100_000, 64, 'sha512');
res.writeHead(200, {'content-type':'text/plain; charset=utf-8'});
res.end(`Handled by worker ${process.pid}\n`);
});
server.listen(3000, () => {
console.log(`Proceso de trabajo ${process.pid}escuchando3000`);
});
}
Ejecutando:node server-cluster.js, múltiples solicitudeshttp://localhost:3000verás diferentesPIDrespuestas.
Nota:En producción, múltiples instancias suelen ir detrás de un proxy inverso/balanceador de carga (Nginx, Envoy, K8s Service), o el cluster agrupa en un solo puerto.
WebSocket/sesiones adhesivas requieren "sticky session" (el mismo cliente cae en el mismo worker). Se puede usar hash de dirección de origen en el balanceador de carga de capa 4, o implementar la persistencia en la capa de aplicación (por ejemplo, distribuir según req.socket.remoteAddress).
Reinicio elegante (concepto de cero tiempo de inactividad)
Idea:El proceso principal recibe la señal de recarga → primero hace fork de un nuevo worker y espera a que esté en listening → luego desconecta los workers antiguos, espera a que terminen las solicitudes y salgan.
Ejemplo
process.on('SIGUSR2', async () => {
console.log('Recibido SIGUSR2, iniciando reinicio elegante');
const workers = Object.values(cluster.workers ?? {});
// 1) Iniciar nuevos
const fresh = cluster.fork();
await new Promise(r => fresh.once('listening', r));
// 2) Desconectar los antiguos uno por uno
for (const w of workers) {
w?.disconnect();
// Si se agota el tiempo y no salen, matarlos a la fuerza
setTimeout(() => w?.process.kill('SIGKILL'), 5000);
}
});
Windows no tiene SIGUSR2; se puede usar una interfaz de administración HTTP/RPC para disparar la recarga.
«Pool de procesos» propio: controlar la concurrencia y reutilizar subprocesos
Cuando tienes muchas tareas de CPU independientes (como compresión por lotes, cifrado, análisis de crawlers), el costo de fork/spawn frecuente es alto; entonces necesitas un Process Pool para reutilizar varios subprocesos, limitando el flujo como un «thread pool».
Archivo:pool/worker.js
Ejemplo
process.on('message', async (msg) => {
if (msg.type === 'task') {
const { id, payload } = msg;
// Simular tarea pesada (Fibonacci)
const fib = (n) => (n <= 1 ? n : fib(n-1) + fib(n-2));
const result = fib(payload.n);
process.send({ type: 'done', id, result });
}
});
Archivo:pool/index.js
Ejemplo
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', () => {
// Completar el pool automáticamente
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();
}
}
Archivo:pool/demo.js
Ejemplo
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('Resultado:', results);
console.log('Tiempo (ms):', Date.now() - t0);
await new Promise(r => setTimeout(r, 100)); // Esperar a que se vacíen los mensajes
pool.close();
Explicación:
- Encapsular las «tareas» como mensajes; el process pool se encarga de asignar y reutilizar los workers.
- En negocios reales se puedeanálisis/transcodificación/compresiónencapsular en
worker.jsdentro. - NotaTiempo de espera de tarea、Reintentos、IdempotenciayContrapresión(límite de longitud de cola).
Ejemplo práctico: una interfaz HTTP envía tareas pesadas al pool de procesos
Ejemplo
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('error');
}
} else {
res.writeHead(404).end('Not Found');
}
});
server.listen(3000, () => console.log('http://localhost:3000'));
Acceder: /fib?n=38etc., el proceso principal no se bloquea, las tareas se calculan en los subprocesos.
Otras extensiones