Node.js 子进程与应用场景

Node.js 子进程与应用场景由于 ons(阿里云 RocketMQ 包)基于 C艹 封装而来,不支持单一进程内实例化多个生产者与消费者,为了解决这一问题,使用了 Node.js 子进程。 close 与 exit 是有区别的,close 是在数据流关闭时触发的事件,exit 是在子进程退出时触发的事件。因…

背景

由于 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 启动项目

  1. 进程 ①:用来解析用户输入,调用启动命令
    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
            });
        });
    
  2. 进程 ①:
    1. 利用 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();
      
    2. 记录子进程 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
                  );
              }
          });
      
  3. 进程 ②:如果进程 ② 中还需要启动自己的子进程,在启动子进程后,监听自己的退出事件,并主动关闭子进程,防止子进程变为操作系统进程而不受控
    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 支撑起业务场景。对于子进程的采坑与使用在本文中记录,以供未来的自己参考。

今天的文章Node.js 子进程与应用场景分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/15502.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注