前言

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构建多进程应用的资料请关注阿兔在线工具其它相关文章!

点赞(0)

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部