node是单线程运行,我们的node项目如何利用多核CPU的资源,同时提高node服务的稳定性呢?

1. node的单线程

进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。

线程是程序执行中一个单一的顺序控制流,它存在于进程之中,是比进程更小的能独立运行的基本单位。

早期在单核 CPU 的系统中,为了实现多任务的运行,引入了进程的概念,不同的程序运行在数据与指令相互隔离的进程中,通过时间片轮转调度执行,由于 CPU 时间片切换与执行很快,所以看上去像是在同一时间运行了多个程序。

由于进程切换时需要保存相关硬件现场、进程控制块等信息,所以系统开销较大。为了进一步提高系统吞吐率,在同一进程执行时更充分的利用 CPU 资源,引入了线程的概念。线程是操作系统调度执行的最小单位,它们依附于进程中,共享同一进程中的资源,基本不拥有或者只拥有少量系统资源,切换开销极小。

Node是基于V8引擎之上构建的,决定了他与浏览器的机制很类似。

一个node进程只能利用一个核,而且node只能运行在单线程中,严格意义上,node并非真正的单线程架构,即一个进程内可以有多个线程,因为node自己还有一定的i/o线程存在,这些I/O线程由底层的libuv处理,但这些线程对node开发者而言是完成透明的,只有在C++扩展时才会用到,这里我们就屏蔽底层的细节,专门讨论我们所要关注的。

单线程的好处是:程序状态单一,在没有多线程的情况下,没有锁、线程同步问题,操作系统在调度时,也因为较少的上下文的切换,可以很好地提高CPU的使用率。然而单核单线程也有相应的缺点:

  • 这个线程挂掉后整个程序就会挂掉;
  • 无法充分利用多核资源

2. node多进程的创建

node中有提供child_process模块,这个模块中,提供了多个方法来创建子进程。

const { spawn, exec, execFile, fork } = require('child_process');

这4个方法都可以创建子进程,不过使用方法还是稍微有点区别。我们以创建一个子进程计算斐波那契数列数列为例,子进程的文件(worker.js):

// worker.js
const fib = (num) => {
if (num === 1 || num === 2) {
return num;
}
let a = 1, b = 2, sum = 0;
for (let i = 3; i <= num; i++) {
sum = a + b;
a = b;
b = sum;
}
return sum;
} const num = Math.floor(Math.random() * 10) + 3;
const result = fib(num);
console.log(num, result, process.pid); // process.pid表示当前的进程id

在master.js中如何调用这些方法创建子进程呢?

命令 使用方法 解析
spawn spawn('node', ['worker.js']) 启动一个字进程来执行命令
exec exec('node worker.js', (err, stdout, stderr) => {}) 启动一个子进程来执行命令,有回调
execFile exexFile('worker.js') 启动一个子进程来执行可执行的文件
(头部要添加#!/usr/bin/env node)
fork fork('worker.js') 与spawn类似,不过这里只需要自定js文件模块即可

以fork命令为例:

const { fork } = require('child_process');
const cpus = require('os').cpus(); for(let i=0, len=cpus.length; i<len; i++) {
fork('./worker.js');
}

3. 多进程之间的通信

node中进程的通信主要在主从(子)进程之间进行通信,子进程之间无法直接通信,若要相互通信,则要通过主进程进行信息的转发。

主进程和子进程之间是通过IPC(Inter Process Communication,进程间通信)进行通信的,IPC也是由底层的libuv根据不同的操作系统来实现的。

我们还是以计算斐波那契数列数列为例,在这里,我们用cpu个数减1个的进程来进行计算,剩余的那一个用来输出结果。这就需要负责计算的子进程,要把结果传给主进程,再让主进程传给输出进行,来进行输出。这里我们需要3个文件:

  • master.js:用来创建子进程和子进程间的通信;
  • fib.js:计算斐波那契数列;
  • log.js:输出斐波那契数列计算的结果;

主进程:

// master.js

const { fork } = require('child_process');
const cpus = require('os').cpus(); const logWorker = fork('./log.js'); for(let i=0, len=cpus.length-1; i<len; i++) {
const worker = fork('./fib.js');
worker.send(Math.floor(Math.random()*10 + 4)); // 要计算的num
worker.on('message', (data) => { // 计算后返回的结果
logWorker.send(data); // 将结果发送给输出进程
})
}

计算进程:

// fib.js
const fib = (num) => {
if (num===1 || num===2) {
return num;
}
let a=1, b=2, sum=0;
for(let i=3; i<num; i++) {
sum = a + b;
a = b;
b = sum;
}
return sum;
}
process.on('message', num => {
const result = fib(num); process.send(JSON.stringify({
num,
result,
pid: process.pid
}))
})

输出进程:

process.on('message', data => {
console.log(process.pid, data);
})

当我们运行master时,就能看到各个子进程计算的结果:

第1个数字表示当前输出子进程的编号,后面表示在各个子进程计算的数据。

同理,我们在进行http服务日志记录时,也可以采用类似的思路,多个子进程承担http服务,剩下的子进程来进行日志记录等操作。

当我想用子进程创建服务器时,采用上面类似斐波那契数列的思路,将fib.js改为httpServer.js:

// httpServer.js
const http = require('http'); http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(Math.random()+'');
}).listen(8080);
console.log('http server has started at 8080, pid: '+process.pid);

