Home » Как запускать дочерние процессы в Node.js
Как запускать дочерние процессы в Node.js

Как запускать дочерние процессы в Node.js

Если ты когда-нибудь запускал Node.js-приложение на продакшене, то наверняка сталкивался с моментами, когда нужно было что-то выполнить на системном уровне — будь то обработка изображений через ImageMagick, создание архивов, или даже запуск других скриптов. Модуль child_process в Node.js — это твой швейцарский нож для работы с дочерними процессами. Он позволяет выполнять команды операционной системы, запускать другие программы и обмениваться данными между процессами.

В этой статье мы разберём, как правильно работать с дочерними процессами в Node.js, рассмотрим все доступные методы, их особенности и подводные камни. Особенно это актуально для серверных приложений, где производительность и стабильность критически важны.

Основы работы с child_process

Модуль child_process предоставляет четыре основных метода для создания дочерних процессов:

  • spawn() — самый низкоуровневый метод, запускает процесс и возвращает поток данных
  • exec() — выполняет команду в shell и буферизует весь вывод
  • execFile() — похож на exec(), но не использует shell
  • fork() — специализированный метод для запуска других Node.js скриптов

Каждый из этих методов имеет свои синхронные версии (execSync, execFileSync, spawnSync), но их использование в продакшене крайне не рекомендуется — они блокируют event loop.

Пошаговое руководство по каждому методу

Метод spawn() — для потоковой обработки данных

Это самый гибкий метод, который отлично подходит для работы с большими объёмами данных или долгоживущими процессами:

const { spawn } = require('child_process');

// Простой пример
const ls = spawn('ls', ['-la']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

Более практичный пример — обработка изображений:

const { spawn } = require('child_process');

function resizeImage(inputPath, outputPath, width, height) {
  return new Promise((resolve, reject) => {
    const convert = spawn('convert', [
      inputPath,
      '-resize', `${width}x${height}`,
      outputPath
    ]);

    convert.on('close', (code) => {
      if (code === 0) {
        resolve('Image resized successfully');
      } else {
        reject(new Error(`Convert process exited with code ${code}`));
      }
    });

    convert.on('error', reject);
  });
}

// Использование
resizeImage('input.jpg', 'output.jpg', 800, 600)
  .then(console.log)
  .catch(console.error);

Метод exec() — для простых команд

Когда нужно выполнить простую команду и получить результат целиком:

const { exec } = require('child_process');

// Получение информации о системе
exec('uname -a', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`System info: ${stdout}`);
});

// Использование с Promise
const { promisify } = require('util');
const execAsync = promisify(exec);

async function getSystemInfo() {
  try {
    const { stdout } = await execAsync('df -h');
    console.log('Disk usage:', stdout);
  } catch (error) {
    console.error('Error:', error.message);
  }
}

Метод execFile() — безопасная альтернатива exec()

Более безопасный способ выполнения команд, так как не использует shell:

const { execFile } = require('child_process');

// Безопасный запуск команды
execFile('node', ['--version'], (error, stdout, stderr) => {
  if (error) {
    console.error('Error:', error);
    return;
  }
  console.log('Node.js version:', stdout);
});

// Запуск Python скрипта
execFile('python3', ['script.py', 'arg1', 'arg2'], {
  cwd: '/path/to/script',
  env: { ...process.env, PYTHONPATH: '/custom/path' }
}, (error, stdout, stderr) => {
  if (error) {
    console.error('Python script error:', error);
    return;
  }
  console.log('Python output:', stdout);
});

Метод fork() — для Node.js скриптов

Специализированный метод для запуска других Node.js процессов с встроенным IPC:

// main.js
const { fork } = require('child_process');

const child = fork('worker.js');

// Отправка данных в дочерний процесс
child.send({ cmd: 'start', data: [1, 2, 3, 4, 5] });

// Получение данных от дочернего процесса
child.on('message', (msg) => {
  console.log('Received from child:', msg);
});

// worker.js
process.on('message', (msg) => {
  if (msg.cmd === 'start') {
    const result = msg.data.reduce((a, b) => a + b, 0);
    process.send({ result });
  }
});

Сравнение методов

Метод Использует shell Потоковые данные Буферизация Лучше всего для
spawn() Нет Да Нет Долгие процессы, большие данные
exec() Да Нет Да (до 1MB) Простые команды с небольшим выводом
execFile() Нет Нет Да (до 1MB) Безопасное выполнение файлов
fork() Нет Да Нет Node.js скрипты с IPC

Опции и настройки

Все методы принимают объект опций, который позволяет тонко настроить поведение процесса:

