溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Node的多進程服務(wù)如何實現(xiàn)

發(fā)布時間:2022-06-07 09:37:33 來源:億速云 閱讀:157 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要介紹“Node的多進程服務(wù)如何實現(xiàn)”的相關(guān)知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“Node的多進程服務(wù)如何實現(xiàn)”文章能幫助大家解決問題。

    我們現(xiàn)在已經(jīng)知道了Node是單線程運行的,這表示潛在的錯誤有可能導(dǎo)致線程崩潰,然后進程也會隨著退出,無法做到企業(yè)追求的穩(wěn)定性;另一方面,單進程也無法充分多核CPU,這是對硬件本身的浪費。Node社區(qū)本身也意識到了這一問題,于是從0.1版本就提供了child_process模塊,用來提供多進程的支持。

    1. child_process 模塊

    child_process模塊中包括了很多創(chuàng)建子進程的方法,包括forkspawn、exec、execFile等等。它們的定義如下:

    • child_process.exec(command[, options][, callback])

    • child_process.spawn(command[, args][, options])

    • child_process.fork(modulePath[, args][, options])

    • child_process.execFile(file[, args][, options][, callback])

    在這4個API中以spawn最為基礎(chǔ),因為其他三個API或多或少都是借助spawn實現(xiàn)的。

    2. spawn

    spawn方法的聲明格式如下:

    child_process.spawn(command[, args][, options])

    spawn方法會使用指定的command來生成一個新進程,執(zhí)行完對應(yīng)的command后子進程會自動退出。

    該命令返回一個child_process對象,這代表開發(fā)者可以通過監(jiān)聽事件來獲得命令執(zhí)行的結(jié)果。

    下面我們使用spwan來執(zhí)行ls命令:

    const spawn = require('child_process').spawn;
    const ls = spawn('ls', ['-1h', '/usr']);
    
    ls.stdout.on('data', (data) => {
        console.log('stdout: ', daata.toString());
    });
    
    ls.stderr.on('data', (data) => {
        console.log('stderr: ', daata.toString());
    });
    
    ls.on('close', (code) => {
        console.log('child process exited with code', code);
    });

    其中spawn的第一個參數(shù)雖然是command,但實際接收的卻是一個file,可以在Linux或者Mac OSX上運行,這是由于ls命令也是以可執(zhí)行文件形式存在的。

    類似的,在Windows系統(tǒng)下我們可以試著使用dir命令來實現(xiàn)功能類似的代碼:

    const spawn = require('child_process').spawn;
    const ls = spawn('dir');
    
    ls.stdout.on('data', (data) => {
        console.log('stdout: ', daata.toString());
    });

    然而在Windows下執(zhí)行上面代碼會出現(xiàn)形如Error:spawn dir ENOENT的錯誤。

    原因就在于spawn實際接收的是一個文件名而非命令,正確的代碼如下:

    const spawn = require('child_process').spawn;
    const ls = spawn('powershell', ['dir']);
    
    ls.stdout.on('data', (data) => {
        console.log('stdout: ', daata.toString());
    });

    這個問題的原因與操作系統(tǒng)本身有關(guān),在Linux中,一般都是文件,命令行的命令也不例外,例如ls命令是一個名為ls的可執(zhí)行文件;而在Windows中并沒有名為dir的可執(zhí)行文件,需要通過cmd或者powershell之類的工具提供執(zhí)行環(huán)境。

    3. fork

    在Linux環(huán)境下,創(chuàng)建一個新進程的本質(zhì)是復(fù)制一個當前的進程,當用戶調(diào)用 fork 后,操作系統(tǒng)會先為這個新進程分配空間,然后將父進程的數(shù)據(jù)原樣復(fù)制一份過去,父進程和子進程只有少數(shù)值不同,例如進程標識符(PD)。

    對于 Node 來說,父進程和子進程都有獨立的內(nèi)存空間和獨立的 V8 實例,它們和父進程唯一的聯(lián)系是用來進程間通信的 IPC Channel。

    此外,Node中fork和 POSIX 系統(tǒng)調(diào)用的不同之處在于Node中的fork并不會復(fù)制父進程。

    Node中的fork是上面提到的spawn的一種特例,前面也提到了Node中的fork并不會復(fù)制當前進程。多數(shù)情況下,fork接收的第一個參數(shù)是一個文件名,使用fork("xx.js")相當于在命令行下調(diào)用node xx.js,并且父進程和子進程之間可以通過process.send方法來進行通信。

    下面我們來看一個簡單的栗子:

    // master.js 調(diào)用 fork 來創(chuàng)建一個子進程
    const child_process = require('child_process');
    const worker = child_process.fork('worker.js', ['args1']);
    worker.on('exit', () => {
      console.log('child process exit');
    });
    worker.send({ msg: 'hello child' });
    worker.on('message', msg => {
      console.log('from child: ', msg);
    });
    
    
    // worker.js
    const begin = process.argv[2];
    console.log('I am worker ' + begin);
    process.on('message', msg => {
      console.log('from parent ', msg);
      process.exit();
    });
    process.send({ msg: 'hello parent' });

    fork內(nèi)部會通過spawn調(diào)用process.executePath,即Node的可執(zhí)行文件地址來生成一個Node實例,然后再用這個實例來執(zhí)行fork方法的modulePath參數(shù)。

    輸出結(jié)果為:

    I am worker args1
    from parent  { msg: 'hello child' }
    from child:  { msg: 'hello parent' }
    child process exit

    4. exec 和 execFile

    如果我們開發(fā)一種系統(tǒng),那么對于不同的模塊可能會用到不同的技術(shù)來實現(xiàn),例如 Web服務(wù)器使用 Node ,然后再使用 Java 的消息隊列提供發(fā)布訂閱服務(wù),這種情況下通常使用進程間通信的方式來實現(xiàn)。

    但有時開發(fā)者不希望使用這么復(fù)雜的方式,或者要調(diào)用的干脆是一個黑盒系統(tǒng),即無法通過修改源碼來進行來實現(xiàn)進程間通信,這時候往往采用折中的方式,例如通過 shell 來調(diào)用目標服務(wù),然后再拿到對應(yīng)的輸出。

    child_process提供了一個execFile方法,它的聲明如下:

    child_process.execFile(file, args, options, callback)

    說明:

    • file {String}要運行的程序的文件名

    • args {Array}字符串參數(shù)列表

    • options {Object}

      • cwd {String}子進程的當前工作目錄

      • env {Object}環(huán)境變量鍵值對

      • encoding {String}編碼(默認為 'utf8'

      • timeout {Number}超時(默認為 0)

      • maxBuffer {Number}緩沖區(qū)大?。J為 200*1024)

      • killSignal {String}結(jié)束信號(默認為'SIGTERM'

    • callback {Function}進程結(jié)束時回調(diào)并帶上輸出

      • error {Error}

      • stdout {Buffer}

      • stderr {Buffer}

      • 返回:ChildProcess對象

    可以看出,execfilespawn在形式上的主要區(qū)別在于execfile提供了一個回調(diào)函數(shù),通過這個回調(diào)函數(shù)可以獲得子進程的標準輸出/錯誤流。

    使用 shell 進行跨進程調(diào)用長久以來被認為是不穩(wěn)定的,這大概源于人們對控制臺不友好的交互體驗的恐懼(輸入命令后,很可能長時間看不到一個輸出,盡管后臺可能在一直運算,但在用戶看來和死機無異)。

    在 Linux下執(zhí)行exec命令后,原有進程會被替換成新的進程,進而失去對新進程的控制,這代表著新進程的狀態(tài)也沒辦法獲取了,此外還有 shell 本身運行出現(xiàn)錯誤,或者因為各種原因出現(xiàn)長時間卡頓甚至失去響應(yīng)等情況。

    Node.js 提供了比較好的解決方案,timeout解決了長時間卡頓的問題,stdoutstderr則提供了標準輸出和錯誤輸出,使得子進程的狀態(tài)可以被獲取。

    5. 各方法之間的比較

    5.1 spawn 和 execFile

    為了更好地說明,我們先寫一段簡單的 C 語言代碼,并將其命名為 example.c:

    #include<stdio.h>
    int main() {
        printf("%s", "Hello World!");
        return 5;
    }

    使用 gcc 編譯該文件:

    gcc example.c -o example

    生成名為example的可執(zhí)行文件,然后將這個可執(zhí)行文件放到系統(tǒng)環(huán)境變量中,然后打開控制臺,輸入example,看到最后輸出"Hello World"。

    確保這個可執(zhí)行文件在任意路徑下都能訪問。

    我們分別用spawnexecfile來調(diào)用example文件。

    首先是spawn。

    const spawn = require('child_process').spawn;
    const ls = spawn('example');
    
    ls.stdout.on('data', (data) => {
        console.log('stdout: ', daata.toString());
    });
    
    ls.stderr.on('data', (data) => {
        console.log('stderr: ', daata.toString());
    });
    
    ls.on('close', (code) => {
        console.log('child process exited with code', code);
    });

    程序輸出:

    stdout: Hello World!
    child process exited with code 5

    程序正確打印出了Hello World,此外還可以看到example最后的return 5會被作為子進程結(jié)束的code被返回。

    然后是execFile

    const exec = require('child_process').exec;
    const child = exec('example', (error, stdout, stderr) => {
        if (error) {
            throw error;
        }
        console.log(stdout);
    });

    同樣打印出Hello World,可見除了調(diào)用形式不同,二者相差不大。

    5.2 execFile 和 spawn

    在子進程的信息交互方面,spawn使用了流式處理的方式,當子進程產(chǎn)生數(shù)據(jù)時,主進程可以通過監(jiān)聽事件來獲取消息;而exec是將所有返回的信息放在stdout里面一次性返回的,也就是該方法的maxBuffer參數(shù),當子進程的輸出超過這個大小時,會產(chǎn)生一個錯誤。

    此外,spawn有一個名為shell的參數(shù):

    其類型為一個布爾值或者字符串,如果這個值被設(shè)置為true,,就會啟動一個 shell 來執(zhí)行命令,這個 shell 在 UNIX上是 bin/sh,,在Windows上則是cmd.exe。

    5.3 exec 和 execFile

    exec在內(nèi)部也是通過調(diào)用execFile來實現(xiàn)的,我們可以從源碼中驗證這一點,在早期的Node源碼中,exec命令會根據(jù)當前環(huán)境來初始化一個 shell,,例如 cmd.exe 或者 bin/sh,然后在shell中調(diào)用作為參數(shù)的命令。

    通常execFile的效率要高于exec,這是因為execFile沒有啟動一個 shell,而是直接調(diào)用 spawn來實現(xiàn)的。

    6. 進程間通信

    前面介紹的幾個用于創(chuàng)建進程的方法,都是屬于child_process的類方法,此外childProcess類繼承了EventEmitter,在childProcess中引入事件給進程間通信帶來很大的便利。

    childProcess中定義了如下事件。

    • Event:'close':進程的輸入輸出流關(guān)閉時會觸發(fā)該事件。

    • Event:'disconnect':通常childProcess.disconnect調(diào)用后會觸發(fā)這一事件。

    • Event:'exit':進程退出時觸發(fā)。

    • Event:'message':調(diào)用child_process.send會觸發(fā)這一事件

    • Event:'error':該事件的觸發(fā)分為幾種情況:

      • 該進程無法創(chuàng)建子進程。

      • 該進程無法通過kill方法關(guān)閉。

      • 無法發(fā)送消息給子進程。

    Event:'error'事件無法保證一定會被觸發(fā),因為可能會遇到一些極端情況,例如服務(wù)器斷電等。

    上面也提到,childProcess模塊定義了send方法,用于進程間通信,該方法的聲明如下:

    child.send(message[, sendHandle[, options]][, callback])

    通過send方法發(fā)送的消息,可以通過監(jiān)聽message事件來獲取。

    // master.js 父進程向子進程發(fā)送消息
    const child_process = require('child_process');
    const worker = child_process.fork('worker.js', ['args1']);
    worker.on('exit', () => {
      console.log('child process exit');
    });
    worker.send({ msg: 'hello child' });
    worker.on('message', msg => {
      console.log('from child: ', msg);
    });
    
    
    // worker.js 子進程接收父進程消息
    const begin = process.argv[2];
    console.log('I am worker ' + begin);
    process.on('message', msg => {
      console.log('from parent ', msg);
      process.exit();
    });
    process.send({ msg: 'hello parent' });

    send方法的第一個參數(shù)類型通常為一個json對象或者原始類型,第二個參數(shù)是一個句柄,該句柄可以是一個net.Socket或者net.Server對象。下面是一個例子:

    //master.js 父進程發(fā)送一個 Socket 對象
    const child = require('child_process').fork('worker.js');
    // Open up the server object and send the handle.
    const server = require('net').createServer();
    server.on('connection', socket => {
      socket.end('handled by parent');
    });
    server.listen(1337, () => {
      child.send('server', server);
    });
    
    
    //worker.js 子進程接收 Socket 對象
    process.on('message', (m, server) => {
      if (m === 'server') {
        server.on('connection', socket => {
          socket.end('handled by child');
        });
      }
    });

    7. Cluster

    前面已經(jīng)介紹了child_process的使用,child_process的一個重要使用場景是創(chuàng)建多進程服務(wù)來保證服務(wù)穩(wěn)定運行。

    為了統(tǒng)一 Node 創(chuàng)建多進程服務(wù)的方式,Node 在之后的版本中增加了Cluster模塊,Cluster可以看作是做了封裝的child_Process模塊。

    Cluster模塊的一個顯著優(yōu)點是可以共享同一個socket連接,這代表可以使用Cluster模塊實現(xiàn)簡單的負載均衡。

    下面是Cluster的簡單栗子:

    const cluster = require('cluster');
    const http = require('http');
    const numCPUs = require('os').cpus().length;
    
    if (cluster.isMaster) {
      console.log('Master process id is', process.pid);
      // Fork workers.
      for (let i = 0; i < numCPUs; i++) {
        cluster.fork();
      }
      cluster.on('exit', (worker, code, signal) => {
        console.log('worker process died, id ', worker.process.pid);
      });
    } else {
      // Worker 可以共享同一個 TCP 連接
      // 這里的例子是一個 http 服務(wù)器
      http.createServer((req, res) => {
        res.writeHead(200);
        res.end('hello world\n');
      }).listen(8000);
    
      console.log('Worker started, process id', process.pid);
    }

    上面是使用Cluster模塊的一個簡單的例子,為了充分利用多核CPU,先調(diào)用OS模塊的cpus()方法來獲得CPU的核心數(shù),假設(shè)主機裝有兩個 CPU,每個CPU有4個核,那么總核數(shù)就是8。

    在上面的代碼中,Cluster模塊調(diào)用fork方法來創(chuàng)建子進程,該方法和child_process中的fork是同一個方法。

    Cluster模塊采用的是經(jīng)典的主從模型,由master進程來管理所有的子進程,可以使用cluster.isMaster屬性判斷當前進程是master還是worker,其中主進程不負責具體的任務(wù)處理,其主要工作是負責調(diào)度和管理,上面的代碼中,所有的子進程都監(jiān)聽8000端口。

    通常情況下,如果多個 Node 進程監(jiān)聽同一個端口時會出現(xiàn)Error: listen EADDRINUS的錯誤,而Cluster模塊能夠讓多個子進程監(jiān)聽同一個端口的原因是master進程內(nèi)部啟動了一個 TCP 服務(wù)器,而真正監(jiān)聽端口的只有這個服務(wù)器,當來自前端的請求觸發(fā)服務(wù)器的connection事件后,master會將對應(yīng)的socket句柄發(fā)送給子進程。

    關(guān)于“Node的多進程服務(wù)如何實現(xiàn)”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識,可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會為大家更新不同的知識點。

    向AI問一下細節(jié)

    免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI