本文首发于我的博客,欢迎踩~另外,本文的代码演示链接,大家可以随意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
