Node之CPU吃重的任務要如何處理 ?
nodejs
Lastmod: 2019-12-15

這篇文章中,我們希望學習到 :

在開發nodejs時,如果遇到cpu密集型的任務時,要如何處理 ?

首先我們先來複習一下nodejs的機制一下。

我們都知道nodejs是屬於單一執行序架構,在其它的語言裡,每當有一個請求進來時,它們都會產生一個執行緒,但nodejs則否,他是用一個執行緒就來處理所有的請求,而他的背後就是有個事件機制設計才能做到這種方法。請參考這篇

但為什麼要設計成用單一執行序架構呢?

這邊我們要先來說說I/O操作。

I/O 問題

I/O就是電腦中資料與記憶體、硬碟或網路的輸入和輸出,他基本上是電腦作業裡最慢的事物,I/O操作基本上對 cpu 而言通常負擔很小,但是問題就在於它很耗時

傳統的阻塞I/O設計方式如下 :

data = getData();

print(data);

我們假設getData是要去讀取一個檔案,而這時會等到getData執行完後,就資料傳送給data時我們才可以使用。

那假設我們這個getData要讀很久,那這樣的話其它的請求著麼辦 ?

傳統的作法就會像下面這張圖一樣,系統會分別的開啟不同的執行緒來進行處理,如此一來,當有某個執行緒因I/O操作而阻塞時,就不會影響到其它的請求。

這種作法的缺點就在於 :

開啟執行緒的成本不低,它會消耗記憶體而且引發環境切換

node他著麼處理呢 ?

他使用單一執行緒機制,而他的執行緒中有一個機制被稱為事件機制,簡單的說事件機制可以將所有的請求收集起來,並且將需要長時間處理的工作丟出去工作給其它人做(I/O),然後繼續接收新的請求,就如同下圖一樣,這樣的優點就在於,他可以接受更多的請求,,而不會因為一個長時間的I/O,其它東西就都卡住不能動。

但他也是有缺點的 :

它無法充分利用多核 cpu 資源

當 Event loop 遇到 CPU 密集型任務會發生什麼事 ?

上面有提到單一執行緒機制有一個缺點,那就是無法統分利用cpu資源,這是什麼意思呢 ?

傳統的方式,每個請求分配一個執行緒,他都可以得到一個不同於自已的 cpu,在這種情況下多執行緒可以大大的提高資源使用效率。

而這也代表的單執行緒他就只能占用一個 cpu ,並且如果某個任務是很吃 cpu 的工作時,這執行緒就會被那個任務占用,導致其它的任務、請求都無法執行。

我們下面簡單的寫一段程式碼來看看會發生什麼事情。

下面這段程式碼裡,我們將簡單的建立一個server,它一收到請求,就會開始計算費波南西數列,這種運算基本上就是一個很耗 CPU 的工作。

const http = require('http');

http.createServer(function (req, res) {
    console.log("master:" + process.pid);

    res.writeHead(200);
    res.write(fib(46).toString());
    res.end();

}).listen(8000, function () {
    console.log('started');
});

function fib(n) {
    return n > 1 ? fib(n - 1) + fib(n - 2) : 1;
}

然後當我們啟動這個 server 後,你會注意到,第一個請求發送以後,你會在 console 看到下面的輸出 :

master:68375

也就是打印出這個process的 pid ,但它會還沒回傳值給第一個請求,然後這時如果你在發送一個請求,你會注意到它沒有打印出 master:68375這段資訊。

為什麼呢 ? 這就是我們上面說的node屬於單一執行緒機制,他就只能占用一個 cpu 並且因為第一個請求的運算還在執行,導致其它的請求都會無法執行,只有等到第一個請求結束後,才會繼續執行。

注意 process 進程thread 執行緒是兩個不一樣的東西

我們這邊簡單的說明一下process進程thread執行緒的關係, 首先在傳統的系統中進程是個容器,而執行緒就是容器中的工作單位

進程就是我們在 windows 系統下,打開工作管員裡的processes,你看到一行一行的就都是進程,而且你打開每個chrome頁面他都是一個進程,而進程間的通訊則使用IPC方法。

執行緒是包含在進程內的工作單位,在同一個進程裡,所有的執行緒都共享系統資源,但他們同時也都有自已的stackcontext,而且可以共享變數。

那要如何解決呢 ?

開一個新的 process 來處理

在 javascript 中我們可以使用一個叫Web Worker的東西來處理,可以看一下筆者年輕時寫的這篇文章HTML5之走在平行時空的Web Worker

而在 node 中我們可以使用child_process,這個模組可以幫助我們建立child process,中文來說就是子進程,我們使用這模組中的fork來建立時,它同時會提供IPC通道讓我們可以使用訊息來進行process 與 process 的溝通

接下來我們就是要將費波南西數列的運算,丟到另一個子進程中來處理,這樣我們的請求也就可以同時的處理了。

下面為我們修改後的程式碼,我們會使用child_process.fork('./subset.js')來建立子進程,並且我們會使用send方法將資料丟到子進程中,然後在用on('message')來監聽回傳結果。

這種寫法實際執行測試後,你會發生每當你發一個請求時,都會打印出master:68375,這也代表我們的執行緒不會在塞住了,而且你在實際丟兩個請求來測試有用子進程的執行速度,你會發現快了兩倍。

const child_process = require('child_process');

http.createServer(function (req, res) {
    console.log("master:" + process.pid);
    const child = child_process.fork('./subprocess.js');
    child.send({ value: 45 });

    child.on('message', function (m) {
        res.writeHead(200);
        res.write(m.result.toString());
        res.end();
    })
}).listen(8000, function () {
    console.log('started');
});
// subprocess.js

function fibo(n){
    return n>1 ? fibo(n-1) + fibo(n-2) : 2;
}

process.on('message', function (message) {
    console.log("child:" + process.pid)
    process.send({result: fibo(message.value)});
})

但是呢 ? 上面這種寫法還是有個缺點,那就是代表每一個請求都會多開一個子進程,這樣也代表這請求一多就會開了一堆子進程,這樣是很浪費資源的,所以接下來我們會修改一下增加一個 pool 來管理這些子進程,好處在於可以節省資源,而另一個好處可以阻斷服務攻擊 Dos

這個我們就留到下一篇cluster時在來說明囉。

參考資料

comments powered by Disqus