Nodejs 之運行機制原理

栏目: Node.js · 发布时间: 6年前

内容简介:Nodejs 出來時它的官網寫這以下的描述 :簡而言之 Nodejs 是運行在 V8 javascript 引擎,並且使用 Event driven 與 non-blocking I/O 模式所建立出來的東東。而這裡我們就要深入的來理解 Nodejs 的運行機制。

Nodejs 出來時它的官網寫這以下的描述 :

Node.js is a JavaScript runtime built on Chrome’s V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js’ package ecosystem, npm, is the largest ecosystem of open source libraries in the world.

簡而言之 Nodejs 是運行在 V8 javascript 引擎,並且使用 Event driven 與 non-blocking I/O 模式所建立出來的東東。

而這裡我們就要深入的來理解 Nodejs 的運行機制。

  • Nodejs 核心設計 - 非阻塞 I/O 模式
  • Nodejs 架構與運行
  • Nodejs 為什麼需要使用 thread ?

Nodejs 核心設計 - 非阻塞 I/O 模式

上面介紹文有提到,它是以 非阻塞 non-blocking I/O 模式所建立出來的,但為什麼它要選用非阻塞 I/O 呢 ?

它想解決什麼問題呢 ?

它最原始想解決的就是 :

阻塞 I/O 模式在大併發請求下的貧頸。

首先我們先來看看傳統的阻塞 blocking I/O 模式問題

阻塞 I/O (Blocking I/O BIO)伺服器的運行

首先我們先說明一下 I/O 這東西,I/O 是指輸入與輸出,只要是與外部記憶體或設置的溝通都算是 I/O 操作,像進行 http 請求或是讀檔案這種,都算是 I/O 操作。

而所謂的 阻塞 I/O 就是指,當執行 I/O 的操作會阻塞,也就是直到操作完成後,才會執行下一段指令,更準備的說法是, 阻塞就是指這個 thread 或 process 無法處理其它事情 ,就算它 CPU 閒閒的也是一樣。

下面為一段模擬碼,process 或 thread 會在執行完 socket.read() 取得完資料後,才會執行下一段,這就是阻塞 I/O,而大部份的 I/O 在沒有特別處理的話,都是阻塞 I/O。

// 直到讀取完資料後,才會執行下一段。
var data = socket.read();

console.log(data);

而傳統阻塞 I/O 伺服器在收到一個 http(I/O) 後,它的運行模式下圖,每一個 http 會開啟一個 thread 或 process 來處,

Nodejs 之運行機制原理

但為什麼呢 ?

最主要的原因在於,每當建立一個連線前,一定要將 process 進行阻塞來接收 socket 事件。也就是如下程式碼一樣,這一段在建立連線前,就需要先執行,而等到要資料進來時,才會往下做。

request = socket.read(); // 這裡會阻塞住
doSomething(request); //有資料時才執行

所以如果我們只有一個 process 來處理,當你一執行這段上面這段讀取請求的程式碼後以後,它就會一直停在 socket.read() 阻塞等待,其它的請求都會進不來,就因為卡在這裡。

這也因此大部份的傳統阻塞 I/O 伺服器在收到一個 Http 請求後,都會開啟一個 thread 或 process 來進行處理,因為這樣就不會卡住了。

阻塞 I/O 伺服器的問題

上面有提到傳統的阻塞 I/O 伺服器每收到一個 http 請求就會開啟一個 thread 來運行。

這就是問題。

因為 thread 是很貴的資源

主要有以下的原因 :

  • linux 系統下 thread 本質就是 process 創建和銷毀都非常的耗成本。
  • 它會暫用不少的 memory。
  • thread 的切換上下文成本很高。

所以當如果連線數來個十萬或上百萬的,那麼阻塞式 I/O 一定會倒給你看。

而這也是為什麼 Nodejs 要以 非阻塞 I/O (Non-Blocking) 為核心來進行設計。

非阻塞 I/O 模式 (Non-Blocking I/O NIO)

由於阻塞 I/O 有以下所提到的問題,因此後來就發展出所謂的非阻塞 I/O 模式 (Non-Blocking I/O NIO) 模式,他想完成的事情如下。