const options = {
  cwd: '/path/to/working/directory',    // Рабочая директория
  env: { ...process.env, NODE_ENV: 'production' }, // Переменные окружения
  stdio: 'inherit',                     // Настройка потоков ввода/вывода
  detached: true,                       // Отсоединённый процесс
  uid: 1000,                           // User ID (только Unix)
  gid: 1000,                           // Group ID (только Unix)
  timeout: 30000,                      // Таймаут в миллисекундах
  killSignal: 'SIGTERM',               // Сигнал для завершения
  maxBuffer: 1024 * 1024 * 10,         // Максимальный размер буфера (10MB)
  shell: '/bin/bash'                   // Конкретная оболочка
};

const child = spawn('long-running-process', [], options);

Обработка ошибок и мониторинг

Правильная обработка ошибок критически важна при работе с дочерними процессами:

const { spawn } = require('child_process');

function runCommand(command, args, options = {}) {
  return new Promise((resolve, reject) => {
    const child = spawn(command, args, options);
    
    let stdout = '';
    let stderr = '';
    
    child.stdout?.on('data', (data) => {
      stdout += data.toString();
    });
    
    child.stderr?.on('data', (data) => {
      stderr += data.toString();
    });
    
    child.on('error', (error) => {
      reject(new Error(`Failed to start process: ${error.message}`));
    });
    
    child.on('close', (code, signal) => {
      if (code === 0) {
        resolve({ stdout, stderr });
      } else {
        reject(new Error(`Process exited with code ${code}, signal ${signal}\nstderr: ${stderr}`));
      }
    });
    
    // Обработка таймаута
    const timer = setTimeout(() => {
      child.kill('SIGTERM');
      reject(new Error('Process timeout'));
    }, options.timeout || 30000);
    
    child.on('close', () => clearTimeout(timer));
  });
}

Продвинутые техники и паттерны

Пул процессов

Для интенсивной обработки данных можно создать пул дочерних процессов:

class ProcessPool {
  constructor(workerScript, poolSize = 4) {
    this.workers = [];
    this.queue = [];
    this.workerScript = workerScript;
    
    for (let i = 0; i < poolSize; i++) {
      this.createWorker();
    }
  }
  
  createWorker() {
    const worker = fork(this.workerScript);
    worker.busy = false;
    
    worker.on('message', (result) => {
      worker.busy = false;
      worker.callback(null, result);
      this.processQueue();
    });
    
    worker.on('error', (error) => {
      worker.busy = false;
      worker.callback(error);
      this.processQueue();
    });
    
    this.workers.push(worker);
  }
  
  execute(data) {
    return new Promise((resolve, reject) => {
      this.queue.push({ data, callback: (err, result) => {
        if (err) reject(err);
        else resolve(result);
      }});
      
      this.processQueue();
    });
  }
  
  processQueue() {
    if (this.queue.length === 0) return;
    
    const availableWorker = this.workers.find(w => !w.busy);
    if (!availableWorker) return;
    
    const task = this.queue.shift();
    availableWorker.busy = true;
    availableWorker.callback = task.callback;
    availableWorker.send(task.data);
  }
}

// Использование
const pool = new ProcessPool('./cpu-intensive-worker.js', 4);

async function processLargeDataset(dataset) {
  const results = await Promise.all(
    dataset.map(chunk => pool.execute(chunk))
  );
  return results;
}

Безопасность и изоляция

При выполнении пользовательских команд важно обеспечить безопасность:

const { spawn } = require('child_process');
const path = require('path');

// Безопасный запуск с ограничениями
function safeExecute(command, args, options = {}) {
  // Валидация команды
  const allowedCommands = ['convert', 'ffmpeg', 'node', 'python3'];
  if (!allowedCommands.includes(command)) {
    throw new Error(`Command ${command} is not allowed`);
  }
  
  // Создание безопасного окружения
  const safeOptions = {
    ...options,
    shell: false,
    env: {
      PATH: '/usr/bin:/bin',
      HOME: '/tmp',
      USER: 'nobody'
    },
    uid: 65534, // nobody
    gid: 65534, // nobody
    timeout: 30000
  };
  
  return spawn(command, args, safeOptions);
}

Интеграция с другими технологиями

Дочерние процессы отлично интегрируются с различными инструментами:

С Docker

const { spawn } = require('child_process');

function runInDocker(image, command, args, options = {}) {
  const dockerArgs = [
    'run', '--rm',
    '-v', `${process.cwd()}:/app`,
    '-w', '/app',
    '--user', `${process.getuid()}:${process.getgid()}`,
    image,
    command,
    ...args
  ];
  
  return spawn('docker', dockerArgs, options);
}

// Запуск Python скрипта в контейнере
const pythonProcess = runInDocker('python:3.9', 'python', ['script.py']);

С PM2

const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);

class PM2Manager {
  async startApp(name, script, options = {}) {
    const cmd = `pm2 start ${script} --name ${name} ${this.buildOptions(options)}`;
    return await execAsync(cmd);
  }
  
  async stopApp(name) {
    return await execAsync(`pm2 stop ${name}`);
  }
  
  async restartApp(name) {
    return await execAsync(`pm2 restart ${name}`);
  }
  
  async getStatus() {
    const { stdout } = await execAsync('pm2 jlist');
    return JSON.parse(stdout);
  }
  
  buildOptions(options) {
    const flags = [];
    if (options.instances) flags.push(`-i ${options.instances}`);
    if (options.maxMemory) flags.push(`--max-memory-restart ${options.maxMemory}`);
    if (options.env) flags.push(`--env ${options.env}`);
    return flags.join(' ');
  }
}

Мониторинг и отладка

Для production-среды важно иметь хороший мониторинг дочерних процессов:

const EventEmitter = require('events');

class ProcessMonitor extends EventEmitter {
  constructor() {
    super();
    this.processes = new Map();
  }
  
  spawn(command, args, options = {}) {
    const child = spawn(command, args, options);
    const processInfo = {
      pid: child.pid,
      command,
      args,
      startTime: Date.now(),
      memoryUsage: 0,
      cpuUsage: 0
    };
    
    this.processes.set(child.pid, processInfo);
    
    // Мониторинг ресурсов
    const monitorInterval = setInterval(() => {
      this.updateProcessStats(child.pid);
    }, 1000);
    
    child.on('close', (code) => {
      clearInterval(monitorInterval);
      this.processes.delete(child.pid);
      this.emit('process-closed', { pid: child.pid, code });
    });
    
    child.on('error', (error) => {
      this.emit('process-error', { pid: child.pid, error });
    });
    
    return child;
  }
  
  updateProcessStats(pid) {
    try {
      const usage = process.cpuUsage();
      const memUsage = process.memoryUsage();
      
      const processInfo = this.processes.get(pid);
      if (processInfo) {
        processInfo.memoryUsage = memUsage.rss;
        processInfo.cpuUsage = usage.user + usage.system;
      }
    } catch (error) {
      // Процесс завершился
    }
  }
  
  getStats() {
    return Array.from(this.processes.values());
  }
}

// Использование
const monitor = new ProcessMonitor();

monitor.on('process-error', ({ pid, error }) => {
  console.error(`Process ${pid} error:`, error);
});

monitor.on('process-closed', ({ pid, code }) => {
  console.log(`Process ${pid} closed with code ${code}`);
});

Производительность и оптимизация

Несколько важных советов для оптимизации работы с дочерними процессами:

  • Переиспользуйте процессы — создание нового процесса дорого, лучше использовать пулы
  • Ограничивайте количество процессов — слишком много процессов может привести к исчерпанию ресурсов
  • Используйте потоки для больших данных — spawn() с потоками эффективнее буферизации
  • Правильно настраивайте stdio — ‘ignore’ для неиспользуемых потоков экономит ресурсы
// Оптимизированный пример для обработки больших файлов
const { spawn } = require('child_process');
const fs = require('fs');

function processLargeFile(inputPath, outputPath) {
  return new Promise((resolve, reject) => {
    const input = fs.createReadStream(inputPath);
    const output = fs.createWriteStream(outputPath);
    
    const processor = spawn('some-processor', [], {
      stdio: ['pipe', 'pipe', 'inherit'] // stdin, stdout, stderr
    });
    
    input.pipe(processor.stdin);
    processor.stdout.pipe(output);
    
    processor.on('close', (code) => {
      if (code === 0) resolve();
      else reject(new Error(`Process failed with code ${code}`));
    });
  });
}

Развёртывание на VPS

При развёртывании приложений с дочерними процессами на VPS важно учесть несколько моментов. Для тестирования и development окружения отлично подойдёт VPS, а для production нагрузок лучше рассмотреть выделенный сервер.

Пример конфигурации для production:

// production-config.js
const config = {
  maxProcesses: process.env.MAX_PROCESSES || 4,
  processTimeout: 30000,
  memoryLimit: '512M',
  
  // Ограничения для безопасности
  security: {
    allowedCommands: ['convert', 'ffmpeg', 'node'],
    maxExecutionTime: 60000,
    runAsUser: 'appuser',
    runAsGroup: 'appgroup'
  },
  
  // Мониторинг
  monitoring: {
    enabled: true,
    logLevel: 'info',
    metricsInterval: 5000
  }
};

module.exports = config;

Альтернативные решения

Хотя встроенный модуль child_process покрывает большинство потребностей, существуют и альтернативы:

  • execa — улучшенная версия child_process с лучшим API
  • shelljs — Unix shell команды для Node.js
  • worker_threads — для CPU-интенсивных задач на JavaScript
  • piscina — быстрый пул воркеров для Node.js

Пример использования execa:

