当前位置: 首页 > 后端技术 > Node.js

nodejs篇-进程与集群cluster

时间:2023-04-03 19:12:22 Node.js

我们启动一个服务,运行一个实例,也就是开启一个服务进程。在Node.js中,通过nodeapp.js启动一个服务进程。多进程就是进程的复制(fork),fork出来的每个进程都有自己独立的空间地址和数据栈。一个进程不能访问另一个进程中定义的变量和数据结构。只有建立了IPC通信,进程之间才能共享数据。child_processnode.js可以通过以下四种方式创建子进程:{spawn}=require("child_process");//创建文件spawn("touch",["index.js"]);spawn()会返回child-process子进程实例:const{spawn}=require("child_process");//cwd指定子进程的工作目录,默认当前目录constchild=spawn("ls",["-l"],{cwd:__dirname});//输出进程信息child.stdout.pipe(process.stdout);console.log(process.pid,child.pid);子进程也是基于事件机制(EventEmitterAPI),提供了一些事件:exit:子进程退出时触发,可以知道进程退出状态(代码和信号)disconnect:父进程调用child时触发。disconnect()错误:子进程创建失败,或者被杀死时触发close:子进程的stdio流(标准输入输出流)关闭时触发发送消息时触发。父子进程消息通信close和exit的区别主要体现在多个??进程共享同一个stdio流的场景。退出进程并不意味着stdio流关闭,子进程具有可读性。流的特性,使用可读流来实现find。-类型f|wc-l,递归统计当前目录的文件数:const{spawn}=require('child_process');constfind=spawn('find',['.','-type','f']);constwc=spawn('wc',['-l']);查找.stdout.pipe(wc.stdin);wc.stdout.on('data',(data)=>{console.log(`文件数${data}`);});execspawn()和exec()的区别在于,exec()不是基于流的,exec()会将传入命令的执行结果暂时存放在缓冲区中,然后整体传递给回调函数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');constchild=spawn('node-v',{shell:true});child.stdout.pipe(process.stdout);这种方式的好处是不仅可以支持shell语法,还可以通过流IO进行标准输入输出。execFileconst{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");constworker=fork("worker.js");worker.on("message",(msg)=>{console.log(`fromworder:${msg}`)});worker.send("thisismaster");//worker.jsprocess.on("message",(msg)=>{console.log("worker",msg)});process.send("thisisworker");使用fork()可用于处理需要大量计算且耗时较长的任务:constlongComputation=()=>{letsum=0;for(leti=0;i<1e10;i++){sum+=i;};返回总和;};将longComputation方法拆分成子进程,这样主进程的事件循环就不会被耗时计算阻塞:consthttp=require('http');const{fork}=require('child_process');constserver=http.createServer();server.on('request',(req,res)=>{if(req.url==='/compute'){//会计算一个大的Task,拆分成子进程处理constcompute=fork('compute.js');compute.send('start');compute.on('message',sum=>{//接收子进程任务后,returnres.end(`Sumis${sum}`);});}else{res.end('Ok')}});server.listen(3000);进程间通信IPC每个进程都有自己不同的用户地址空间,任何一个进程的全局变量在另一个进程中是看不到的,所以进程间的数据交换必须经过内核,在内核中开辟一个缓冲区,进程1将数据从用户空间复制到内核缓冲区,进程2再次从内核缓冲区读取数据,内核提供的这种机制称为进程间通信(IPC,InterProcessCommunication)。进程可以使用内置的IPC机制与父进程通信:接收事件进程。on('message')向子进程发送信息master.send()子进程:收到一个事件process.on('message')向父进程发送信息process.send()forkmulti-processmulti-processinnodejs是多进程+单线程模式//master.js.process.title='node-master'constnet=require("net");const{fork}=require("child_process");consthandle=net._createServerHandle("127.0.0.1",3000);for(leti=0;i<4;i++){fork("./worker.js").send({},handle);}//工人。jsprocess.title='worker-master';const网络=要求ire("net");process.on("message",(msg,handle)=>start(handle));constbuf="hellonodejs";constres=["HTTP/1.1200ok","内容-length:"+buf.length].join("\r\n")+"\r\n\r\n"+buf;functionstart(server){server.listen();让数=0;server.onconnection=function(err,handle){num++;console.log(`worker${process.pid}num${num}`);让socket=newnet.Socket({handle});socket.readable=socket.writable=truesocket.end(res);}}运行nodemaster.js,这里可以使用测试工具siege:siege-c20-r10http://localhost:3000-c并发数,并发数为20人-r为重复次数,重复10次。这个创建进程的特点是:在一个服务上同时启动多个进程,每个进程运行相同的代码(start方法)。多个进程可以同时监听一个端口(3000)。master不知道每个请求交给哪个worker。我们希望master能掌控全局,把请求分配给worker。我们做如下转换://master.jsprocess.title='node-master'constnet=require("net");const{fork}=require("child_process");//定义workers变量来保存子进程workerletworkers=[];for(leti=0;i<4;i++){workers.push(fork("./worker.js"));}consthandle=net._createServerHandle("0.0.0.0",3000)handle.listen();//主控请求handle.onconnection=function(err,handle){letworker=workers.pop();//将请求传递给子进程worker.send({},handle);workers.unshift(worker);}//worker.jsprocess.title='worker-master';constnet=require("net")process.on("message",(msg,handle)=>start(handle))constbuf="hellonodejs"constres=["HTTP/1.1200ok","content-length:"+buf.length].join("\r\n")+"\r\n\r\n"+buffunctionstart(handle){console.log(`getaconnectiononworker,pid=%d`,process.pid)letsocket=newnet.Socket({handle})socket.readable=socket.writable=truesocket.end(res)}Cluster多进程Node.js官方提供的Cluster模块不仅使得充分利用机器CPU核心开箱即用的方案,也有助于增加Node进程能力的可用性,Cluster模块是对多进程服务能力的封装//master.jsconstcluster=require("cluster");constnumCPUS=require("os").cpus().length;if(cluster.isMaster){console.log(`masterstart...`)for(leti=0;i{console.log(`masterlistingworkerpid${worker.process.pid}addressport:${address.port}`)})}elseif(cluster.isWorker){require("./wroker.js")}//wroker.jsconsthttp=require("http");http.createServer((req,res)=>res.end(`hello`).listen(3000)重启进程并重启守护进程为了增加服务器的可用性,我们希望当实例崩溃或异常退出时,能够自动重启。//master.jsconstcluster=require("cluster")constnumCPUS=require("os").cpus().lengthif(cluster.isMaster){console.log("masterstart..")for(leti=0;i{console.log("listeningworkerpid"+worker.process.pid)})cluster.on("exit",(worker,code,signal)=>{//子进程异常或崩溃退出if(code!==0&&!worker.exitedAfterDisconnect){console.log(`workerprocess${worker.id}crashed,startinganewworkerprocess`)//重启子进程cluster.fork()}})}elseif(cluster.isWorker){require("./server")}consthttp=require("http")constserver=http.createServer((req,res)=>{//随机触发错误if(Math.random()>0.5){thrownewError(`workererrorpid=${process.pid}`)}res.end(`workerpid:${process.pid}num:${num}`)}).listen(3000)如果请求抛出异常结束子进程,主进程可以listen结束事件,重启子进程。上面的重启只是一个简单的过程,实际项目中需要考虑的事情很多。这里可以参考egg的多进程模型和进程间通信。下面是来自Node.js进阶之进程与线程更全面的例子://master.jsconst{fork}=require("child_process");constnumCPUS=require("os").cpus().length;constserver=require("net").createServer();server.listen(3000);process.title="node-master";constworkers={};constcreateWorker=()=>{constworker=fork(“worker.js”);worker.on("message",message=>{if(message.act==="suicide"){createWorker();}})worker.on("exit",(code,signal)=>{控制台。log('workerprocessexited,code%ssignal:%s',code,signal);deleteworkers[worker.pid];});worker.send("服务器",服务器);工人[worker.pid]=工人;console.log("workerprocesscreated,pid%sppid:%s",worker.pid,process.ppid)}for(leti=0;i{res.writeHead(200,{"Content-Type":"text/plain"});res.end(`workerpid:${process.pid},ppid:${process.ppid}`)thrownewError("workerprocessexception!");});letworker;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('someerror')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")functionstartDaemon(){constdaemon=spawn("node",["daemon.js"],{//当前工作目录cwd:__dirname,//作为独立进程存在detached:true,//忽略输入输出流stdio:"ignore",})console.log(`Daemonppid:%spid:%s`,process.pid,daemon.pid)//切断父子进程关系daemon.unref()}startDaemon()//daemon.jsconstfs=require("fs")const{Console}=require("console");//输出日志constlogger=newConsole(fs.createWriteStream("./stdout.log"),fs.createWriteStream("./stderr.log"));//保持进程在后台运行setInterval(()=>{logger.log("daemonpid:",process.pid,"ppid:",process.ppid)},1000*10);//生成和关闭文件fs.writeFileSync("./stop.js",`process.kill(${process.pid},"SIGTERM")`)参考链接node.js使用集群实现多进程Nodejs进阶:如何玩转子进程(child_process)Node.js进阶进程与线程Nodejs进程间通信Node.js集群(cluster):ExtendyourNode.jsapplicationeggjs-多进程模型与进程间通信