Nodejs 之運行機制原理

而實作 NIO 的設計模式有以下兩個 :

  • Reactor
  • proactor

我們此篇的主軸為 Reactor ,主要的原因為 Nodejs 主要就是使用 Reactor 來建立它的架構。( proactor 會開另一篇 )

它主要的概念圖就是咱們所謂的 Event loop 機制,如下圖 :

Nodejs 之運行機制原理

簡單的說它有一個 Event loop 會一直不斷的去 Event Queue 中 check 是否有 I/O 事件,如果有的話就將它丟到指定的 handler 去,如下概念碼。

while(true){

    events = sockets.fetchIOEvents();
    
    for (var i=0; i < events.length; i++){
        if(events[0].type === 'write'){
            writeHandler(event[0].data);
        }
        
        if(events[0].type === 'read'){
            readHandler(event[0].data);
        }
        
        if(events[0].type === 'accept'){
            acceptHandler(event[0].dadta);
        }
    }
}

但這裡有問題想問問。

為什麼可以取得到 I/O 事件呢 ?

上面不是有提到要從 socket 中取得資料,需要使用 linux 底層的 socket.read() 阻塞方法,那為什麼概念碼的 sockets.fetchIOEvents() 可以取得呢 ?

主要的答案在於的各系統的下列方法 :

epoll(linux)、kqueue(Mac)、IOCP(Window)

這幾個方法的功用就是,它們可以幫我們監控 I/O 操作,當 socket 有事件產生時(ex. 有資料進來時)會自動的將它相關資料推送到一個 event queue 中,然後你可以使用它提供的方法,來取得事件相關資料。

我們這裡以 epoll 來說明它的使用方法,基本上它提供三個方法。

int epoll_create(int size); 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

首先第一個 epoll_create 就是建立一個 epoll 對像,然後它的 size 就是 kernal 保証可以監控的最大 file descitpor 數。

再來是 epoll_ctl ,它就是將 socket 加入到 epoll 的監控中,當 socket 有產生什麼事件時,它可以從 epoll_wait 中取得到事件資訊。

最後是 epoll_wait ,它在給定的 timeout 時間內,如果監控的 socket有產生事件,則會返回用戶狀態。

下面為我們使用它的範例碼,這個範例的功用就是讓 epoll 監聽你有註冊的 socket,然後當有事件產生時,就可能從 epoll_wait 取得相對應的 socket 與事件,最後再將此事件執行到對應的 event handler。

struct events[10];

// 建立一個 epoll 用 file descriptor
epollfd = epoll_create();

// 註冊讓 epoll 監聽某 socket 的 EPOLLIN 事件
epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);

while(true){

    // 如果 epoll queue 中有事件產生,則會回傳產生事件的 socket 與 events。
    have_events_fds = epoll_wait( epollfd, events, MAX_EVENTS, -1 );

    // 讀取每個有產生事件的 socket,並執行對應的 eventHandler。
    for(int i=0; i < have_events_fds; i++){
        eventHandler(events[n]);
    }
}

上面這些就是最基本的 NIO 的 Reactor 架構基本概念,接下來我們將要來看看 Nodejs 中的 Reactor 架構。

阻塞 I/O 與非阻塞 I/O 的比較圖

這章節最後,我們來看看阻塞 I/O 與非阻塞 I/O 的比較圖。

阻塞 : 監控 socket,然後就開始卡住整個 thread,直到有資料進來後才結束。

非阻塞 : 不斷的去問 socket 有沒有資料。

Nodejs 之運行機制原理

Nodejs 架構與基本運行

Nodejs 的基本組合與運行如下。

Nodejs 之運行機制原理

其中 libuv 就是實現非阻塞 I/O 的核心庫,它讓我們可以跨平台的實現非阻塞 I/O。

Nodejs 之運行機制原理

根據上圖如果有一段程式碼執行下去,那它的運行流程如下 :

  1. 使用 V8 引擎解析 Javascript 語法。
  2. 解析後呼叫對應對 Node C++ 程式碼。
  3. 將所有同步的程式碼運行完。
  4. libuv 建立起 Event loop 並且不斷的去輪詢 Event Queue 來執行那些可以呼叫的異步操作 callback。

