示例代码托管在:https://www.github.com/dashnowords/blogs
博客园地址:《大史住在大前端》原创博文目录
华为云社区地址:【你要的前端打怪升级指南】
一. 概述
cluster
模块是node.js
中用于实现和管理多进程的模块。常规的node.js
应用程序是单线程单进程的,这也意味着它很难充分利用服务器多核CPU的性能,而cluster
模块就是为了解决这个 问题的,它使得node.js
程序可以以多个实例并存的方式运行在不同的进程中,以求更大地榨取服务器的性能。node.js
在官方示例代码中使用worker
实例来表示主进程fork出的子进程,使得前端开发者在学习过程中非常容易和浏览器环境中的worker
实现的多线程混淆。为了容易区分,我们和node
官方文档使用一致的名称,用集群中的master
和worker
来区分主进程和工作进程,用worker_threads
来描述工作线程。
node.js
的主从模型中,master
主进程相当于一个包工头,主管监听端口,而slave
进程被用于实际的任务执行,当任务请求到达后,它会根据某种方式将连接循环分发给worker
进程来处理。理论上,如果根据当前各个worker
进程的负载状况或者相关信息来挑选工作进程,效率应该比直接循环发放要更高,但node.js
文档中声明这种方式由于受到操作系统调度机制的影响,会使得分发变得不稳定,所以会将"循环法"作为默认的分发策略。
关于cluster
模块的用法和API细节,可以直接参考官方文档《Node.js中文网V10.15.3/cluster》。
二. 线程与进程
想要尽可能利用服务器性能,首先需要了解“线程”(thread)和“进程”(process)这两个概念。
计算机是由CPU来执行计算任务的,如果你只有一个CPU,那么这台机器上所有的任务都将由它来执行。它既可以按照串联执行的原则一个接一个执行任务,也可以依据并联原则同步执行多个任务,多个任务同步执行时,CPU会快速在多个线程之间进行切换,切换线程的同时要切换对应任务的上下文,这就会造成额外的CPU资源消耗,所以当线程数量非常多时,线程切换本身就会浪费大量的CPU资源。如果在执行一个任务的同时,CPU和内存都还有充足的剩余,就可以通过某种方式让它们去执行其他任务。
你可以将“线程”看作是一种轻量级的“进程”。
如果你在操作系统中打开任务管理器,在进程
标签下就可以看到如下图的示例:
我们可以看到每一个程序至少开辟一个新的进程(你可能瞬间就明白了chrome效率高的原因,我什么都没说),它是一种粒度更大的资源隔离单元,进程之间使用不同的内存区域,无法直接共享数据,只能通过跨进程通讯机制来通讯,而且由于要使用新的内存区域,它的创建销毁和切换相对而言都更耗时,它的好处就是进程之间是互相隔离的,互不影响,所以你可以一边听音乐一边玩游戏,而不会因为音乐软件里突然放了一首轻音乐,结果你游戏里的角色攻击力减半了。
再来看一下性能
这个标签:
可以看到线程数是远大于进程数的。“线程”通常用来在单个“进程”中提高CPU的利用率,它是一种粒度更细的资源调度单位,它更容易创建和销毁,在同一个进程内的线程共享分配给这个进程的内存,所以也就实现了共享数据,多线程的编程要更加复杂,由于共享数据,如果线程之间传递指针然后操作同一数据源,就必须考虑“原子操作”和“锁”的问题,否则很容易就乱套了,如果传递数据的拷贝,又会造成内存浪费,另外线程异常不会被隔离,而会导致整个进程异常。
线程和进程的相关知识涉及到底层操作系统的内容,笔者涉猎有限,先分享这么多(会的都告诉你了,还要我怎样)。
三. cluster模块源码解析
源码中个别方法比较长,建议使用带有代码折叠的工具来看。
3.1 起步
cluster
模块的用法看起来并不复杂,官方给出的示例是这样的:
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);
// 衍生工作进程。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
});
} else {
// 工作进程可以共享任何 TCP 连接。
// 在本例子中,共享的是 HTTP 服务器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);
console.log(`工作进程 ${process.pid} 已启动`);
}
3.2 入口
cluster
模块的入口在/lib/cluster.js
,这里的代码很简单:
'use strict';
const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';
module.exports = require(`internal/cluster/${childOrMaster}`);
可以看到,如果进程对象的环境变量中有NODE_UNIQUE_ID
这个变量,就透传internal/cluster/child.js
模块的输出,否则就透传internal/cluster/master.js
模块的输出。这是node
的主进程在进行子进程管理时的标识,后面的代码中可以看到当调用cluster.fork( )
生成一个子进程时会以一个自增ID的形式生成这个环境变量。
3.3 主进程模块master.js
首先运行node
程序的肯定是主线程,那么我们从master.js
这个模块开始,先用工具折叠一下代码浏览一下:
可以看到除了模块属性外,cluster模块对外暴露的方法只有下面3个,其他的都是用来完成内部功能的:
setupMaster(options )
-修改fork
时默认设置fork( )
-生成子进程disconnect( )
- 断开和所有子进程的连接
我们按照官方示例的逻辑路线来阅读代码cluster.fork( )
方法定义在161-217行,一样是用折叠工具来看全貌:
可以看到cluster.fork( )
执行时做了如下几件事情:
1.设置主线程参数
2.传入一个自增参数id(就是前文提到的NODE_UNIQUE_ID)和环境信息env来生成一个worker线程的process对象
3.将id和新的process对象传入Worker构造器生成新的worker进程实例
4