咱们启动一个服务、运行一个实例,就是开一个服务过程,Node.js 里通过 node app.js
开启一个服务过程,多过程就是过程的复制(fork),fork 进去的每个过程都领有本人的独立空间地址、数据栈,一个过程无法访问另外一个过程里定义的变量、数据结构,只有建设了 IPC 通信,过程之间才可数据共享。
child_process
node.js
中能够通过上面四种形式创立子过程:
- child_process.spawn(command, args)
- child_process.exec(command, options)
- child_process.execFile(file, args[, callback])
- child_process.fork(modulePath, args)
spawn
const {spawn} = require("child_process");// 创立 文件spawn("touch",["index.js"]);
spawn()
会返回child-process
子过程实例:
const {spawn} = require("child_process");// cwd 指定子过程的工作目录,默认当前目录const child = spawn("ls",["-l"],{cwd:__dirname});// 输入过程信息child.stdout.pipe(process.stdout);console.log(process.pid,child.pid);
子过程同样基于事件机制(EventEmitter API),提供了一些事件:
exit
:子过程退出时触发,能够得悉过程退出状态(code和signal)disconnect
:父过程调用child.disconnect()时触发error
:子过程创立失败,或被kill时触发close
:子过程的stdio流(规范输入输出流)敞开时触发message
:子过程通过process.send()发送音讯时触发,父子过程音讯通信
close与exit的区别次要体现在多过程共享同一stdio流的场景,某个过程退出了并不意味着stdio流被敞开了
子过程具备可读流的个性,利用可读流实现find . -type f | wc -l
,递归统计当前目录文件数量:
const { spawn } = require('child_process');const find = spawn('find', ['.', '-type', 'f']);const wc = spawn('wc', ['-l']);find.stdout.pipe(wc.stdin);wc.stdout.on('data', (data) => { console.log(`Number of files ${data}`);});
exec
spawn()
跟exec()
办法的区别在于,exec()
不是基于stream的,exec()
会将传入命令的执行后果暂存到buffer中,再整个传递给回调函数。
spawn()
默认不会创立shell去执行命令(性能上会稍好),而exec()
办法执行是会先创立shell,所以能够在exec()
办法中传入任意shell脚本。
const {exec} = require("child_process");exec("node -v",(error,stdout,stderr)=>{ if (error) console.log(error); console.log(stdout)})
exec()
办法因为能够传入任意shell脚本所以存在平安危险。
spawn()
办法默认不会创立shell去执行传入的命令(所以性能上略微好一点),不过能够通过参数实现:
const { spawn } = require('child_process');const child = spawn('node -v', { shell: true});child.stdout.pipe(process.stdout);
这种做法的益处是,既能反对shell语法,也能通过stream IO
进行规范输入输出。
execFile
const {execFile} = require("child_process");execFile("node",["-v"],(error,stdout,stderr)=>{ console.log({ error, stdout, stderr }) console.log(stdout)})
通过可执行文件门路执行:
const {execFile} = require("child_process");execFile("/Users/.nvm/versions/node/v12.1.0/bin/node",["-v"],(error,stdout,stderr)=>{ console.log({ error, stdout, stderr }) console.log(stdout)})
fork
fork()
办法能够用来创立Node过程,并且父子过程能够相互通信
//master.jsconst {fork} = require("child_process");const worker = fork("worker.js");worker.on("message",(msg)=>{ console.log(`from worder:${msg}`)});worker.send("this is master");// worker.jsprocess.on("message",(msg)=>{ console.log("worker",msg)});process.send("this is worker");
利用fork()
能够用来解决计算量大,耗时长的工作:
const longComputation = () => { let sum = 0; for (let i = 0; i < 1e10; i++) { sum += i; }; return sum;};
将longComputation
办法拆分到子过程中,这样主过程的事件循环不会被耗时计算阻塞:
const http = require('http');const { fork } = require('child_process');const server = http.createServer();server.on('request', (req, res) => { if (req.url === '/compute') { // 将计算量大的工作,拆分到子过程中解决 const compute = fork('compute.js'); compute.send('start'); compute.on('message', sum => { // 收到子过程工作后,返回 res.end(`Sum is ${sum}`); }); } else { res.end('Ok') }});server.listen(3000);
过程间通信IPC
每个过程各自有不同的用户地址空间,任何一个过程的全局变量在另一个过程中都看不到,所以过程之间要替换数据必须通过内核,在内核中开拓一块缓冲区,过程1把数据从用户空间拷到内核缓冲区,过程2再从内核缓冲区把数据读走,内核提供的这种机制称为过程间通信(IPC,InterProcess Communication)
过程之间能够借助内置的IPC机制通信
父过程:
- 接管事件
process.on('message')
- 发送信息给子过程
master.send()
子过程:
- 接管事件
process.on('message')
- 发送信息给父过程
process.send()
fork 多过程
nodejs中的多过程是 多过程 + 单线程
的模式
// master.js. process.title = 'node-master'const net = require("net");const {fork} = require("child_process");const handle = net._createServerHandle("127.0.0.1",3000);for(let i=0;i<4;i++){ fork("./worker.js").send({},handle);}// worker.jsprocess.title = 'worker-master';const net = require("net");process.on("message",(msg,handle)=>start(handle));const buf = "hello nodejs";const res= ["HTTP/1.1 200 ok","content-length:"+buf.length].join("\r\n")+"\r\n\r\n"+buf;function start(server){ server.listen(); let num=0; server.onconnection = function(err,handle){ num++; console.log(`worker ${process.pid} num ${num}`); let socket = new net.Socket({handle}); socket.readable = socket.writable = true socket.end(res); }}
运行node master.js,这里能够应用测试工具 Siege :
siege -c 20 -r 10 http://localhost:3000
-c 并发量,并发数为20人 -r 是反复次数, 反复10次
这种创立过程的特点是:
- 在一个服务上同时启动多个过程
- 每个过程运行同样的代码(start办法)
- 多个过程能够同时监听一个端口(3000)
不过每次申请过去交给哪个worker
解决,master
并不分明,咱们更心愿master
可能掌控全局,将申请指定给worker
,咱们做上面的革新:
//master.jsprocess.title = 'node-master'const net =require("net");const {fork} = require("child_process");// 定义workers变量,保留子过程workerlet workers = [];for(let i=0;i<4;i++){ workers.push(fork("./worker.js"));}const handle = net._createServerHandle("0.0.0.0", 3000)handle.listen();// master管制申请handle.onconnection = function(err,handle){ let worker = workers.pop(); // 将申请传递给子过程 worker.send({},handle); workers.unshift(worker);}// worker.jsprocess.title = 'worker-master';const net = require("net")process.on("message", (msg, handle) => start(handle))const buf = "hello nodejs"const res = ["HTTP/1.1 200 ok", "content-length:" + buf.length].join("\r\n") + "\r\n\r\n" + buffunction start(handle) { console.log(`get a connection on worker,pid = %d`, process.pid) let socket = new net.Socket({ handle }) socket.readable = socket.writable = true socket.end(res)}
Cluster 多过程
Node.js 官网提供的Cluster
模块不仅充分利用机器 CPU 内核开箱即用的解决方案,还有助于 Node 过程减少可用性的能力,Cluster
模块是对多过程服务能力的封装。
// master.jsconst cluster = require("cluster");const numCPUS = require("os").cpus().length;if(cluster.isMaster){ console.log(`master start...`) for(let i=0;i<numCPUS;i++){ cluster.fork(); }; cluster.on("listening",(worker,address)=>{ console.log(`master listing worker pid ${worker.process.pid} address port:${address.port}`) })}else if(cluster.isWorker){ require("./wroker.js")}
//wroker.jsconst http = require("http");http.createServer((req,res)=>res.end(`hello`)).listen(3000)
过程重启和守护
过程重启
为了减少服务器的可用性,咱们心愿实例在呈现解体或者异样退出时,可能主动重启。
//master.jsconst cluster = require("cluster")const numCPUS = require("os").cpus().lengthif (cluster.isMaster) { console.log("master start..") for (let i = 0; i < numCPUS; i++) { cluster.fork() } cluster.on("listening", (worker, address) => { console.log("listening worker pid " + worker.process.pid) }) cluster.on("exit", (worker, code, signal) => { // 子过程出现异常或者奔败退出 if (code !== 0 && !worker.exitedAfterDisconnect) { console.log(`工作过程 ${worker.id} 解体了,正在开始一个新的工作过程`) // 从新开启子过程 cluster.fork() } })} else if (cluster.isWorker) { require("./server")}
const http = require("http")const server = http.createServer((req, res) => { // 随机触发谬误 if (Math.random() > 0.5) { throw new Error(`worker error pid=${process.pid}`) } res.end(`worker pid:${process.pid} num:${num}`)}).listen(3000)
如果申请抛出异样而完结子过程,主过程可能监听到完结事件,重启开启子过程。
下面的重启只是简略解决,真正我的项目中要思考到的就很多了,这里能够参考egg的多过程模型和过程间通信。
上面是来自文章Node.js进阶之过程与线程更全面的例子:
// master.jsconst {fork} = require("child_process");const numCPUS = require("os").cpus().length;const server = require("net").createServer();server.listen(3000);process.title="node-master";const workers = {};const createWorker = ()=>{ const worker = fork("worker.js"); worker.on("message",message=>{ if(message.act==="suicide"){ createWorker(); } }) worker.on("exit",(code,signal)=>{ console.log('worker process exited,code %s signal:%s',code,signal); delete workers[worker.pid]; }); worker.send("server",server); workers[worker.pid] = worker; console.log("worker process created,pid %s ppid:%s", worker.pid, process.ppid)}for (let i = 0; i < numCPUS; i++) { createWorker()}process.once("SIGINT",close.bind(this,"SIGINT")); // kill(2) Ctrl+Cprocess.once("SIGQUIT", close.bind(this, "SIGQUIT")) // kill(3) Ctrl+lprocess.once("SIGTERM", close.bind(this, "SIGTERM")) // kill(15) defaultprocess.once("exit", close.bind(this))function close(code){ console.log('process exit',code); if(code!=0){ for(let pid in workers){ console.log('master process exit,kill worker pid:',pid); workers[pid].kill("SIGINT"); } }; process.exit(0);}
//worker.jsconst http=require("http");const server = http.createServer((req,res)=>{ res.writeHead(200,{"Content-Type":"text/plain"}); res.end(`worker pid:${process.pid},ppid:${process.ppid}`) throw new Error("worker process exception!");});let worker;process.title = "node-worker";process.on("message",(message,handle)=>{ if(message==="server"){ worker = handle; worker.on("connection",socket=>{ server.emit("connection",socket) }) }})process.on("uncaughtException",(error)=>{ console.log('some error') process.send({act:"suicide"}); worker.close(()=>{ console.log(process.pid+" close") process.exit(1); })})
这个例子思考更加周到,通过uncaughtException
捕捉子过程异样后,发送信息给主过程重启,并在链接敞开后退出。
过程守护
pm2能够使服务在后盾运行不受终端的影响,这里次要通过两步解决:
options.detached
:为true
时运行子过程在父过程退出后持续运行unref()
办法能够断绝跟父过程的关系,使父过程退出后子过程不会跟着退出
const { spawn } = require("child_process")function startDaemon() { const daemon = spawn("node", ["daemon.js"], { // 当前工作目录 cwd: __dirname, // 作为独立过程存在 detached: true, // 漠视输入输出流 stdio: "ignore", }) console.log(`守护过程 ppid:%s pid:%s`, process.pid, daemon.pid) // 断绝父子过程关系 daemon.unref()}startDaemon()
// daemon.jsconst fs = require("fs")const {Console} = require("console");// 输入日志const logger = new Console(fs.createWriteStream("./stdout.log"),fs.createWriteStream("./stderr.log"));// 放弃过程始终在后盾运行setInterval(()=>{ logger.log("daemon pid:",process.pid,"ppid:",process.ppid)},1000*10);// 生成敞开文件fs.writeFileSync("./stop.js", `process.kill(${process.pid}, "SIGTERM")`)
参考链接
- node.js应用cluster实现多过程
- Nodejs进阶:如何玩转子过程(child_process)
- Node.js进阶之过程与线程
- Nodejs过程间通信
- Node.js 集群(cluster):扩大你的 Node.js 利用
- eggjs-多过程模型和过程间通信