JS 是单线程执行的,但是我们可以启动多个进程来执行,nodejs 中子进程管理以及进程守候是非常重要的知识点。
- 线程 vs 进程
- 为何要启用多进程
- child_process
- cluster
进程(Process) 是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。 线程(Thread) 是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己不拥有系统资源,它与同属一个进程的其他的线程共享进程所拥有的全部资源。
在 mac os 或者 linux 系统中运行top
命令,可以看到如下列表,这些就是进程。windows 系统中的任务管理器,可以看到各个启动的软件的列表,也是进程。通过这些列表,我们都能看到每个进程 CPU 使用率,内存占用,符合上文对于进程的描述(独立分配、调度资源)
PID COMMAND %CPU TIME #TH #WQ #PORT MEM PURG CMPRS PGRP PPID STATE BOOSTS
12080 top 9.1 00:00.62 1/1 0 23 3672K 0B 0B 12080 12059 running *0[1]
12059 zsh 0.0 00:00.16 1 0 19 3368K 0B 0B 12059 12058 sleeping *0[1]
12058 login 0.0 00:00.04 2 1 30 1904K 0B 0B 12058 12057 sleeping *0[9]
12057 iTerm2 0.0 00:00.03 2 1 30 2364K 0B 0B 12057 945 sleeping *0[1]
12055 lsof 0.0 00:00.00 1 0 8 232K 0B 0B 12054 12054 sleeping *0[1]
12054 lsof 0.0 00:00.84 1 0 19 6004K 0B 0B 12054 70 sleeping *0[1]
12040 QuickLookSat 0.0 00:00.40 5 1 92 10M 232K 0B 12040 1 sleeping 0[12]
12012 quicklookd 0.0 00:00.61 4 1 90 4716K 216K 0B 12012 1 sleeping 0[14]
线程是进程中更小的单位,我们无法通过工具直观的看到。一个进程至少启动一个线程,或者启动若干个线程(多线程)。JS 是单线程运行的,我们无法通过 JS 代码新启动一个线程(java 就可以),但是可以新启动一个进程。
注意,新启动一个进程是比较耗费资源的,不应频繁启动。如果遇到需要频繁启动新进程的需求,应该考虑其他的解决方案(我曾经就遇到过,差点入坑)。
第一,现在的服务器都是多核 CPU ,启动多进程可以有效提高 CPU 利用率,否则 CPU 资源就白白浪费了。一般会根据 CPU 的核数,启动数量相同的进程数。
PS:和开发客户端程序不同,开发 server 端程序时,要时刻注意“节省”和“压榨”,通俗一点就是“抠门”。“节省”就是尽量减少计算次数(时间复杂度)、内存使用(空间复杂度);“压榨”就是尽量多的合理利用起现有的资源,CPU、内存和硬盘等。有时你在开发和测试时候,对于“节省”和“压榨”看不出效果,但是一旦上线访问量增大,效果将会越来越明显。
第二,受到 v8 引擎的垃圾回收算法的限制,nodejs 能使用的系统内存是受限制的(64 位最多使用 1.4GB ,32 位最多使用 0.7GB)。如何突破这种限制呢?—— 多进程。因为每个进程都是一个新的 v8 实例,都有权利重新分配、调度资源。
child_process 提供了创建子进程的方法
spawn
exec
execFile
fork
var cp = require('child_process')
cp.spawn('node', ['worker.js'])
cp.exec('node worker.js', function (err, stdout, stderr) {
// todo
})
cp.execFile('worker.js', function (err, stdout, stderr) {
// todo
})
cp.fork('./worker.js')
进程之间的通讯,代码如下。跟前端WebWorker
类似,使用on
监听(此前讲过的自定义事件),使用send
发送。
// parent.js
var cp = require('child_process')
var n = cp.for('./sub.js')
n.on('message', function (m) {
console.log('PARENT got message: ' + m)
})
n.send({hello: 'workd'})
// sub.js
process.on('message', function (m) {
console.log('CHILD got message: ' + m)
})
process.send({foo: 'bar'})
cluster 模块允许设立一个主进程和若干个 worker 进程,由主进程监控和协调 worker 进程的运行。worker 之间采用进程间通信交换消息,cluster模块内置一个负载均衡器,采用 Round-robin 算法协调各个 worker 进程之间的负载。运行时,所有新建立的链接都由主进程完成,然后主进程再把 TCP 连接分配给指定的 worker 进程。
const cluster = require('cluster')
const os = require('os')
const http = require('http')
if (cluster.isMaster) {
console.log('是主进程')
const cpus = os.cpus() // cpu 信息
const cpusLength = cpus.length // cpu 核数
for (let i = 0; i < cpusLength; i++) {
// fork() 方法用于新建一个 worker 进程,上下文都复制主进程。只有主进程才能调用这个方法
// 该方法返回一个 worker 对象。
cluster.fork()
}
} else {
console.log('不是主进程')
// 运行该 demo 之后,可以运行 top 命令看下 node 的进程数量
// 如果电脑是 4 核 CPU ,会生成 4 个子进程,另外还有 1 个主进程,一共 5 个 node 进程
// 其中, 4 个子进程受理 http-server
http.createServer((req, res) => {
res.writeHead(200)
res.end('hello world')
}).listen(8000) // 注意,这里就不会有端口冲突的问题了!!!
}
维护进程健壮性,通过 Cluster 能监听到进程退出,然后自动重启,即自动容错,这就是进程守候。
if (cluster.isMaster) {
const num = os.cpus().length
console.log('Master cluster setting up ' + num + ' workers...')
for (let i = 0; i < num; i++) {
// 按照 CPU 核数,创建 N 个子进程
cluster.fork()
}
cluster.on('online', worker => {
// 监听 workder 进程上线(启动)
console.log('worker ' + worker.process.pid + ' is online')
})
cluster.on('exit', (worker, code, signal) => {
// 兼容 workder 进程退出
console.log('worker ' + worker.process.pid + ' exited with code: ' + code + ' and signal: ' + signal)
// 退出一个,即可立即重启一个
console.log('starting a new workder')
cluster.fork()
})
}
示例看似简单,但是实际应用还是尽量使用成熟的工具,例如 pm2,可以自己去看文档使用。
总要明白线程和进程的区别、联系,以及为何使用多进程,后面的 API 用法相对比较简单。