iT邦幫忙

6

Week2 - 你有沒有想過,到底Server是如何「同時處理多個requests」的? - Node.js篇 [鼠年全馬鐵人挑戰-NodeJs轉Golang的爆炸之旅系列]

文章也同時發表於medium(`・ω・´)”

某次我跟partner小明發生了一件趣事,不知大家是否也有以下相似的經驗:

小明:你的server好像卡住了,client這邊打requests要等很久耶!

我:ㄜ...我記得Node.js因為有Event queueEvent loop非同步模型,所以是非阻塞IO的server,這好像就是我要一直要用callback的原因,整個server那麼多callback應該是不會被多個requests而阻塞吧?

小明:喔喔,這應該就是Node.js適合IO-bound的原因吧,但聽說還有CPU-bound,CPU是不是也有可能阻塞啊?

我:???

小明:我說...

等等!好多專有名詞你到底在公蝦咪

一開始聽到這些名詞,我矇圈了許久。

我們現在來介紹這些名詞到底能辦到什麼,讓大家先有個概念。

大家要先有一個認知就是,Node.js是單執行緒-single thread,所以做什麼事情都沒辦法使用影分身之術多執行緒-multi thread來多工,但網路的世界肯定是多工的啊!

舉個例子:「如果FB網頁載入一張圖片,聊天框就不能動了」,那麼大家肯定都在也不用FB,大家都視力2.0,而Node.js怎麼解決這件事呢?

以下就用麥當當點餐情境配合圖片來解釋:

  • 讓requests可等候的-Event queue

顧客(request)光顧麥當當時,需要有一個空間可以等候餐點,而Event queue就是此空間。

  • 負責傳遞餐點task的-Event loop

面對大量個顧客,Node.js必須要有快速處理餐點的能力,此時靠的就是櫃檯Event loop(這間麥當當只有一個櫃檯),負責把顧客的餐點需求告知給廚房,大廚們就會「自行分配」到底誰要處理漢堡、薯條等等。

  • 你們廚房繼續做,我繼續為顧客點餐的-非同步

而當廚房餐點正在製作時,櫃檯人員還是能繼續服務顧客點餐,在處理點餐時,大廚們的餐點好了通知櫃檯(callback),這時櫃檯會停下手邊工作,把餐點送給客人,再繼續原本的點餐服務。

這代表了Node.js不會一直處理顧客點餐的服務,當餐點好了就會先暫停手邊工作而去送餐,這讓執行的順序是非同步的

  • 因櫃檯與廚房都好好合作,所以排隊不會塞車的-非阻塞IO(non-blocking IO)

大家可以發現到,就算櫃檯始終只有一個,但有了大廚們的配合,還是可以很快地處理完這些餐點,可以想想看,如果沒有大廚們,全都靠櫃檯人員負責點餐跟煮菜,那麥當當應該直接排隊排到馬路上。

所以套回Node.js上,因為Node.js很適合這樣與「不同周邊互動」的情境,例如:其他server、資料庫、文件溝通時,所以他很適合密集的接受一連串的點餐,「不會因溝通而停止自己手邊的工作」即是非阻塞IO,因此我們說「Node.js適合IO-bound(IO密集型)操作」。


於是麥當當又過了和平沒有塞車的一天,直到...

  • 櫃檯人員點餐處理太麻煩而塞車的-阻塞CPU(blocking CPU)

但是,如果今天賣當當規定,櫃檯要先與顧客打五場任天堂明星大亂鬥才能點餐,那櫃檯肯定會因這五場遊戲而「卡住」了所有事情,其他顧客點餐要等很久,而大廚在廚房也沒事做,因為櫃檯打遊戲太拖台錢了。

櫃檯沒辦法分身(多執行緒)去應付點餐,所以我們才會說「Node.js不適合CPU-bound(CPU密集型)操作」。

大致上理解一點了,但有沒有一些範例?

我這邊準備了Node.js 兩個server與test script來解釋,

完整範例:Github link

Server分別為:

  1. Node.js server:主server
  2. Sleep server:模擬耗時API的server,對任何requests都會故意睡5秒再回response

首先是測試Node.js的強項非阻塞IO的部分,

我們用testIO.js發送多個requests至主server,
而主server再去向Sleep server發出請求,此動作即是非同步的。

// server.js
async function request () {
  console.log("let't go")
  await rp('http://localhost:9999/sleep')
  console.log('done')
}

app.get('/io', async function (req, res) {
  await request()
  res.send('Hello World!');
})
// testIO.js
async function request () {
    console.log("let't go")
    await rp('http://localhost:3000/io')
    console.log('done')
}

(async () => {
    await Promise.all([
        request(),
        request(),
        request(),
        request(),
        request(),
        request(),
    ])
})()

在測試腳本執行下去之後會得到此結果:

腳本的全部requests發出後,會在約五秒後,一次把responses都拿回來,在這五秒到底發生了什麼事呢?

我們用時序圖來看看

我們可以發現,在藍箭頭處,主server並沒有停留在等待SleepServer的回應,而是緊接著處理下一個request,

假如主server沒有非阻塞IO的特性,會等待SleepServer回應才處理下個request,就會變成下圖

處理的時間會直接變成三倍,因為主server與SleepServer溝通時都在等待SleepServer,完完全全的被「阻塞」了,

這就是非阻塞IO與阻塞IO實務上的狀況


再來是測試Node.js的會阻塞CPU的部分,

testCPU.js也發送多個requests至主server,
而主server上面是重複計算x++十萬次並打印出他,

// server.js
async function request () {
  console.log("let't go")
  await rp('http://localhost:9999/sleep')
  console.log('done')
}

app.get('/cpu', function (req, res) {
    let x = 0
    while (x <= 100000){
        console.log(x)
        x++
    }
    res.send('Hello World!');
});
// testCPU.js
async function request () {
    console.log("let't go")
    await rp(`http://localhost:${host}/cpu`)
    console.log('done')
}

