- Home »

Как запускать дочерние процессы в 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 среде рассмотрите использование пулов процессов для оптимизации производительности.
Дочерние процессы идеально подходят для создания робастных серверных приложений, систем автоматизации, обработки медиа-файлов и интеграции с внешними инструментами. Главное — помнить о безопасности и правильно управлять ресурсами системы.
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.