前言
JavaScript 主线程运行在单个进程的单个线程上。这样做的好处是:
程序状态是单一的,在没有多线程的情况下没有锁、线程同步问题, 操作系统在调度时因为较少上下文的切换,可以很好地提高CPU的使用率。但是单进程单线程并非完美的结构,一旦线程中某段代码发生异常阻塞,会阻塞代码执行而浪费资源。同时,如今CPU基本均是多核的,服务器往往还有多个CPU。
因此 Nodejs 不可避免的面临两个问题: 如何充分利用多核CPU ****和 **** 如何保证进程的健壮性和稳定性 。 另外,由于 Nodejs 中所有处理都在单线程上进行,影响事件驱动服务模型性能的点在于CPU的计算能力,而不受多进程或多线程模式中资源上限的影响,可伸缩性远比前两者高。如果解决掉多核CPU的利用问题,带来的性能上提升是可观的。
进程的创建和使用
多核利用率
面对单进程单线程对多核使用不足的问题,最简单的方法是启动多进程即可。理想状态下每个进程各自利用一个CPU,以此实现多核CPU的利用。Nodejs 提供了 child_process模块 ,并且也提供了 child_process.fork() 方法帮助我们实现进程的复制。
你可以通过这个方法在本地启动多个 HTTP 服务,首先编写一段创建 http服务端 的代码:
const http = require("http"); const server = http.createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("hello,world!"); }); // 随机监听1000-1999的任意一个端口 server.listen(Math.floor((1 + Math.random()) * 1000));
然后创建一个主进程 master.js 来启动和管理他们:
const fork = require("child_process").fork; const cpus = require("os").cpus(); console.log(cpus.length); for (let i = 0; i < cpus.length; i++) { fork('./server.js'); }
?? 在 Linux 系统中,你可以通过 ps aux | grep worker.js 来直接查看,在 Windows 中,你能通过 netstat -a 然后查看 1000-1999 端口的进程。
这样的模式称为 Master-Worker模式 ,又称 主从模式 。如图,进程分为两种:主进程和工作进程。这是典型的分布式架构中用于并行处理业务的模式,具备较好的可伸缩性和稳定性:
主进程不负责具体的业务处理,而是负责调度或管理工作进程,它是趋向于稳定的。 工作进程负责具体的业务处理,趋于不稳定。
?? fork() 能让我们复制进程使每个CPU内核都使用上,但是依然要切记 fork() 进程是昂贵的。因为Node通过事件驱动和异步IO的方式很大的缓解了并发问题,所以这里启动多个进程只是为了充分将CPU资源利用起来,而不是为了解决并发问题。
创建子进程
child_process模块 给予 Node 可以随意创建子进程(child_process)的能力。它提供了4个方法用于创建子进程:
spawn() :启动一个子进程来执行命令。 exec() :启动一个子进程来执行命令。与spawn()不同的是其接口不同,它有一个回调函数获知子进程的状况。 execFile() :启动一个子进程来执行可执行文件。 fork() :与 spawn() 类似,不同点在于它创建 Node 的子进程只需指定要执行的 JavaScript 文件模块即可。const cp = require("child_process"); cp.spawn("node", ["./server.js"]); cp.exec("node server.js", (err, stdout, stderr) => { // TODO }); cp.execFile("server.js", (err, stdout, stderr) => { // TODO }); cp.fork('./server.js');
?? execFile() 只能用于可以直接执行的文件。在 Linux 中,你可以在文件开头加入 #! /usr/bin/env node
进程间通信 IPC
在 Master-Worker模式 中,要实现主进程管理和调度工作进程的功能,需要主进程和工作进程之间的通信。子进程对象则由 send() 方法实现主进程向子进程发送数据, message事件 实现收听子进程发来的数据:
// parent.js const cp = require("child_process"); const n = cp.fork("./child.js"); n.on('message', (msg) => { console.log('parent got msg: ', msg); }); n.send({ s: 'hello, world', }); // child.js process.on('message', (msg) => { console.log('child got msg', msg); }); process.send({ b: 'bar', });
?? HTML5提出了 WebWorker API 来创建工作线程,而主进程和工作进程之间通过 onmessage() 和 postMessage() 进行通信。具体参考 MDN文档 。
通过 fork() 或者其他API,创建子进程之后,为了实现父子进程之间的通信,父进程与子进程之间将会创建IPC通道(Inter-Process Communication,即进程间通信) 。通过IPC通道,父子进程之间才能通过message和send()传递消息。
父进程在实际创建子进程之前,会创建IPC通道并监听它,然后才真正创建出子进程,并通过环境变量(NODE_CHANNEL_FD)告诉子进程这个IPC通道的文件描述符。子进程在启动的过程中,根据文件描述符去连接这个已存在的IPC通道,从而完成父子进程之间的连接。
?? 只有启动的子进程是Node进程时,子进程才会根据环境变量去连接IPC通道。对于其他类型的子进程则无法实现进程间通信,除非其他进程也按约定去连接这个已经创建好的IPC通道
总结
今天我们讨论了 Nodejs 在多核利用方法的问题,然后介绍了创建子进程、进程间通信与IPC通道的内容。通过这些基础技术,用 child_process模块 在单机上搭建 Nodejs集群 是件相对容易的事情,让 Nodejs 能够在多核CPU的环境下充分利用计算资源。
以上就是详解如何利用Nodejs构建多进程应用的详细内容,更多关于Nodejs构建多进程应用的资料请关注其它相关文章!
查看更多关于详解如何利用Nodejs构建多进程应用的详细内容...