结果却出现错误了,提示8080端口已经被占用了:

Error: listen EADDRINUSE: address already in use :::8080

这是因为:在TCP端socket套接字监听端口有一个文件描述符,每个进程的文件描述符都不相同,监听相同端口时就会失败。

解决方案有两种:首先最简单的就是每个子进程都使用不同的端口,主进程将循环的标识给子进程,子进程通过这个标识来使用相关的端口(例如从8080+传入的标识作为当前进程的端口号)。

第二种方案是,在主进程进行端口的监听,然后将监听的套接字传给子进程。

主进程:

// master.js
const fork = require('child_process').fork;
const net = require('net'); const server = net.createServer();
const child1 = fork('./httpServer1.js'); // random
const child2 = fork('./httpServer2.js'); // now server.listen(8080, () => {
child1.send('server', server);
child2.send('server', server);
server.close();
})

httpServer1.js:

const http = require('http');

const server = http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end(Math.random()+', at pid: ' + process.pid);
}); process.on('message', (type, tcp) => {
if (type==='server') {
tcp.on('connection', socket => {
server.emit('connection', socket)
})
}
})

httpServer2.js:

const http = require('http');

const server = http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end(Date.now()+', at pid: ' + process.pid);
}); process.on('message', (type, tcp) => {
if (type==='server') {
tcp.on('connection', socket => {
server.emit('connection', socket)
})
}
})

我们的2个server,一个是输出随机数,一个是输出当前的时间戳,可以发现这两个server都可以正常的运行。同时,因为这些进程服务是抢占式的,哪个进程抢到连接,就哪个进程处理请求。

我们也应当知道的是:

每个进程之间的内存数据是不互通的,若我们在某一进程中使用变量缓存了数据,另一个进程是读取不到的。

4. 多进程的守护

刚才我们在第3部分创建的多进程,解决了多核CPU利用率的问题,接下来要解决进程稳定的问题。

每个子进程退出时,都会触发exit事件,因此我们通过监听exit事件来获知有进程退出了,这时,我们就可以创建一个新的进程来替代。

const fork = require('child_process').fork;
const cpus = require('os').cpus();
const net = require('net'); const server = net.createServer(); const createServer = () => {
const worker = fork('./httpServer.js');
worker.on('exit', () => {
// 当有进程退出时,则创建一个新的进程
console.log('worker exit: ' + worker.pid);
createServer();
}); worker.send('server', server);
console.log('create worker: ' + worker.pid);
} server.listen(8080, () => {
for(let i=0, len=cpus.length; i<len; i++) {
createServer();
}
})

cluster模块

在多进程守护这块,node也推出了cluster模块,用来解决多核CPU的利用率问题。同时cluster中也提供了exit事件来监听子进程的退出。

一个经典的案例:

const cluster = require('cluster');
const http = require('http');
const cpus = require('os').cpus(); if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`); // 衍生工作进程。
for (let i = 0, len=cpus.length; i < len; i++) {
cluster.fork();
} cluster.on('exit', (worker) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
cluster.fork();
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end(Math.random()+ ', at pid: ' + process.pid);
}).listen(8080); console.log(`工作进程 ${process.pid} 已启动`);
}

5. 总结

node虽然是单线程运行的,但我们可以通过创建多个子进程,来充分利用多核CPU资源,通过可以监听进程的一些事件,来感知每个进程的运行状态,来提高我们项目整体的稳定性。

欢迎关注我的公众号,查阅更多的前端文章:

node多进程的创建与守护的更多相关文章

  1. 创建Android守护进程(底层服务)【转】

    本文转载自:https://blog.csdn.net/myfriend0/article/details/80016739 创建Android守护进程(底层服务) 前言 Android底层服务,即运 ...

  2. 通过node指令自动创建一个package.json文件,并封装发布使用

    通过node指令自动创建一个package.json文件,并封装发布使用:https://blog.csdn.net/scu_cindy/article/details/78208268

  3. node多进程

    内容: 1.多进程与多线程 2.node中多进程相关模块的使用 1.多进程与多线程 多线程:性能高:复杂.考验程序员 多进程:性能略低:简单.对程序员要求低 Node.js中默认:单进程.单线程,但是 ...

  4. vue+node+es6+webpack创建简单vue的demo

    闲聊: 小颖之前一直说是写一篇用vue做的简单demo的文章,然而小颖总是给自己找借口,说没时间,这一没时间一下就推到现在了,今天抽时间把这个简单的demo整理下,给大家分享出来,希望对大家也有所帮助 ...

  5. Node.js之创建应用

    1.使用Node.js时,不仅仅在实现一个应用,同时实现了整个HTTP服务器: 2.Node.js由下列几部分组成: (1)引入required模块:我们可以使用require指令来载入Node.js ...

  6. node.Js学习-- 创建服务器简要步骤

    1.创建项目目录 mkdir ningha(文件夹名)npm init 初始化项目  获得package.json 2..在node.Js命令行操作进入到文件所在目录 3.输入browser-sync ...

  7. day 33 什么是线程? 两种创建方式. 守护线程. 锁. 死锁现象. 递归锁. GIL锁

    一.线程     1.进程:资源的分配单位    线程:cpu执行单位(实体) 2.线程的创建和销毁开销特别小 3.线程之间资源共享,共享的是同一个进程中的资源 4.线程之间不是隔离的 5.线程可不需 ...

  8. Node.js:创建应用+回调函数(阻塞/非阻塞)+事件循环

    一.创建应用 如果我们使用PHP来编写后端的代码时,需要Apache 或者 Nginx 的HTTP 服务器,并配上 mod_php5 模块和php-cgi.从这个角度看,整个"接收 HTTP ...

  9. Node.js:创建第一个应用

    ylbtech-Node.js:创建第一个应用 1.返回顶部 1. Node.js 创建第一个应用 如果我们使用PHP来编写后端的代码时,需要Apache 或者 Nginx 的HTTP 服务器,并配上 ...

随机推荐

  1. Java练习 SDUT-2746_大小写转换

    大小写转换 Time Limit: 1000 ms Memory Limit: 65536 KiB Problem Description X现在要学习英文以及各种稀奇古怪的字符的了.现在他想把一串字 ...

  2. <肖申克的救赎>观后感

    肖申克的救赎主要讲述了银行家安迪在不健全的法律制度下被陷害进入了--鲨堡监狱,最后为了重见光明.追求自由,实现“自我救赎”的故事. 1.希望是件好东西,也许是世上最好的东西.好东西从来不会流逝. Ho ...

  3. selenium webdriver学习(二)————对浏览器的简单操作(转载JARVI)

    selenium webdriver学习(二)————对浏览器的简单操作 博客分类: Selenium-webdriver   selenium webdriver对浏览器的简单操作 打开一个测试浏览 ...

  4. oracle用WHERE替代ORDER BY

    ORDER BY 子句只在两种严格的条件下使用索引. ORDER BY中所有的列必须包含在相同的索引中并保持在索引中的排列顺序. ORDER BY中所有的列必须定义为非空. WHERE子句使用的索引和 ...

  5. oracle总是使用索引的第一个列

    如果索引是建立在多个列上, 只有在它的第一个列(leading column)被where子句引用时,优化器才会选择使用该索引. 译者按: 这也是一条简单而重要的规则. 见以下实例. SQL> ...

  6. em&rem

    PX特点 1. IE无法调整那些使用px作为单位的字体大小: 2. 国外的大部分网站能够调整的原因在于其使用了em或rem作为字体单位: 3. Firefox能够调整px和em,rem px像素(Pi ...

  7. 浏览器间CSS样式兼容问题

    1.display:table居中显示 在chrome和safari浏览器上兼容问题 2.滤镜 在chrome浏览器中能正常显示,在360浏览器中不能正常显示 3.省略号问题 对于一行显示,基本上对所 ...

  8. 【CSS3】精美横向滚动菜单按钮

    废话不多说,直接上图: 然后是代码: <!DOCTYPE html> <html> <head> <meta charset="utf-8" ...

  9. PyCharm自定义代码块设置方法-添加-删除【详细步骤】

    原文:https://blog.csdn.net/chichu261/article/details/82887108 在做项目的时候,有些代码会需要频繁的码.如果去已有的项目中去复制,又需要找很久. ...

  10. css写一个计算器叭

    显示效果如图,emoji可替换为数字.