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

Node单机集群入门

时间:2023-04-03 12:02:36 Node.js

本文首发于我的博客,欢迎踩~另外,本文的代码演示链接,大家可以随意fork和提PR?。文章开头,先问大家一个问题。用过Node的人都知道,Node采用了类似于Nginx的单进程、异步IO运行模型,这也是Node性能强大的根源。我们可能也经常听人说js的执行是单进程单线程的。那么,如果说Node是单进程单线程的,对吗?下面我们来验证一下。让我们执行最简单的Node程序。它只做一件事,就是不停地接受标准输入流并丢弃它,从而保证进程一直存在process.stdin.resume();启动后,我们使用ps-ef|grepnode命令查找进程的pid,并使用top命令查看进程的线程数会打印出如下信息。我不会在这里重复top命令的用法。有兴趣的同学可以自行google。这里加框的部分是进程中的线程数。可以看到不是1,而是7,由此我们就有了上一题的结论。Node是单进程的,但不是单线程的。js是单线程的呢?带着疑问,我们来看一下Node的架构图:Node标准库是我们常用的Node核心模块,如fs、path、http等。NodeBindings是JS和C++的桥梁,封装了细节V8和Libuv的,向上底层提供基础的API服务。底层也是支持Node.js的核心部分。V8是谷歌开发的JavaScript引擎。它提供了一个JavaScript运行时环境。可以说是Node.js的引擎。libuv是专门为Node.js开发的打包库。提供跨平台异步I/O能力C-ares:提供异步DNS相关能力http_parser、OpenSSL、zlib等:提供包括http解析、SSL、数据压缩等其他能力解释上面为什么有7个pictureThreads,关键在于libuv库。libuv是一个跨平台的异步IO库,实现了网络请求、文件IO、子进程、线程池等功能。可以发现libuv中有一个线程池,可以推断这7个线程很可能是libuv创建的。具体原因限于篇幅,这不是本文的重点,就不赘述了。有兴趣的同学可以这样启动Node,设置UV_THREADPOOL_SIZE=100&&nodeyour-node.js,执行需要依赖线程池的方法,比如fs.readFile,你会发现线程数增加了。综上所述,我们可以得出结论,Node默认是单进程多线程的,而js执行是单线程的。索引在本文中,我将按以下顺序介绍如何使用集群模块创建单机集群以及集群实现的基本原理。可以让大家对Node的进程和进程间通信机制有一个全面的了解。Node中集群模块的使用基本原理。由于作者还是渣男,所以还有很多地方没看懂,可能有描述不准确的地方,请原谅我。在本文的代码演示链接中,还有一些待研究的问题,已标注为TODO:。如果有人知道,请提交PR。提前致谢!!!在Node中实现一个进程的单机集群,首先要具备创建子进程的能力。默认情况下,Node作为单个进程运行,但也可以创建子进程以利用多核CPU的功能。Node中创建子进程的模块依赖于child_process。主要有四个方法:spawn(command,args):核心方法,其余三个方法在底层依赖它exec(command,options):spawn一个shell执行系统命令会有一个回调函数参数知道子进程的错误,标准输出等。execFile(file,args[,callback]):spawn一个子进程来执行一个可执行文件fork(modulePath,args):fork是spawn的一个变体,专门用来派生一个node进程。最大的特点是父子进程有自己的通信机制(IPC管道)。以上四种方法中,spwan方法是核心。了解了它的用法之后,剩下的三个就很好学了。它有几个重要的选项,如下:shell:默认情况下,spawn不会在新的shell中执行。要启用它,您可以将此配置设置为true,或将shell的名称指定为字符串。因此,支持命令的执行完全在shell的语法范围内。详见官方文档stdio:选项用于配置子进程与父进程之间建立的管道。详见detached官方文档:默认情况下,父进程退出时,子进程也会退出。当该选项设置为true时,子进程将独立于父进程,即父进程退出,子进程不退出。默认情况下,父进程会等待所有子进程退出,然后自动退出。如果想让父进程独立于子进程退出,可以调用childProcess.unref()方法断开子进程。上面两个选项stdio和unref是实现单机集群的关键选项,下面会用到。进程之间如何通信?要实现多进程架构,进程间通信能力必不可少。Node中进程间通信的方式有很多种,常用的有以下几种:IPC:Node内置的进程间通信方式,通过创建子进程时的stdio选项开启限制:需要获取进程的句柄,比如进程对象,所以完全独立两个进程不能使用这个方法stdio:这个stdio不是另一个stdio,它只是一个同义词,意思是通过进程的stdin,stdout,stderr限制:同上限制1只能传递String或Buffersocket:一种常用的进程间通信手段。Node中的net模块提供了通过socket进行通信的功能优势:无需获取进程句柄即可方便的跨进程通信限制:需要创建socket文件本文将重点介绍IPC方式,也是最Node中常用的方法。其他通信方式可以在代码演示中找到。开启方式:spawning时,将stdio选项传入数组,加上'ipc',如['ipc'],或[0,1,2,'ipc'],表示子进程的stdin,stdout,stderr都继承了主进程,并开启了IPC管道,具体见官方文档。//代码示例constcp=child_process.spawn('node',[yourfilepath],{stdio:[0,1,2,'ipc']});//orconstcp=child_process.fork(yourfile小路);fork方法创建的子进程默认有一个IPC管道。使用方法:主进程:在主进程中,可以得到子进程的句柄,如上例,就是cp,可以通过send方法向其发送消息。子进程可以通过on('message')事件监听。子进程:可以通过子进程中的进程对象获取主进程的句柄,使用方法与主进程相同。/*主进程*/constcp=spawn('node',[resolve(__dirname,'./child.js')],{//继承父进程的stdin,stdout,stderr,并在主进程建立IPC通道stdiosametime:[0,1,2,'ipc']});//将输入发送给子进程process.stdin.on('data',(d)=>{//判断是否是IPC管道connectedif(cp.connected){cp.send(d.toString());}});cp.on('message',(data)=>{log('父进程收到数据');log(data.toString());});cp.on('disconnect',()=>{log('OK,goodbyeson');});/*子进程*/process.on('message',(data)=>{process.send('子进程接收数据');//如果子进程没有继承的stdin,stdout,stderr父进程,那么这一行没有输出process.stdout.write(data);});此代码示例位于process/ipc/ipc中。使用集群模块创建集群终于进入正题。默认情况下,Node程序是单进程运行的,而js是单线程执行的,因此无法发挥多核CPU的并行能力。但是Node也提供了cluster模块,可以方便的创建多进程的单机集群。Node单机集群的核心思想是“master-worker模式”,即master进程负责向工作进程分发工作,工作进程负责完成下发的任务。以WebServer为例,主进程负责监听端口,并将每个传入的请求分发给工作进程进行业务逻辑处理。先贴官方文档。集群常用API如下:isMaster/isWorker:用于判断当前进程是主进程还是工作进程setupMaster([settings]):集群内部通过fork创建子进程,用于设置fork方法的默认配置,唯一不能设置的是fork参数中的env属性fork(filepath?):创建一个工作进程Worker实例,包括进程、id等。更多字段参见文档cluster.schedulingPolicy:设置调度策略。这是一个全局设置,将在第一个工作进程生成或调用cluster.setupMaster()时立即生效。集群中有以下两种调度策略cluster.SCHED_RR:round-robin,循环策略,即每个worker进程按顺序接收请求cluster.SCHED_NONE:抢占策略。也就是说,系统决定哪个工作进程应该处理请求。下面是一个简单的单机集群。/*主进程*/cluster.schedulingPolicy=cluster.SCHED_NONE;cluster.setupMaster({exec:resolve(__dirname,'./worker.js'),});for(leti=0;i{console.log(worker.process.pid+'responserequest');res.end('hello');}).listen(5000,()=>{console.log('process%sstarted',worker.process.pid);});本示例代码在cluster/basic中实现,搭建一个简单的单机集群后,可以通过ab-n10-c5http://127.0.0.1:5000/命令测试效果。如果不出意外,服务器的输出应该如下图所示:可以看到,分配给各个worker进程的请求基本是平均的。你可以试试换个调度策略再看看?~但是我们的集群还没有出现任何错误的处理能力,万一其中一个工作进程因为错误挂掉了怎么办?这样,工作流程就越来越少了。要解决这个问题,只需在上例的主流程代码中添加几行简单的代码即可。cluster.on('exit',(worker,code,signal)=>{console.log(`Workerprocess${worker.process.pid}hasexited`);constnewWorker=cluster.fork();console.log(`工作进程已经重启,pid:${newWorker.process.pid}`);})本示例代码如上在cluster/refork中,通过cluster.on('exit')事件监听子进程退出,并自动重启一个新的工作进程。这样,您就可以从容应对工作流程出现问题的情况。现在我们的集群已经比较稳定了,但是启动还不太优雅。因为只能在shell中启动,所以相当于shell的一个子进程。当你退出shell时,shell会回收它创建的子进程,我们的服务就会被杀死。我们需要一种方法来后台服务。还记得上面提到的ChildProcess.unref方法吗?这个方法是实现这个功能的关键。默认情况下,父进程在等待所有子进程退出后自动退出。如果希望父进程独立于子进程(即子进程退出后父进程仍然运行或者父进程不等子进程退出就可以退出),可以调用该方法断开连接子进程。调用此方法。该方法有几个注意事项:如果父子进程之间存在通信管道,则该选项无效,如stdio:'pipe'。您必须将stdio设置为'ignore'或将子进程的标准输入和输出重定向到其他地方(独立于父进程)。如果启用,主进程默认执行后直接退出,但是子进程不会退出,会被提升为init进程(Mac下launchd)的子进程,即ppid为1且unref不能用fork实现。手工来吧~我们只需要新建一个启动脚本,它所做的只是接受启动服务或终止服务的命令。实现原理是通过上面介绍的unref方法断开与脚本进程的连接,将其提升为后台进程,并将服务的进程id保存为pid文件,用于kill和调用服务进程时stop子命令被传入。也可以使用detached属性达到同样的效果,让主进程退出后子进程依然存在,但是与unref相比,使用detached还需要手动kill主进程,否则默认主进程会等待退出所有子进程。constpidFile=__dirname+'/pid';//如果进程子命令停止,killif(process.argv[2]==='stop'){constpid=fs.readFileSync(pidFile,'utf8');if(!process.kill(pid,0)){console.log(`Process${pid}不存在!`);返回;}process.kill(Number(pid));fs.unlinkSync(pidFile);}else{constcp=spawn('node',[resolve(__dirname,'./main.js')],{stdio:'ignore'});//记录主进程pidfs.writeFileSync(pidFile,cp.pid);//删除当前进程的引用计数,取消进程与子进程的关联cp.unref();}本示例代码在cluster/background/index.js中。这样我们就可以通过nodecluster/background/index.js启动服务,通过cluster/background/index.jsstop终止服务~如果想更方便的调用这个命令,也可以把脚本改成一个shell脚本,只需要在文件的顶部添加一个解析器注释,比如#!/usr/bin/envnode。至此,我们就完成了一个简单且相对稳定的单机集群,可以通过命令轻松启动和关闭。但是总的来说,我们的集群还远没有在生产环境中使用。node的cluster模块实现的单机集群还是太粗糙了。个人觉得还是用pm2比较好,这个工具功能全面,稳定,不需要修改任何业务代码。~cluster模块的基本原理由于笔者能力有限,未能完全理解cluster模块的所有代码。这里我只介绍清楚的。应该好好研究一下,写一篇关于集群原理的文章?。如何实现isMaster/isWorker?使用环境变量来判断当前进程是主进程还是子进程。fork子进程时,node会为子进程添加一个特殊的环境变量。如何创建工作流程?worker进程是通过child_process.fork方法创建的,因此可以直接使用IPC与父进程通信。请求呢?只有主进程监听端口,通过IPC管道将请求分发给子进程,子进程只通过启动服务来处理,并不真正监听端口。因为内部的listen方法被伪造成直接返回0的空方法,所以不会真正监听端口连接。问题3.主进程的服务是什么时候创建的?主进程的服务器启动实现在子进程调用listen方法时开始。如果子进程中有调用listen,就会触发主进程创建服务器或者获取创建的服务器的句柄。在创建的时候,会把子进程启动服务器的参数传递给主进程(比如端口,host等)那么问题3,主进程如何分发请求给worker进程?上面提到,进程可以通过IPC管道进行通信,即使用process.send方法向子进程发送消息。该方法的另一个重要功能是可以发送句柄,如net.Server、net.Socket等,因此可以直接将主进程的net.Server实例发送给worker进程进行处理。======分界线=======看到这里证明你是一个优秀的热爱技术的程序员,欢迎加入我们!字节跳动招聘前端已经很久了。internhcs不限,社招hcs多。将简历发送至此邮箱=>yuanye.markey@bytedance.com,并在邮箱名中注明来自Sifu。别犹豫,就现在~!!!