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模块,这个模块中,提供了多个方法来创建子进程。

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

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

  1. // worker.js
  2. const fib = (num) => {
  3. if (num === 1 || num === 2) {
  4. return num;
  5. }
  6. let a = 1, b = 2, sum = 0;
  7. for (let i = 3; i <= num; i++) {
  8. sum = a + b;
  9. a = b;
  10. b = sum;
  11. }
  12. return sum;
  13. }
  14. const num = Math.floor(Math.random() * 10) + 3;
  15. const result = fib(num);
  16. 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命令为例:

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

3. 多进程之间的通信

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

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

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

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

主进程:

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

计算进程:

  1. // fib.js
  2. const fib = (num) => {
  3. if (num===1 || num===2) {
  4. return num;
  5. }
  6. let a=1, b=2, sum=0;
  7. for(let i=3; i<num; i++) {
  8. sum = a + b;
  9. a = b;
  10. b = sum;
  11. }
  12. return sum;
  13. }
  14. process.on('message', num => {
  15. const result = fib(num);
  16. process.send(JSON.stringify({
  17. num,
  18. result,
  19. pid: process.pid
  20. }))
  21. })

输出进程:

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

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

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

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

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

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

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

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

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

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

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

主进程:

  1. // master.js
  2. const fork = require('child_process').fork;
  3. const net = require('net');
  4. const server = net.createServer();
  5. const child1 = fork('./httpServer1.js'); // random
  6. const child2 = fork('./httpServer2.js'); // now
  7. server.listen(8080, () => {
  8. child1.send('server', server);
  9. child2.send('server', server);
  10. server.close();
  11. })

httpServer1.js:

  1. const http = require('http');
  2. const server = http.createServer((req, res) => {
  3. res.writeHead(200, {
  4. 'Content-Type': 'text/plain'
  5. });
  6. res.end(Math.random()+', at pid: ' + process.pid);
  7. });
  8. process.on('message', (type, tcp) => {
  9. if (type==='server') {
  10. tcp.on('connection', socket => {
  11. server.emit('connection', socket)
  12. })
  13. }
  14. })

httpServer2.js:

  1. const http = require('http');
  2. const server = http.createServer((req, res) => {
  3. res.writeHead(200, {
  4. 'Content-Type': 'text/plain'
  5. });
  6. res.end(Date.now()+', at pid: ' + process.pid);
  7. });
  8. process.on('message', (type, tcp) => {
  9. if (type==='server') {
  10. tcp.on('connection', socket => {
  11. server.emit('connection', socket)
  12. })
  13. }
  14. })

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

我们也应当知道的是:

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

4. 多进程的守护

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

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

  1. const fork = require('child_process').fork;
  2. const cpus = require('os').cpus();
  3. const net = require('net');
  4. const server = net.createServer();
  5. const createServer = () => {
  6. const worker = fork('./httpServer.js');
  7. worker.on('exit', () => {
  8. // 当有进程退出时,则创建一个新的进程
  9. console.log('worker exit: ' + worker.pid);
  10. createServer();
  11. });
  12. worker.send('server', server);
  13. console.log('create worker: ' + worker.pid);
  14. }
  15. server.listen(8080, () => {
  16. for(let i=0, len=cpus.length; i<len; i++) {
  17. createServer();
  18. }
  19. })

cluster模块

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

一个经典的案例:

  1. const cluster = require('cluster');
  2. const http = require('http');
  3. const cpus = require('os').cpus();
  4. if (cluster.isMaster) {
  5. console.log(`主进程 ${process.pid} 正在运行`);
  6. // 衍生工作进程。
  7. for (let i = 0, len=cpus.length; i < len; i++) {
  8. cluster.fork();
  9. }
  10. cluster.on('exit', (worker) => {
  11. console.log(`工作进程 ${worker.process.pid} 已退出`);
  12. cluster.fork();
  13. });
  14. } else {
  15. http.createServer((req, res) => {
  16. res.writeHead(200);
  17. res.end(Math.random()+ ', at pid: ' + process.pid);
  18. }).listen(8080);
  19. console.log(`工作进程 ${process.pid} 已启动`);
  20. }

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. @codeforces - 702F@ T-Shirts

    目录 @description@ @solution@ @accepted code@ @details@ @description@ 有 n 件 T-shirt,第 i 件 T-shirt 有一个 ...

  2. [C#] 如何分析stackoverflow等clr错误

    有时候由于无限递归调用等代码错误,w3wp.exe会报错退出,原因是clr.exe出错了. 这种错误比较难分析,因为C#代码抓不住StackOverflowException等异常. 处理方法是:生成 ...

  3. Pytorch的网络结构可视化(tensorboardX)(详细)

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/xiaoxifei/article/det ...

  4. 我钟爱的HTML5和CSS3在线工具【转】

    我真的喜欢上了HTML5, CSS3, JavaScript编程,但是有一些代码还是需要一些辅助工具来做才行,例如,CSS3的Gradient渐变如果手写代码的话真的不爽,还有像animation动画 ...

  5. iptables 累计(Accounting)

    对於每一条规则,核心各自设置两个专属的计数器,用于累计符合该条件的封包数,以及这些封包的总位元组数.这两项资讯可用於统计网路用量. 举例来說,假设有一台Internet闸道器路,eth0接内部网络,e ...

  6. PHP实现图片的等比缩放和Logo水印功能示例

    文章来自于:脚本之家 文章链接:https://www.jb51.net/article/112909.htm 这篇文章主要介绍了PHP实现图片的等比缩放和Logo水印功能,结合实例形式分析了php图 ...

  7. css设置Overflow实现隐藏滚动条的同时又可以滚动

    .scroll-list ul{ white-space: nowrap; -webkit-overflow-scrolling: touch; overflow-x: auto; overflow- ...

  8. VMware station 安装报错 failed to install the hcmon driver

    VMware station 安装报错 failed to install the hcmon driver 1.将 C:\Windows\System32\drivers 下的hcmon.sys改名 ...

  9. 【BestCoder Round #93 1002】MG loves apple

    [题目链接]:http://acm.hdu.edu.cn/showproblem.php?pid=6020 [题意] 给你一个长度为n的数字,然后让你删掉k个数字,问你有没有删数方案使得剩下的N-K个 ...

  10. 用winrar和zip命令拷贝目录结构

    linux系统下使用zip命令 zip -r source.zip source -x *.php -x *.html # 压缩source目录,排除里面的php和html文件 windows系统下使 ...