const execa = require('execa');

// Более удобный API
const { stdout } = await execa('echo', ['Hello World']);
console.log(stdout); // 'Hello World'

// С лучшей обработкой ошибок
try {
  await execa('non-existent-command');
} catch (error) {
  console.log(error.exitCode); // 127
  console.log(error.stderr); // 'command not found'
}

Интересные факты и нестандартные применения

Несколько интересных способов использования дочерних процессов:

Создание микросервисной архитектуры

// Каждый сервис как отдельный процесс
class MicroserviceManager {
  constructor() {
    this.services = new Map();
  }
  
  startService(name, script, port) {
    const service = fork(script, [], {
      env: { ...process.env, PORT: port }
    });
    
    this.services.set(name, { process: service, port });
    
    service.on('message', (msg) => {
      if (msg.type === 'health') {
        console.log(`Service ${name} health: ${msg.status}`);
      }
    });
    
    return service;
  }
  
  async healthCheck() {
    const promises = Array.from(this.services.entries()).map(([name, { process }]) => {
      return new Promise((resolve) => {
        process.send({ type: 'health-check' });
        
        const timeout = setTimeout(() => {
          resolve({ name, status: 'unhealthy' });
        }, 5000);
        
        process.once('message', (msg) => {
          if (msg.type === 'health') {
            clearTimeout(timeout);
            resolve({ name, status: msg.status });
          }
        });
      });
    });
    
    return Promise.all(promises);
  }
}

Создание CLI-обёртки

// CLI wrapper для сложных команд
class CLIWrapper {
  constructor(command, defaultOptions = {}) {
    this.command = command;
    this.defaultOptions = defaultOptions;
  }
  
  async run(args = [], options = {}) {
    const finalOptions = { ...this.defaultOptions, ...options };
    const child = spawn(this.command, args, finalOptions);
    
    return new Promise((resolve, reject) => {
      let output = '';
      let error = '';
      
      child.stdout?.on('data', (data) => {
        output += data;
        if (finalOptions.onData) finalOptions.onData(data);
      });
      
      child.stderr?.on('data', (data) => {
        error += data;
        if (finalOptions.onError) finalOptions.onError(data);
      });
      
      child.on('close', (code) => {
        if (code === 0) {
          resolve({ output, error });
        } else {
          reject(new Error(`Command failed with code ${code}: ${error}`));
        }
      });
    });
  }
}

// Использование
const git = new CLIWrapper('git', { cwd: '/path/to/repo' });

await git.run(['status', '--porcelain'], {
  onData: (data) => console.log(`Git: ${data}`)
});

Автоматизация и CI/CD

Дочерние процессы незаменимы для создания систем автоматизации:

// Система деплоя
class DeploymentSystem {
  constructor(config) {
    this.config = config;
  }
  
  async deploy(branch = 'main') {
    const steps = [
      () => this.gitPull(branch),
      () => this.installDependencies(),
      () => this.runTests(),
      () => this.buildApplication(),
      () => this.restartServices()
    ];
    
    for (const step of steps) {
      await step();
    }
  }
  
  async gitPull(branch) {
    console.log(`Pulling ${branch}...`);
    await this.runCommand('git', ['pull', 'origin', branch]);
  }
  
  async installDependencies() {
    console.log('Installing dependencies...');
    await this.runCommand('npm', ['ci']);
  }
  
  async runTests() {
    console.log('Running tests...');
    await this.runCommand('npm', ['test']);
  }
  
  async buildApplication() {
    console.log('Building application...');
    await this.runCommand('npm', ['run', 'build']);
  }
  
  async restartServices() {
    console.log('Restarting services...');
    await this.runCommand('pm2', ['restart', 'all']);
  }
  
  runCommand(command, args) {
    return new Promise((resolve, reject) => {
      const child = spawn(command, args, { stdio: 'inherit' });
      child.on('close', (code) => {
        if (code === 0) resolve();
        else reject(new Error(`Command failed: ${command} ${args.join(' ')}`));
      });
    });
  }
}

Заключение и рекомендации

Модуль child_process — это мощный инструмент, который открывает огромные возможности для Node.js приложений. Вот основные рекомендации:

  • Используйте spawn() для долгих процессов и больших данных
  • Выбирайте exec() для простых команд с небольшим выводом
  • Применяйте execFile() когда безопасность критична
  • Используйте fork() для коммуникации между Node.js процессами

Обязательно реализуйте правильную обработку ошибок, таймауты и мониторинг. В production среде рассмотрите использование пулов процессов для оптимизации производительности.

Дочерние процессы идеально подходят для создания робастных серверных приложений, систем автоматизации, обработки медиа-файлов и интеграции с внешними инструментами. Главное — помнить о безопасности и правильно управлять ресурсами системы.


В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked