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資源,經過能夠監聽進程的一些事件,來感知每一個進程的運行狀態,來提升咱們項目總體的穩定性。

歡迎關注個人公衆號,我們一塊兒學習進步:

蚊子的博客-公衆號
相關文章
相關標籤/搜索