(async () => {
    await Promise.all([
        request(),
        request(),
        request(),
        request(),
        request(),
        request(),
    ])
})()

執行起來會發現,done是一個一個出現的,主server每打印完十萬次才會回應給腳本,

我們來看看時序圖到底發生了什麼事

可以看到主server因為要處理這十萬次的x++,導致第二個request沒辦法馬上被處理到,凡事像這樣的演算,都是靠CPU而不是IO操作,所以沒辦法透過Event loop來解決,由於Node.js只有單執行緒,就會被這演算「阻塞」住,

這就是阻塞CPU實務上的狀況。

Damn,那Node.js是不是弱掉了

這是與一些朋友討論時,有些不同語言的人滿喜歡提的點,但我認為,其實沒有所謂的「誰弱誰強」,只是此類語言特性「適合」用在什麼情境。

CPU處理是單執行緒,而IO上是多執行緒,幫助了Node.js開法者可以不用管理thread pool就可以擁有多IO的功能,而一定程度上程度也解決了race condition的問題。

什麼事race condition?接幾週會再繼續深入介紹,但簡單來說,就是

如果多個執行緒對同一個進行演算,同時存取某變數,造成存取值不正常

更白話的來說,就是當三個人要同時吃一顆蘋果時,三人如果同時伸手去拿,結果發生靈異事件,三個人都拿到,並且咬了一口放回去,此時放回去的蘋果到底長怎樣...這是什麼巫術ヘ(;´Д`ヘ)

而Node.js在演算任何事情時,都只有單執行緒一個人,所以Node.js「演算上」不會發生這樣的靈異事件,

不過,「其他周邊IO可能會發生」,因為Event loop通知的thread pool是多執行緒,這些廚房大廚們還是可能發生race condition的,比如同時讀取一個文件,就有可能會有存取錯誤的問題。

但實際上這些race condtion的可能性並不出在Node.js「本身的演算上」,開發上的難度還是減少許多。

那Golang呢?他適合CPU-bound嗎

適合!接下幾篇就來介紹介紹他ヽ(`◇´)/


如有錯誤歡迎勘正指教,謝謝你的閱讀~


1 則留言

0
imacbelong
iT邦新手 5 級 ‧ 2020-03-17 18:36:47

1.想請問NodeJS如果使用pm2 start app.js -i max去運行的話,這樣不是等於在每個thread上各運行一個process,請求會分別負載平衡導向至每個thread上執行,那這樣與golang使用全部thread的效能會有什麼差別嗎?

2.另外想請問 如果在server.js中的迴圈部分裡的console.log拿掉,感覺不出有CPU-bound的問題,是不是不要加console.log就不會有CPU-bound的問題呢?

我要留言

立即登入留言