node多进程的建立与守护

本篇文章主要分为4部分讲解:javascript

  1. node的单线程
  2. node多进程的建立
  3. 多进程间的通讯
  4. 多进程的维护

1. node的单线程

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

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

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

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

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

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

node的基础架构-蚊子的博客

单线程的好处是:程序状态单一,在没有多线程的状况下,没有锁、线程同步问题,操做系统在调度时,也由于较少的上下文的切换,能够很好地提升CPU的使用率。然而单核单线程也有相应的缺点:服务器

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

node的单核运行-蚊子的博客

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资源,经过能够监听进程的一些事件,来感知每一个进程的运行状态,来提升咱们项目总体的稳定性。

欢迎关注个人公众号,我们一块儿学习进步:

蚊子的博客-公众号
相关文章
相关标签/搜索