Coding 應當是一輩子的事業,而不只僅是 30 歲的青春飯
本文已收錄 Github https://github.com/ponkans/F2E,歡迎 Star,持續更新前端
字節跳動面試官問:Node.js 多進程模型,以及多進程監聽同一端口的底層原理是如何實現滴?node
好朋友被字節跳動面試官這道題吊打了, 週末怪怪加班,寫下這篇深刻探究 Node.js 多進程架構的底層實現~ 純乾貨,分享給你們!!!python
不少小夥伴對一些基礎,特別是底層不是很瞭解,順帶也能夠好好補一下底層原理的基礎哈 ~react
每篇文章都但願你能收穫到東西,這篇由淺入深講 Node.js 多進程模型(後面會有些底層,小夥伴們作好心理準備哦~),但願看完可以有這些收穫:git
以前的《吊打面試官》系列 Node.js 雙十一秒殺系統中提了一下 Node 的多進程模型,本文將詳細的講解 Node 進程的各個細節github
進程和線程,能夠說是老僧長談的話題了。面試
只要是從事計算機相關的小夥伴,提起這個大都思如泉涌,多線程~高併發~ 但各類零散的概念和認知或許難以匯成一個成體系的知識結構。咱們先來羅列一下這兩個概念簡潔的官方解釋。windows
看到上面兩個定義,不少小夥伴小眉頭可能會皺一下,啥@#¥%玩意。。怪怪給小夥伴們準備了下圖幫助理解哈~。
後端
進程其實遍及在咱們電腦的每一個角落,剛剛被對面團滅的英雄聯盟,瀏覽器上正在播放的小電影等等,都是一個個運行中的進程。數組
進程實際上是處於執行期的程序和相關資源的總稱,裏面包含了要執行的代碼段,須要用到的文件,端口,硬件資源,很常見的一種說法是進程是資源分配的最小單位,這句話更直白的說就是,要運行某個可執行的代碼段會須要某些資源,當這個代碼段運行起來的時候,這些資源也必須被分配給他。
那咱們總結下就是:運行中的代碼+他佔有的資源 = 進程。
講完進程後,有些小夥伴可能懵了。
進程=運行的代碼段+資源,那咱們的線程存在的意義在哪?爲何不直接讓進程去運行。
上面咱們提到了,進程是資源分配的最小單位,意味着進程和資源是1:1,與之對應的一句話就是,線程是調度的最小單位,進程和線程是一個1:n的關係。
舉個不徹底恰當的栗子:咱們把一家商場比作一臺計算機,裏面一個一個的店家就是進程,他們是商場資源的最小單位了,他們既有對應的資源,也在進行着商業活動,就如同一個有資源和在運行中的進程。
每一個商鋪裏面的店員就是一個個線程,他們在本身的資源裏各司其職,有人拉客,有人站臺,有人把風。這些人才是真正調度的最小單位。
咱們試想,若是資源的分配和調度是 1:1 的關係,那意味着一個商店裏在活動的人同一時間只能有一個,當你在拉客的時候,其餘人不能夠在店裏,你在站臺的時候,其餘人也只能在一邊候着,但其實大家都是用的同一家店鋪的資源。
這顯然不 OK,因此進程同理,在進程中使用多線程就是讓共享同一批資源的操做一塊兒進行。
這樣能夠極大的減小進程資源切換的開銷。當咱們在進行多個操做的時候,他們相互之間在切換時天然是越輕量越好。
就像玩手機的時候,你在刷微博,這時你突然又想玩遊戲,當你在這兩個操做之間切換的時候,天然是越輕越好,你無需把手關機再重啓,而後再打開遊戲吧,否則這手機也太弱雞了吧~~
既然說到了進程的切換,那咱們能夠細探一下進程切換的開銷。一個進程會獨佔一批資源,好比使用寄存器,內存,文件等。
當切換的時候,首先會保存現場,將一系列執行的中間結果保存起來,存放在內存中的程序的代碼和數據,它的棧、通用目的寄存器的內容、程序計數器、環境變量以及打開的文件描述符的集合,這個狀態叫作上下文。
而後在他恢復回來的時候又須要將上述資源切換回去。顯而易見,切換的時候須要保存的資源越少,系統性能就會越好,線程存在的意義就在於此。線程有本身的上下文,包括惟一的整數線程 ID,棧、棧指針、程序計數器、通用目的寄存器和條件碼。
能夠理解爲線程上下文是進程上下文的子集。
程序的編寫老是追求最極致的性能優化,線程的出現讓共享同一批資源的程序在切換時更輕量,那有沒有比線程還要輕的呢?
協程的出現讓這個變成了可能,線程和進程是操做系統的支持帶來的優化,而協程本質上是一種應用層面的優化了。
這就如同線程和進程是天生的遊戲奇才,超神玩家,協程是這位奇才以爲本身超神不夠還想超鬼,是本身又作了後天努力。
協程能夠理解爲特殊的函數,這個函數能夠在某個地方掛起,而且能夠從新在掛起處外繼續運行,簡單來講,一個線程內能夠由多個這樣的特殊函數在運行,可是有一點必須明確的是,一個線程的多個協程的運行是串行的。
(圈重點啦)若是是多核 CPU,多個進程或一個進程內的多個線程是能夠並行運行的,可是一個線程內協程卻絕對是串行的,不管 CPU 有多少個核。
畢竟協程雖然是一個特殊的函數,但仍然是一個函數。一個線程內能夠運行多個函數,但這些函數都是串行運行的。當一個協程運行時,其它協程必須掛起。
協程通常來自語言的支持,如 Python,下面隨意貼一段協程的 py 代碼。裏面作的事情也很簡單,yield 是 python 當中的語法。
當函數執行到 yield 關鍵字時,會暫停在那一行(並不是阻塞,只是應用層面的暫停),等到主線程調用 send 方法發送了數據,協程纔會接到數據繼續執行,我的感受跟回調比較像。(Python yield 這個語法比較老舊,新語法使用 async/await)
下面是運行結果。
Linux 的設計總讓人有種化繁爲簡的感受,除了你們熟悉的一切皆文件,他對進程線程的設計也是相似的感受,嚴格來講在 Linux 上並無線程的概念,此話怎麼說?
由於不管是進程仍是線程,都要有存在的證實,你說你在世界上存在,你怎麼證實呢?
進程的存在證實就是進程控制塊,Linux 每個進程都有其對應的控制塊,裏面包含了進程 id,須要的硬件資源,執行的代碼段等等。線程亦如是,在 windows 中有明確的線程控制塊,由操做系統來作線程調度。
Linux 視線程和進程是同樣的,都用進程控制塊進行管控,但這並不等於 Linux 不支持線程,只是不一樣操做系統對概念的抽象不一樣,Linux 提供 pthread 庫來 fork 微進程,多個微進程能夠共享資源,和線程本質上並沒有區別,只是沒有提供專門的線程管控,有興趣的同窗能夠詳細瞭解下。
哦豁,是否是感受怪怪有點東西了?彆着急,接續往下看↓~
要成爲 nb 的業界大手,你要會哪些技能?
面試扛千億併發,入職調按鈕樣式,哈哈哈。
這裏有個概念是併發,與之爛兄爛弟的概念就是並行,讓人意亂神迷傻傻分不清。
如今咱們常常會聽到各類名詞,什麼多核機器,多 cpu 什麼的。多個 cpu 意味着什麼呢?
首先要搞清楚 cpu 究竟是幹嗎的。cpu 的做用用兩個字來說就是:計算。
咱們的各類花裏胡哨的代碼,最終編譯完真正執行的時候也無非這兩個字:計算。上面提到了進程必定是在運行的代碼,那代碼的運行必然就是在 CPU 上。
咱們有幾個 cpu 意味着咱們能夠有幾個程序同時在計算,這就是並行,就如同小時候會想有鳴人的影分身,就可讓他們一個來寫數學,一個來寫語文,一個來寫英語。
與多核對應的就是苦逼的單核今計算機了,就像沒有影分身的我,這個時候也有多個做業要作,咋整?半個小時寫語文,半個小時寫數學,再半個小時寫語文,再來半小時寫數學。。(強行時間片輪轉了)這是語文數學英語也都同時寫了,但實際上只有我苦逼的一我的,這就是分時併發,但非並行。
總結下就是並行必定併發,併發未必並行。
關於 cpu 調度進程的策略,cpu 執行代碼的細節,若是有興趣能夠留言,後續有時間能夠安排,這裏就不展開了
學習 Node 的第一天就看到過 Node 是個單進程單線程模型,他線程安全。嗯確實是線程安全。。但在後端同窗看來就如同一個單身狗在說我是不會迷失在愛情裏的,廢話由於你原本就沒有。。
如咱們上面所講,單線程再怎麼秀,也只能在一個 cpu 上花裏胡哨,對於咱們要對標全棧的 Node 必然是不能接受。
既然一個 Node 進程只能有一個線程,那想經過單進程多線程的姿式來壓榨 cpu(相似於 Java)應該是黃了,但 Node 支持多進程模型。
Node 提供了 child_process 模塊,經過 child_process.fork()函數來進行進程的複製。
以下圖,master 調用 child_process.fork 進程,被 fork 出的進程爲 worker。
child_process 模塊給予 Node 建立子進程的能力,父進程與子進程之間是一種 master/worker 的工做模式。
這種模式在分佈式系統中隨處可見,但高手老是能撒豆成兵,Node 在單機上對父子進程採用了這種管理模式,這種模式很像經典的 reactor 模式(只是 reactor 是主線程),利用父進程來作主進程,而且將任務 dispatch 到 worker 進程。
一般會阻塞的操做分發給 worker 來執行(查 db,讀文件,進程耗時的計算等等),master 上儘可能編寫非阻塞的代碼。
既然提到了主從進程,那避免不了的一個問題就是他們之間的通訊。
進程通訊的姿式不少,例如基於 socket,基於管道,基於 mmap 內存映射等等,這裏咱們主要討論Node 的通訊,這裏和你們先簡單的講解兩個概念:文件描述符、管道。
文件描述符是操做系統用來作文件管理的一個概念,如上圖所示,每一個進程會有一個本身的文件描述符表,裏面包含了文件描述符標誌和文件指針,每一個進程本身的表都是從 0 開始,而後由文件指針來指向同一個系統級的打開文件表,打開文件表裏面會記錄文件偏移量(這個文件被讀寫到了哪一個位置)、inode 指針。
再由 inode 指針來指向系統級的 inode 表,inode 表就是真正維護操做系統文件自己的一個實體了,裏面包含了文件類型,大小,create time 等等~
其實系統中的文件描述符不必定是指向一個磁盤文件,也能夠能是指向一個網絡的 socket 這種,站在Linux的角度上來講,操做系統把一切都抽象爲文件,網絡數據,磁盤數據等等,都是用文件描述符來作維護。
講了文件描述符,咱們能夠大體感知到進程要讀東西,必定須要一個媒介,那咱們父子進程之間的通訊也必定須要一個介質來通訊。
接下來咱們拋出管道的概念,如同其名字,管道必定是用來連通兩個東西的,就像家裏的水管,一個入口,一個出口。
咱們來分析一下兩個進程是如何創建起來通訊的。
以前提到了進程會有本身的文件描述符表,咱們在 fork 進程的時候父進程也會把本身的文件描述符拷貝給子進程。咱們來看一段比較拙劣的 C 代碼。(還記得大學剛開始學 C 時,指針帶給你的困擾嘛)
咱們分析一下上面代碼,小夥伴們沒必要在乎 C 的語法哈~,只需關注管道的創建過程
咱們一開始調用 pipe(fd),傳人的是一個 size 是 2 的空數組,若是建立成功,這個數組的 fd[0]就是讀所用的文件描述符,fd[1]就是寫所用的文件描述符。
這個時候,咱們在當前進程調用 vfork(),create 出一個子進程,父子進程都持有這個 fd[]。
若是咱們判斷是子進程,就關閉他的讀文件描述符,若是是父進程,就關閉他的寫文件描述符。
這時,以下圖所示,咱們會實現一個單向通訊,操做系統調用 pipe(建立管道)的時候,會新建一片內存空間,這片內存專用與兩個進程通訊,這應證了咱們上面所說的,系統會把不少東西抽象成文件,好比這裏就是把那一片共用內存抽象了起來,以後子進程經過 fd[1],往那片內存區域寫入數據,父進程經過 fd[0]來讀,這裏就實現了一個單工通訊。
或許上面講的有點晦澀,咱們來舉一個不徹底恰當的栗子,你住長江頭,妹子住長江尾,河流就像大家之間的管道,你想跟她之間有所交流咋整?只需寫一封信,順着江流流下去(write),她在那邊接收就行(read)。大家之間就是一個單向的管道通訊。
但單向確定是不行的,如何實現一個雙工通訊呢,很簡單,用兩個管道就 OK 了。
若是上面的解釋還沒看懂,請結合下面的圖,再去理解一下,或者加羣@接水怪,爲你提供一對一私人服務!!!
接下來咱們回到最初的起點,Node 之間的進程如何通訊,其實也不過如此。Node 本身抽象了一個 libuv 的概念,根據不一樣操做系統有不一樣的底層實現,咱們上面講到的雙工管道通訊就是其中一種。
要真正理解服務端爲什麼能承受高併發,理解當前服務架構的核心,須要從網絡到操做系統的每個細節進行理解。
上面聊了一系列比較晦澀的裝逼話題,接下來咱們聊點相對實際的。咱們寫出來服務端是爲了什麼?
目的天然是讓別人來調用,想一想咱們平時調用服務的方式,最簡單的就是咱們的 http,用瀏覽器發起小電影請求,小電影服務端接收到並返回結果,而後開始一個個不眠的夜晚。
咱們的請求本質就是去訪問小電影服務器,服務器對應的端口收到了請求而後作相應處理而且返回結果。看小電影最不能接受的就是卡頓,好比說看建黨偉業的時候,在下由於在聽xxx宣言的時候卡住了捶胸頓足了很久,hhhh~~。
那服務端如何能不卡?上面咱們的多進程如何用起來?
上圖是一種能夠實現的架構,由 master 監聽默認的 80 端口,用戶的請求都打在 80 上,其餘子進程監聽一個別的端口,當父進程收到後往子進程監聽的端口寫數據,子進程來作處理。
這裏看似能夠實現,實則浪費了太多文件描述符,上面講到了每一個進程都有文件描述符表,而每一個 socket 的讀寫也是基於文件描述符,操做系統的文件描述符是有限的,這樣的設計顯然不夠優雅,拓展性不強。
這個時候有小夥伴會問,爲何不直接讓每一個進程都去監聽 80,幹嗎還要轉一次。這個思路很 OK。
But,最終會發現直接的監聽最後只會有一個進程搶佔端口成功,其餘進程會拋出端口被佔用的異常。爲了解決這個問題,Node 用了另一種架構模式。以下圖。
一開始依然是 master 進程監聽 80,當收到用戶請求以後,master 並非直接把這些數據扔給 worker,而是在 80 端口接收到數據後,生成對應的 socket,再把該 socket 對應的文件描述符經過管道傳給 worker,一個 socket 意味着服務端和客戶端的一個數據通道,也就意味着 master 把跟客戶端的數據通道傳給了 worker。
以下圖,在以後 master 中止監聽 80port,由於已經把文件描述符給了 worker,以後 worker 直接監聽這個套接字便可。
因而就有了下面那種模式,多個 worker 直接監聽同一個 port。
這個時候小夥伴們可能很疑惑,爲啥這個時候不會端口衝突??
這裏的關鍵在於兩個點。
第一個是,Node 對每一個端口監聽設置了SO_REUSEADRR,標示能夠容許這個端口被多個進程監聽。
第二個點是,用這個的前提是每一個監聽這個端口的進程,監聽的文件描述符要相同。
以前講文件描述符的時候提到過,文件描述符表是每一個進程私有的,相互之間不可見,那對這個端口他們也會有各自的文件描述符,這樣就沒法利用 SO_REUSEADRR 的特性。
那爲何經過 master 傳給 worker 就能夠了呢?
由於 master 在與 worker 通訊的時候,每一個子進程收到的文件描述符都是同樣的(經過 master 傳入,不理解的參見上面雙工通訊的講解),這個時候就是全部子進程監聽相同的 socket 文件描述符,就能夠實現多個進程監聽同一個端口的目標啦~。
本文已收錄 Github https://github.com/ponkans/F2E,歡迎 Star,持續更新💧
Node 利用 master/worker 模式來利用多核資源,利用 SO_REUSEADRR 與句柄(文件描述符)傳遞來使多個進程同時監聽同一個端口,提升吞吐量。
對進程、線程、cpu 有認知是最基本的,這樣寫項目才能對本身的每一行代碼瞭然於心。
本文僅算是入門貼,真正的 Node 內核有待你們一一深刻學習,若是對某一塊有特別的興趣能夠在下面留言,直接加羣來討論,怪怪我等你!~
近期會針對 Node.js 寫一個系列,同系列傳送門:
喜歡的小夥伴加個關注,點個贊哦,感恩💕😊
微信搜索【接水怪】或掃描下面二維碼回覆」加羣「,我會拉你進技術交流羣。講真的,在這個羣,哪怕您不說話,光看聊天記錄也是一種成長。(阿里技術專家、敖丙做者、Java3y、蘑菇街資深前端、螞蟻金服安全專家、各路大牛都在)。
接水怪也會按期原創,按期跟小夥伴進行經驗交流或幫忙看簡歷。加關注,不迷路,有機會一塊兒跑個步🏃 ↓↓↓