背景
今天在看Node.js hmr相关的资料时,看到nodemon重启服务的过程有点疑惑,流程是这样的:
1 通过pstree插件获取所有子进程,并关闭所有子进程;
2 关闭主进程;
3 启动服务(优先通过child_process.fork启动,默认child_process.spawn);
非常疑惑,难道关闭主进程之后,所有子进程仍会存在吗?带着这个疑惑,深度整理下进程/子进程/线程(本文章节较多,断断续续记录了很多内容,又花了一天时间整理,建议可以跟着例子做一遍),及应用场景;
注:本人使用macOS,主要是介绍进程在Node.js中的应用
before start
开始之前先说说下涉及到的Node.js相关的接口,另本文进行了大量测试,有一些常用的linux命令需要了解下,方便调试
1 查看端口占用的进程信息; lsof -i:port
2 查看tcp端口占用情况; netstat -anvp tcp
3 查看进程状态; top -pid pid
4 查看子进程; pstree -p pid
5 查看线程; ps -M pid
6 杀死进程; kill -9 pid(通过pid杀死进程)/pkill command(通过进程名称杀死进程, e.g. pkill node,杀死所有node应用)
本文主要是使用Node.js提供的四个api, process, child_process, cluster, worker_threads.
process
process主要提供了以下功能:
1、EventEmitter的实例,可以监听/emit进程的各个阶段事件(beforeExit, exit, onece, warnning, rejectionHandled等);
process.on('exit', (code) => {
console.log(code)
})
// 使用 process.exit 事件杀死进程 或 只提交emit触发监听事件(在处理一些异常的时候,可以通过emit进行触发)
process.exit(1) // 1
process.emit('exit', 'just emit, not exit') // just emit, not exit
2、获取启动参数;e.g.
// 启动
node index.js -x 3 -y 4
// 打印参数,还可以通过一些工具来序列化参数,更加方便使用,e.g. argvs
console.log(argv, argv0) // ['node', 'index.js', '-x', '3', '-y', '4'] 'node'
3、提供进程信息(pid,ppid,platform,etc.);
child_process
1、shell语句/文件执行api, child_process.execFile()/child_process.exec();
2、fork新的子进程, child_process.fork();
3、spawn语句,用新的进程执行shell语句;
4、eventEmitter实例,提供一些进程管理api和进程信息api(subprocess.kill(), subprocess.exitCode(), subprocess.pid, etc.)
这里主要是需要区分,exec(execFile), fork, spawn这三个api的区别,
相同点:
1、三个api都是用来创建新的子进程的;
2、exec(execFile),fork都是基于spawn进行拓展的;
不同点:
// 应用场景不同
1 exec(execFile)是执行一些shell命令或shell脚本文件(execFile是执行shell命令,exec是执行shell脚本文件,这点容易混淆),且不需要和父进程通信;
2 fork()是复制并创建一个新的子进程,通常是在已有进程上进行fork();
3 以上场景不合适或者不能实现需求的话就用spawn;
// 性能/便捷性
1、对于执行shell(性能顺序由高到底),execFile->exec->spawn;对于复制新的子进程, fork()->spawn();
2、spawn是最基本的api,但相对的在具体的场景上,性能/便捷性却是最低的(这个是相对的,如果你的实现可以比node性能更好,请去提pr);
// 传参/通信/回调不同
1、exec(execFile)支持回调函数,并且会将(err, stdout, stderr)传入回调函数;
2、fork(),复制新建子进程,并已构建IPC通信(关于通信在另一篇文章细说);
// 什么时候结束
1 exec(execFile)执行完shell语句/脚本后就exit;
总结,其实就像数组的方法有很多,最基本是是for循环,但我们在具体的场景上应该用性能更高,且性能更高,更语义化的api。
cluster
node提供的集群管理接口,基于eventEmitter,提供了fork, isPrimary, isWorker, workers等方法;官网例子如下:
import cluster from 'cluster';
import http from 'http';
import { cpus } from 'os';
import process from 'process';
const numCPUs = cpus().length;
// 兼容cluster.isMaster
if (cluster.isPrimary || cluster.isMaster) {
console.log(`Primary ${process.pid} is running`);
// Fork workers.
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
// Workers can share any TCP connection
// In this case it is an HTTP server
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000);
console.log(`Worker ${process.pid} started`);
}
看到这里其实一头雾,这不是创建n个进程监听一个端口吗?
原以为操作是在cluster.fork方法中,但找了很久没发现有特殊的地方,后来看了下http.createServer方法,里面区分了isPrimary和isworker的创建方式,具体可以参考从源码分析Node的Cluster模块, 简单来说就是主进程监听了端口,主进程通过ipc通信方式将服务分配到子进程处理新的连接和数据;
worker_threads
worker_threads允许js创建新的线程并行执行任务,主要提供了:
1、获取线程信息的api(isMainThread, parentPort, threadId等);
2、通信类(MessageChannel, MessagePort),提供了线程和进程之间通信的方法(后面通信篇详细说明);
3、Worker类,基于eventEmitter,提供了线程管理的一些方法(线程开启,关闭);e.g.
// 开启新的进程
new Worker(file)
进程
概念: 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
概念很抽象,我认为理解为一个占用一些资源的正在执行的程序即可。在Node.js中,就是通过node执行我们代码的程序,e.g.
import Koa from 'koa'
const app = new Koa()
app.use((ctx, next) => {
ctx.body = 'hello world'
})
app.listen(3002)
我们可以通过端口可以查询到进程的pid,通过pid可以查询到进程运行状态;e.g.
lsof -i:3002
//COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
//node 82410 vb 23u IPv6 ********* 0t0 TCP *:exlm-agent (LISTEN)
top -pid 82410
// PID COMMAND %CPU TIME #TH #WQ #PORTS MEM PURG CMPRS PGRP PPID STATE BOOSTS %CPU_ME %CPU_OTHRS UID FAULTS COW MSGSENT MSGRECV SYSBSD SYSMACH CSW PAGEINS
// 15715 node 0.0 00:02.36 8 0 30 106M 0B 102M 4084 1 sleeping *0[1] 0.00000 0.00000 502 64987 629 106 47 12045 351 2730 0
通过top命令我们可以查看到进程的资源占用情况,这里主要的指标项是内存占用,cpu占用,状态(http服务属于守护进程,只有用户请求进来时才会影响,state默认是sleeping状态)。
进程管理
因为有很多优秀的Node.js进程管理工具(例如pm2, nodemon, forever等),所以我们几乎不需要手动进行进程管理,而这些进程管理工具主要是提供以下功能:
1、提供进程管理的命令行(杀死/启动/重启/热重启);
2、进程守护(监听异常,进行热重启);
3、多进程;
4、负载均衡; 5、日志管理;
除了进程管理外,其他不在这里进行说明。为了对Node.js进程的进一步了解,我们可以尝试手动实现下进程管理相关的代码;
杀死/启动/重启/热重启 进程
一、杀死进程
process.exit(code) // code for listen event
二、启动进程
child_process.fork();
三、重启进程
// 先fork,再exit()
child_process.fork();
process.exit(code) // code for listen event
注:关于执行顺序后面有说明
热重启(滚动发布)
对于单机部署的node服务,热重启一般做法是滚动发布,一个个服务轮流进行重启。需要实现以下功能:
// 1 通知主进程不再进行任务派发(disconnect);
workder.emit('disconnect')
2 等待10s(时间自己定,一般根据设定的连接超时时间来,避免仍在进行的任务被终止);
sleep(10000)
workder.kill()
3 关闭,重启服务;
cluster.fork()
具体可参考源码
线程
线程(英語:thread)是操作系统能夠進行運算调度的最小單位。(取自维基百科) 我自己的理解是进程内任务调度单位,每个进程会根据特定算法进行任务调度执行任务。众所周知,javascrpt是单线程的,将调用的方法按栈的数据结构入栈/出栈进行调用,再加上event loop的异步任务队列组成。但实际上,javascript真的是单线程的吗?
我们还是用一个简单的例子看下:
import Koa from 'koa'
const app = new Koa()
app.use((ctx, next) => {
ctx.body = 'hello world'
})
app.listen(3002)
// 通过端口获取pid, lsof -i:3002
// 通过pid获取线程信息 ps -M pid
USER PID TT %CPU STAT PRI STIME UTIME COMMAND
vb 45954 s012 0.0 S 31T 0:00.03 0:00.11 node index.js
45954 0.0 S 31T 0:00.00 0:00.00
45954 0.0 S 31T 0:00.00 0:00.01
45954 0.0 S 31T 0:00.00 0:00.01
45954 0.0 S 31T 0:00.00 0:00.01
45954 0.0 S 31T 0:00.00 0:00.01
45954 0.0 S 31T 0:00.00 0:00.00
45954 0.0 S 31T 0:00.00 0:00.00
45954 0.0 S 31T 0:00.00 0:00.00
45954 0.0 S 31T 0:00.00 0:00.00
45954 0.0 S 31T 0:00.00 0:00.00
可以看到其实一个Node.js进程开启了n个线程在处理任务,只是我们在实际开发过程中无法调用这些线程。
如果有一些复杂的计算,为了避免请求的阻塞,我们是否可以开启另外一个线程进行运算呢?答案是可以的,Node.js提供了worker_threads api给我们实现,e.g.
// sum.js
const {
Worker, isMainThread, parentPort, workerData
} = require('worker_threads');
if (isMainThread) {
module.exports = function sumAsync(script) {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename, {
workerData: script
});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0)
reject(new Error(`Worker stopped with exit code ${code}`));
});
});
};
} else {
// 假装这是复杂计算
function sum() {
return 1 + 2 + 3 + 4 + 5
}
parentPort.postMessage(sum());
}
// main.js
const Koa = require('Koa')
const app = new Koa()
const sum = require('./sum')
app.use(async (ctx, next) => {
let result = await sum()
ctx.body = `hello world ${result}`
})
app.listen(3002)
但这里有一些问题:
1、线程创建/通信对于开发者来说比较繁琐;
2、每次创建线程花销较大,需要创建线程池保存线程;
3、每次线程消费完后自动销毁线程(困扰了很久,例如在线程内做一个消息监听保持进程不被销毁);e.g.
// 注意是在线程内
parentPort.on('message', (data) => {
console.log(data)
})
所以一般需要通过插件实现,现在比较热门的插件piscina, threads等。
线程池
不管是进程池,线程池,连接池等,其实都是同样的设计,为了避免创建的性能消耗,提前创建多个资源,建立队列,队列在添加的时候触发,不断轮询有效的资源进行调用。
主要流程如下:
1、初始化创建线程池(默认1个线程);
2、任务进来后,封装为Promise, 将resolve,reject作为句柄传入队列(队列不断进行轮询直至所有任务完成);
3、任务完成后将结果通知主进程;
这是一个简单版本的线程池,还有一些问题需要注意:
1、这是一个实例,如果需要在多处使用,建议挂载到全局变量/全局可以访问的对象下,通过单例模式使用;
2、跟new worker()使用不太一样,new worker()接受的是可执行文件的路径,而此线程池接受的是线程需要执行的方法new pool(function);
源码
问题记录
1、杀死父进程是否也会杀死所有子进程?
可能不会,通过fork()/spawn()创建的进程,根据创建的参数detached决定是否会跟随父进程一起被杀(默认false,会跟随父进程一起被杀),如果设置为true,在父进程被杀后,会挂到系统跟节点上,继续执行;
2、关于重启,先fork(),在exit()。如果是同一个端口号,怎么保证执行顺序不会出错(fork的时候,端口仍被占用)?
fork()是异步的,exit是同步执行的,fork的执行时机比exit慢,所以端口不会仍被占用。
3、child_process.fork(), child_process.exec(), worker_threads等仅支持.js/.mjs/.cjs文件不支持.ts文件,如果是typescript环境下怎么处理?
通过ts-node的registry方法来处理,e.g.
import { WorkerOptions, Worker } from 'worker_threads'
const workerTs = (file: string, wkOpts: WorkerOptions) => {
wkOpts.eval = true;
if (!wkOpts.workerData) {
wkOpts.workerData = {};
}
wkOpts.workerData.__filename = file;
return new Worker(`
const wk = require('worker_threads');
require('ts-node').register();
let file = wk.workerData.__filename;
delete wk.workerData.__filename;
require(file);
`,
wkOpts
);
}
参考文档
1 ps command: ss64.com/osx/ps.html
2 Node.js Child Processes: Everything you need to know: www.freecodecamp.org/news/node-j…
3 cluster是怎样开启多进程的,并且一个端口可以被多个 进程监听吗?: juejin.cn/post/691145…
4 从源码分析Node的Cluster模块: juejin.cn/post/684490…
5 A complete guide to threads in Node.js: blog.logrocket.com/a-complete-…
今天的文章Node.js 进程/线程管理分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/17983.html