我們簡單的看一下範例

假設我們有一段程式碼如下 :

console.log('Hi');

setTimeout(() => console.log('fuck u'), 0);

console.log('Mark');

它的運行流程為:

  1. 執行同步程式碼 console.log('Hi')。
  2. 將 setTimeout 事件與 callback 丟到 Event Queue 中。
  3. 執行同步程式碼 console.log('Mark')。
  4. 開啟執行 Event loop。
  5. 發現 Event Queue 中有需要執行的 time 事件,執行 console.log('fuck u')。

所以最後輸出的結果為 :

// Hi

// Mark

// fuck u

這裡問個問題 ~ Nodejs 是單進程的架構,可是為什麼架構圖中最後有 worker thread 的東西呢 ?

這就是我們接下來要章節要說明的東西。

Nodejs 為什麼需要使用 thread ?

根據 官方文件 ,可以知道,有以下幾個東西需要使用 thread 來處理。

首先來說說 cpu 密集的這兩個 CryptoZlib 加密與壓縮套件,為什麼需要丟到 thread 做呢 ? 主要的原因為,基本上正常的運算都是屬於同步程式碼,也就是說會在 event loop 前執行,如果這時運算太花時間,那就代表他會卡住 event loop 運行,那如果是 callback 的呢 ? 它應該是在 event loop 內執行吧 ? 嗯沒錯,但問題就是,當它執行時,它就會卡 event loop,不要忘了 event loop 只是一個 while,然裡面執行的東西還是同步。

那接下來看 I/O-intensive 的 DNSFileSystem

我們先來說說 FileSystem 的情況。

首先基本上 I/O 阻塞來源為 :

I/O 阻塞來源 = Network I/O + File I/O

那為什麼在 Nodejs 中,可以做到非阻塞呢 ?

因為他們兩個都有解法可處理。

libuv 解決 I/O 阻塞方法 = Network I/O (epoll) + File I/O (thread)

Network I/O 可以用 epoll 來處理

但重點是為什麼 File I/O 無法用 epoll 來處理呢 ?

目前筆者只知道,如果你將檔案的 file descriptor 註冊到 epoll 中,會發生以下的錯誤:

EPERM The target file fd does not support epoll.

簡單來說就是 epoll 不支援檔案類型的 file descriptor 監控。

這也是為什麼 Nodejs 實際上會偷偷的開幾個 worker thread 來處理 file system 這件事情。

file I/O 可以理解,但為什麼 dns 他不是 network I/O 嗎 ? 那為什麼它還需要 thread 來處理呢。

筆者覺得主要的原因在於,DNS 的操作事實上為 :

Network I/O + File I/O

這或需就是為什麼它需使用 thread 來完成非阻塞 I/O。

注意 nodejs 只能說架構是非阻塞,但不代表不會阻塞

我們有以下兩個假設:

  • Thread number: 3
  • test.txt 讀檔時間: 2 sec

那你執行以下的程式碼。

const fs = require('fs');

const start = process.hrtime();

for (var i = 1; i <= 3; i++) {
  ((id) => {
    fs.readdir('test.txt', () => {
      let end = process.hrtime(start);
      console.log(util.format('read file %d finished in %ds', id, end[0] + end[1] / 1e9));
    });
  })(i);
}

然後你就會發結果如下,實際上在讀三個檔案時,就已經阻塞了,原因在於我們只有三個 thread,它們都在忙錄,因此第四個才會需要花 4 秒才完成。

read file 1 finished in 2 sec
read file 2 finished in 2 sec
read file 3 finished in 2 sec
read file 4 finished in 4 sec

參考資料


以上所述就是小编给大家介绍的《Nodejs 之運行機制原理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Cyberwar

Cyberwar

Kathleen Hall Jamieson / Oxford University Press / 2018-10-3 / USD 16.96

The question of how Donald Trump won the 2016 election looms over his presidency. In particular, were the 78,000 voters who gave him an Electoral College victory affected by the Russian trolls and hac......一起来看看 《Cyberwar》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

MD5 加密
MD5 加密

MD5 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器