背景
由于 ons (阿里云 RocketMQ 包)基于 C 艹 封装而来,不支持单一进程内实例化多个生产者与消费者,为了解决这一问题,使用了 Node.js 子进程.
在使用的过程中碰到的坑
发布:进程管理关闭主进程后,子进程变为操作系统进程(pid 为 1)
几种解决方案
将子进程看做独立运行的进程,记录 pid,发布时进程管理关闭主进程同时关闭子进程
主进程监听关闭事件,主动关闭从属于自己的子进程
子进程种类
spawn:执行命令
exec:执行命令(新建 shell)
execFile:执行文件
fork:执行文件
子进程常用事件
exit
close
error
message
close 与 exit 是有区别的,close 是在数据流关闭时触发的事件,exit 是在子进程退出时触发的事件.因为多个子进程可以共享同一个数据流,所以当某个子进程 exit 时不一定会触发 close 事件,因为这个时候还存在其他子进程在使用数据流.
子进程数据流
stdin
stdout
stderr
因为是以主进程为出发点,所以子进程的数据流与常规理解的数据流方向相反,stdin:写入流,stdout,stderr:读取流.
spawn
spawn(command[, args][, options])
执行一条命令,通过 data 数据流返回各种执行结果.
基础使用
const { spawn } = require('child_process');
const child = spawn('find', [ '.', '-type', 'f' ]);
child.stdout.on('data', (data) => {
console.log(`child stdout:\n${data}`);
});
child.stderr.on('data', (data) => {
console.error(`child stderr:\n${data}`);
});
child.on('exit', (code, signal) => {
console.log(`child process exit with: code ${code}, signal: ${signal}`);
});
常用参数
{
cwd: String,
env: Object,
stdio: Array | String,
detached: Boolean,
shell: Boolean,
uid: Number,
gid: Number
}
重点说明下 detached 属性,detached 设置为 true 是为子进程独立运行做准备.子进程的具体行为与操作系统相关,不同系统表现不同,Windows 系统子进程会拥有自己的控制台窗口,POSIX 系统子进程会成为新进程组与会话负责人.
这个时候子进程还没有完全独立,子进程的运行结果会展示在主进程设置的数据流上,并且主进程退出会影响子进程运行.当 stdio 设置为 ignore 并调用 child.unref(); 子进程开始真正独立运行,主进程可独立退出.
exec
exec(command[, options][, callback])
执行一条命令,通过回调参数返回结果,指令未执行完时会缓存部分结果到系统内存.
const { exec } = require('child_process');
exec('find . -type f | wc -l', (err, stdout, stderr) => {
if (err) {
console.error(`exec error: ${err}`);
return;
}
console.log(`Number of files ${stdout}`);
});
两全其美 -- spawn 代替 exec
由于 exec 的结果是一次性返回,在返回前是缓存在内存中的,所以在执行的 shell 命令输出过大时,使用 exec 执行命令的方式就无法按期望完成我们的工作,这个时候可以使用 spawn 代替 exec 执行 shell 命令.
const { spawn } = require('child_process');
const child = spawn('find . -type f | wc -l', {
stdio: 'inherit',
shell: true
});
child.stdout.on('data', (data) => {
console.log(`child stdout:\n${data}`);
});
child.stderr.on('data', (data) => {
console.error(`child stderr:\n${data}`);
});
child.on('exit', (code, signal) => {
console.log(`child process exit with: code ${code}, signal: ${signal}`);
});
execFile
child_process.execFile(file[, args][, options][, callback])
执行一个文件
与 exec 功能基本相同,不同之处在于执行给定路径的一个脚本文件,并且是直接创建一个新的进程,而不是创建一个 shell 环境再去运行脚本,相对更轻量级更高效.但是在 Windows 系统中如 .cmd,.bat 等文件无法直接运行,这是 execFile 就无法工作,可以使用 spawn,exec 代替.
fork
child_process.fork(modulePath[, args][, options])
执行一个 Node.js 文件
// parent.js
const { fork } = require('child_process');
const child = fork('child.js');
child.on('message', (msg) => {
console.log('Message from child', msg);
});
child.send({ hello: 'world' });
// child.js
process.on('message', (msg) => {
console.log('Message from parent:', msg);
});
let counter = 0;
setInterval(() => {
process.send({ counter: counter++ });
}, 3000);
fork 实际是 spawn 的一种特殊形式,固定 spawn Node.js 进程,并且在主子进程间建立了通信通道,让主子进程可以使用 process 模块基于事件进行通信.
子进程使用场景
计算密集型系统
前端构建工具利用多核 CPU 并行计算,提升构建效率
进程管理工具,如:PM2 中部分功能
实践:Akyuu PM 启动项目
进程 ①:用来解析用户输入,调用启动命令
const commander = require('commander');
const path = require('path');
const start = require('./lib/start');
commander
.command('start <entry>')
.description('start process')
.option('--dev', 'set dev environment')
.action(function(entry, options) {
start({
entry: path.resolve(__dirname, `../entry/${entry}`),
isDaemon: !options.dev
});
});
进程 ①:
利用 spawn 启动指定入口文件进程 ②, 设置 detached 为 true,调用 child.unref(); 使子进程独立运行
const { spawn } = require('child_process');
const child = spawn(process.execPath, [ path.resolve(__dirname, 'cluster') ], {
cwd: path.resolve(__dirname, '../../'),
env: Object.assign({}, process.env, {
izayoiCoffee: JSON.stringify({
configDir: config.akyuuConfigDir,
entry: options.entry
})
}),
detached: true,
stdio: 'ignore'
});
child.on('exit', function(code, signal) {
console.error(`start process \`${path.basename(options.entry)}\` failed, ` +
`code: ${code}, signal: ${signal}`);
process.exit(1);
});
child.unref();
记录子进程 pid 到文件
child
.on('fork', function(worker) {
try {
fs.writeFileSync(
'pid file path',
worker.process.pid,
{ encoding: 'utf8' }
);
} catch(err) {
console.error(
'[%s] [uncaughtException] [master: %d] \n%s',
moment().utcOffset(8).format('YYYY-MM-DD HH:mm:ss.SSS'),
process.pid,
err.stack
);
}
})
.on('exit', function(worker, code, signal) {
try {
fs.unlinkSync('pid file path');
} catch(err) {
console.error(
'[%s] [uncaughtException] [master: %d] \n%s',
moment().utcOffset(8).format('YYYY-MM-DD HH:mm:ss.SSS'),
process.pid,
err.stack
);
}
});
进程 ②:如果进程 ② 中还需要启动自己的子进程,在启动子进程后,监听自己的退出事件,并主动关闭子进程,防止子进程变为操作系统进程而不受控
const { fork } = require('child_process);
const child = fork('some child process file');
// 程序停止信号
process.on('SIGHUP', function() {
child.kill('SIGHUP');
process.exit(0);
});
// kill 默认参数信号
process.on('SIGTERM', function() {
child.kill('SIGHUP');
process.exit(0);
});
// Ctrl + c 信号
process.on('SIGINT', function() {
child.kill('SIGHUP');
process.exit(0);
});
// 退出事件
process.on('exit', function() {
child.kill('SIGHUP');
process.exit(0);
});
// 未捕获异常
process.on('uncaughtException', function() {
child.kill('SIGHUP');
process.exit(0);
});
总结
在使用 Node.js 做开发中,尤其是 API 开发过程中很少涉及到子进程,但是子进程还是比较重要的一个组成部分.Node.js 可以利用子进程做些计算密集型任务,虽然没有 C 艹 等其他语言高效,方便,但是也不失为一种方案,在没有掌握其他语言时可以用 Node.js 支撑起业务场景.对于子进程的采坑与使用在本文中记录,以供未来的自己参考.
来源: https://juejin.im/post/5a67d33651882535a5545f8d