Nodejs 之運行機制原理

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

内容简介: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 之運行機制原理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

Python基础教程

Python基础教程

[挪] Magnus Lie Hetland / 袁国忠 / 人民邮电出版 / 2018-2-1 / CNY 99.00

本书包括Python程序设计的方方面面:首先从Python的安装开始,随后介绍了Python的基础知识和基本概念,包括列表、元组、字符串、字典以及各种语句;然后循序渐进地介绍了一些相对高级的主题,包括抽象、异常、魔法方法、属性、迭代器;此后探讨了如何将Python与数据库、网络、C语言等工具结合使用,从而发挥出Python的强大功能,同时介绍了Python程序测试、打包、发布等知识;最后,作者结合......一起来看看 《Python基础教程》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具