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, oworker_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:

console.log("Proceso" + process.argv[2] + "ejecutando." );

Código del archivo 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('Error code: '+error.code); console.log('Signal received: '+error.signal); } console.log('stdout: ' + stdout); console.log('stderr: ' + stderr); }); workerProcess.on('exit', function (code) { console.log('El proceso hijo ha salido, código de salida'+code); }); }

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 0
  • node support.js 1
  • node 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:

console.log("Proceso" + process.argv[2] + "ejecutando." );

Código del archivo 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('El proceso hijo ha salido, código de salida'+code); }); }

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 estruedel 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:

console.log("Proceso" + process.argv[2] + "Ejecutar." );

Código del archivo 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('El subproceso ha salido, código de salida' + code); }); }

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

// 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(`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

// Ejemplo resumido (en la rama cluster.isPrimary)
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

// pool/worker.js
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

// 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', () => {
      // 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

// 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('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 enworker.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

// 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('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