内容简介: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 來處,
但為什麼呢 ?
最主要的原因在於,每當建立一個連線前,一定要將 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) 模式,他想完成的事情如下。
而實作 NIO 的設計模式有以下兩個 :
- Reactor
- proactor
我們此篇的主軸為 Reactor
,主要的原因為 Nodejs 主要就是使用 Reactor 來建立它的架構。( proactor 會開另一篇 )
它主要的概念圖就是咱們所謂的 Event loop 機制,如下圖 :
簡單的說它有一個 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 的基本組合與運行如下。
其中 libuv 就是實現非阻塞 I/O 的核心庫,它讓我們可以跨平台的實現非阻塞 I/O。
根據上圖如果有一段程式碼執行下去,那它的運行流程如下 :
- 使用 V8 引擎解析 Javascript 語法。
- 解析後呼叫對應對 Node C++ 程式碼。
- 將所有同步的程式碼運行完。
- libuv 建立起 Event loop 並且不斷的去輪詢 Event Queue 來執行那些可以呼叫的異步操作 callback。
我們簡單的看一下範例
假設我們有一段程式碼如下 :
console.log('Hi'); setTimeout(() => console.log('fuck u'), 0); console.log('Mark');
它的運行流程為:
- 執行同步程式碼 console.log('Hi')。
- 將 setTimeout 事件與 callback 丟到 Event Queue 中。
- 執行同步程式碼 console.log('Mark')。
- 開啟執行 Event loop。
- 發現 Event Queue 中有需要執行的 time 事件,執行 console.log('fuck u')。
所以最後輸出的結果為 :
// Hi // Mark // fuck u
這裡問個問題 ~ Nodejs 是單進程的架構,可是為什麼架構圖中最後有 worker thread 的東西呢 ?
這就是我們接下來要章節要說明的東西。
Nodejs 為什麼需要使用 thread ?
根據 官方文件 ,可以知道,有以下幾個東西需要使用 thread 來處理。
- CPU-intensive : Crypto 、 Zlib 。
- I/O-intensive : DNS 、 FileSystem 。
首先來說說 cpu 密集的這兩個 Crypto
與 Zlib
加密與壓縮套件,為什麼需要丟到 thread 做呢 ? 主要的原因為,基本上正常的運算都是屬於同步程式碼,也就是說會在 event loop 前執行,如果這時運算太花時間,那就代表他會卡住 event loop 運行,那如果是 callback 的呢 ? 它應該是在 event loop 內執行吧 ? 嗯沒錯,但問題就是,當它執行時,它就會卡 event loop,不要忘了 event loop 只是一個 while,然裡面執行的東西還是同步。
那接下來看 I/O-intensive 的 DNS
與 FileSystem
。
我們先來說說 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 之運行機制原理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- java反射原理, 注解原理
- Webpack 原理(二):加载原理
- Docker原理之 - CGroup实现原理
- 【Vue原理】响应式原理 - 白话版
- Docker实现原理之 - OverlayFS实现原理
- UAV MOF工作原理之Agent注入机制原理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Python基础教程
[挪] Magnus Lie Hetland / 袁国忠 / 人民邮电出版 / 2018-2-1 / CNY 99.00
本书包括Python程序设计的方方面面:首先从Python的安装开始,随后介绍了Python的基础知识和基本概念,包括列表、元组、字符串、字典以及各种语句;然后循序渐进地介绍了一些相对高级的主题,包括抽象、异常、魔法方法、属性、迭代器;此后探讨了如何将Python与数据库、网络、C语言等工具结合使用,从而发挥出Python的强大功能,同时介绍了Python程序测试、打包、发布等知识;最后,作者结合......一起来看看 《Python基础教程》 这本书的介绍吧!