每个Node.js都是单线程的,这样其实也存在一定的缺点:1. 线程崩溃则程序崩溃;2. 没有充分用起来多核CPU的优势;
Node.js注定不会引入语言级别的多线程,这是不可能的。
但是,Node.js提供了多进程模块,通过开启多个进程来解决上面的两个问题,对应就是其内置的child_process模块。

四种模式

四种模式开启子进程:spawn、fork、exec、execFile,对应的就是child_process的四个方法。逐一看下:

spawn

这个词是产卵的意思….
spawn是使用指定的命令行参数创建新进程,语法格式如下:
child_process.spawn(command[, args][, options])
举个例子:

  1. child_process.spawn('node', ['support.js']);

上面的例子中其实开启子进程的是: $ node support.js,所以我们看到,其实就是开了一个进程执行命令而已,只不过这种模式下,命令是被分解的,比如数组里面的就是命令的参数,比如现在执行命令:$ node support.js dev,我们给support.js传递了参数dev,那么对应的代码就是:child_process.spawn(‘node’, [‘support.js’, ‘dev’]);
(support.js如何拿到这个参数?process.argv[2],参见process.argv文档
主进程中拿到子进程的执行结果是:

  1. const workerProcess = child_process.spawn('node', ['support.js', 'dev']);
  2. workerProcess.stdout.on('data', function (data) {
  3. console.log('stdout: ' + data);
  4. });

exec

child_process.exec 使用子进程执行命令,缓存子进程的输出,并将子进程的输出以回调函数参数的形式返回。
简单点来说,就是比spawn在命令方面爽快:
child_process.exec(command[, options], callback)
exec不用把一个命令拆开,而是一条命令就是一个字符串;另外,在拿到返回数据的时候,是通过传入callback拿到的,具体看代码:

  1. const workerProcess = child_process.exec('node support.js dev', function (error, stdout, stderr) {
  2. if (error) {
  3. // ...
  4. }
  5. console.log('stdout: ' + stdout);
  6. console.log('stderr: ' + stderr);
  7. });

execFile

execFile从命名上像exec,但是我觉得更像spawn,它和spawn的唯一区别就是获得返回值方式不同。
可以这么理解,execFile传递参数和spawn一样,返回值的获得和exec一样。

fork

fork才是最常用的。
child_process.fork(modulePath[, args][, options])

modulePath,望文生义,什么东西才能称之为module?自然是js模块,所以,fork其实可以理解成是特殊的spawn:
就是fork(‘./son.js’) 对应 spawn(‘node’, [‘./son.js’])。
在Linux中,创建进程,本质就是一个复制的过程,当用户调用了fork,操作系统就为这个新的进程分配空间,然后将父进程的数据(内存中的代码,数据等)原样复制,父子进程间只有PID等个别属性不同。
不过,代码中有方式来判断是不是子进程,这就为相同代码下执行不同任务的需求提供了可能性,这是后话。

进程间通信

child_process虽然是小写,但是可以作为类来理解,所以刚才构造进程的方法比如fork,可以理解这就是这个类的类方法,同时,child_process又继承了EventEmitter,那么,其实例就有on和emit的能力了。
不过对应的on,和send方法。
master.js:

  1. const child_process = require('child_process');
  2. const worker = child_process.fork('./worker.js', ['csgWorker']);
  3. worker.on('exit', () => {
  4. console.log('child exist')
  5. });
  6. // worker.send,理解为向worker发送
  7. worker.send('hello son');
  8. // 监听来自worker的消息
  9. worker.on('message', msg => {
  10. console.log('form son:', msg)
  11. })

worker.js:

  1. console.log('I am worker', process.argv[2])
  2. // 在worker中向父进程发送消息用process.send
  3. process.send('hello yoyoyo, father');
  4. // 在worker中监听来自father的消息
  5. process.on('message', msg => {
  6. console.log('form father:', msg)
  7. process.exit();
  8. })

执行以后输出:
I am worker csgWorker
form father: hello son
form son: hello yoyoyo, father
child exist

另外,进程间通信(Inter-process communication, IPC)这个概念,本质是要进程的数据传递到另外一个进程,这就是 IPC ,实现的方式很多,比如:
image.png
在 Node.js 中 IPC 的实现,在 Windows 上通过命名管道,在 Unix 通过 UNIX domain sockets 实现(详见官方文档)。

Cluster

Cluster是Node从0.6版本引入的,可以看作是对child_process做统一管理的类。
Cluster API可以了解细节,但是这里想说一个经典的‘主从模型’。

  1. const cluster = require('cluster');
  2. const numCPUs = require('os').cpus().length;
  3. if (cluster.isMaster) {
  4. // fork工作进程。
  5. for (let i = 0; i < numCPUs; i++) {
  6. cluster.fork();
  7. }
  8. } else {
  9. // do somthing by worker
  10. }

这个主从模型就是借助统一的进程管理cluster提供的isMater方法,鉴别是主线程还是工作线程,前面说过,fork的本质是OS复制一个同样的数据,开辟不同的内存,这数据自然包括代码,但是cluster.isMaster的鉴别能力,就保证了同样的一份代码,不同身份的进程最终执行的结果不同。
然后再看下一个经典的LB的例子(Load Balance, 负载均衡)

  1. const cluster = require('cluster');
  2. const http = require('http');
  3. const numCPUs = require('os').cpus().length;
  4. if (cluster.isMaster) {
  5. console.log(`主进程 ${process.pid} 正在运行`);
  6. for (let i = 0; i < numCPUs; i++) {
  7. cluster.fork();
  8. }
  9. cluster.on('exit', (worker, code, signal) => {
  10. console.log(`工作进程 ${worker.process.pid} 已退出`);
  11. });
  12. } else {
  13. http.createServer((req, res) => {
  14. res.writeHead(200);
  15. res.end('你好世界\n');
  16. }).listen(8000);
  17. console.log(`工作进程 ${process.pid} 已启动`);
  18. }

我们看到,又几个CPU在master中fork了多少个进程,这样充分利用了多CPU的处理能力。不过这里还有知识点,我们仔细审视代码,发现,每一个进程都在监听8000端口!这样不会端口冲突了吗?
这样想没毛病,如果端口冲突了,一般Node会报错了:Error: listen EADDRIUNS。但是,Cluster能让多个Worker监听同一个端口的原因是,这些Cluster集群中的Worker本质都在复用一个TCP端口。有个专业词叫,round-robin:主进程监听端口,接收到新连接之后,通过时间片轮转法来决定将接收到的客户端的 socket 句柄传递给指定的 worker 处理。至于每个连接由哪个 worker 来处理,完全由内置的循环算法决定。
参考:知乎_Node.js cluster 踩坑小结

守护进程管理工具PM2

PM2 is a daemon process manager that will help you manage and keep your application online. Getting started with PM2 is straightforward, it is offered as a simple and intuitive CLI, installable via NPM.

启动Node.js项目怎么启动?直接node命令吗?
别傻了,如果这么做,那就说明没有工程实践经验,面试等着挂吧。
直接通过node app来启动,如果报错了可能直接停在整个运行。目前似乎最常见的线上部署nodejs项目的有、supervisorforever、pm2等,其中pm2是最流行的。
对比下:

  • supervisor是开发环境用。
  • forever管理多个站点,每个站点访问量不大,不需要监控。
  • nodemon 是开发环境使用,修改自动重启。
  • pm2 网站访问量比较大,需要完整的监控界面。
    PM2的主要特性:
  • 内建负载均衡(使用Node cluster 集群模块)
  • 后台运行
  • 0秒停机重载,我理解大概意思是维护升级的时候不需要停机.
    等等…就不列出了。
    安装之后,几个简单的命令就能使用,非常简单;

参考学习:
pm2入门
pm2官网
知乎:egg为什么没用PM2