2017-05~06 溫故而知新--NodeJs書摘(一)

前言:

畢業到入職騰訊已經差很少一年的時光了,接觸了不少項目,也積累了不少實踐經驗,在處理問題的方式方法上有很大的提高。隨着時間的增長,越發發現基礎知識的重要性,不少開發過程當中遇到的問題都是由最基礎的知識點遺忘形成,基礎不牢,地動山搖。因此,就再次迴歸基礎知識,從新學習NodeJs相關內容,加深對NodeJs本質的理解。日知其所亡,身爲有追求的程序員,理應不斷學習,不斷拓展本身的知識邊界。本系列文章是在此階段產生的積累,以記錄下以往沒有關注的核心知識點,供後續查閱之用。前端

2017

05/27

Node保持了JavaScript在瀏覽器中單線程的特色。並且在Node中,JavaScript與其他線程是沒法共享任何狀態的。單線程的最大好處是不用像多線程編程那樣到處在乎狀態的同步問題,這裏沒有死鎖的存在,也沒有線程上下文交換所帶來的性能上的開銷。
一樣,單線程也有它自身的弱點,這些弱點是學習Node的過程當中必需要面對的。積極面對這些弱點,能夠享受到Node帶來的好處,也能避免潛在的問題,使其得以高效利用。單線程的弱點具體有如下3方面。
  • 沒法利用多核CPU。
  • 錯誤會引發整個應用退出,應用的健壯性值得考驗。
  • 大量計算佔用CPU致使沒法繼續調用異步I/O。
Node採用了與Web Workers相同的思路來解決單線程中大計算量的問題:child_process 。 子進程的出現,意味着Node能夠從容地應對單線程在健壯性和沒法利用多核CPU方面的問題。經過將計算分發到各個子進程,能夠將大量計算分解掉,而後再經過進程之間的事件消息來傳遞結果,這能夠很好地保持應用模型的簡單和低依賴。經過Master-Worker的管理方式,也能夠很好地管理各個工做進程,以達到更高的健壯性。

05/30

應用場景:
I/O密集型:I/O密集的優點主要在於Node利用事件循環的處理能力,而不是啓動每個線程爲每個請求服務,資源佔用極少。
CPU密集型:關於CPU密集型應用,Node的異步I/O已經解決了在單線程上CPU與I/O之間阻塞沒法重疊利用的問題,I/O阻塞形成的性能浪費遠比CPU的影響小。對於長時間運行的計算,若是它的耗時超過普通阻塞I/O的耗時,那麼應用場景就須要從新評估,由於這類計算比阻塞I/O還影響效率,甚至說就是一個純計算的場景,根本沒有I/O。此類應用場景或許應當採用多線程的方式進行計算。
與遺留系統和平共處  :在Web端,過去大多都是同步的方式編寫的程序,這種串行調用下層應用數據的過程當中充斥着串行的等待時間。 採用Node來完成Web端的開發,使得前端工程師在HTTP協議棧的兩端可以高效靈活地開發。並行I/O,有效利用穩定接口提高Web渲染能力。
分佈式應用 :Node高效利用並行I/O。

06/02

在Node中引入模塊,須要經歷以下3個步驟。
  • 路徑分析
  • 文件定位
  • 編譯執行
在Node中,模塊分爲兩類:一類是Node提供的模塊,稱爲核心模塊;另外一類是用戶編寫的模
塊,稱爲文件模塊。  
核心模塊部分在Node源代碼的編譯過程當中,編譯進了二進制執行文件。在Node進程啓動時,部分核心模塊就被直接加載進內存中,因此這部分核心模塊引入時,文件定位和編譯執行這兩個步驟能夠省略掉,而且在路徑分析中優先判斷,因此它的加載速度是最 快的。
文件模塊則是在運行時動態加載,須要完整的路徑分析、文件定位、編譯執行過程,速度比核心模塊慢。

06/04

JavaScript模塊的編譯 :在編譯的過程當中,Node對獲取的JavaScript文件內容進行了頭尾包裝。在頭部添加了(function (exports, require, module, __filename, __dirname) {\n,在尾部添加了\n});。 在執行以後,模塊的exports屬性被返回給了調用方。exports屬性上的任何方法和屬性均可以被外部調用到,可是模塊中的其他變量或屬性則不可直接被調用。
JavaScript的一個典型弱點就是位運算。JavaScript的位運算參照Java的位運算實現,可是Java位運算是在int型數字的基礎上進行的,而JavaScript中只有double型的數據類型,在進行位運算的過程當中,須要將double 型轉換爲 int型,而後再進行。因此,在JavaScript層面上作位運算的效率不高。

06/05

PHP對調用層不只屏蔽了異步,甚至連多線程都不提供。PHP語言從頭到腳都是以同步阻塞的方式來執行的。它的優勢十分明顯,利於程序員順序編寫業務邏輯;它的缺點在小規模站點中基本不存在,可是在複雜的網絡應用中,阻塞致使它沒法更好地併發。
伴隨着異步I/O的還有事件驅動和單線程,它們構成Node的基調,Ryan Dahl正是基於這幾個因素設計了Node。
與Node的事件驅動、異步I/O設計理念比較相近的一個知名產品爲Nginx。Nginx採用純C編寫,性能表現很是優異。它們的區別在於,Nginx具有面向客戶端管理鏈接的強大能力,可是它的背後依然受限於各類同步方式的編程語言。但Node倒是全方位的,既能夠做爲服務器端去處理客戶端帶來的大量併發請求,也能做爲客戶端向網絡中的各個應用進行併發請求。
I/O是昂貴的,分佈式I/O是更昂貴的 。  
Node在二者之間給出了它的方案:利用單線程,遠離多線程死鎖、狀態同步等問題;利用異步I/O,讓單線程遠離阻塞,以更好地使用CPU。
操做系統對計算機進行了抽象,將全部輸入輸出設備抽象爲文件。內核在進行文件I/O操做時,經過文件描述符進行管理,而文件描述符相似於應用程序與系統內核之間的憑證。應用程序若是須要進行I/O調用,須要先打開文件描述符,而後再根據文件描述符去實現文件的數據讀寫。此處非阻塞I/O與阻塞I/O的區別在於阻塞I/O完成整個獲取數據的過程,而非阻塞I/O則不帶數據直接返回,要獲取數據,還須要經過文件描述符再次讀取。 因爲完整的I/O並無完成,當即返回的並非業務層指望的數據,而僅僅是當前調用的狀態。爲了獲取完整的數據,應用程序須要重複調用I/O操做來確認是否完成。
epoll。該方案是Linux下效率最高的I/O事件通知機制,在進入輪詢的時候若是沒有檢查到I/O事件,將會進行休眠,直到事件發生將它喚醒。它是真實利用了事件通知、執行回調的方式,而不是遍歷查詢,因此不會浪費CPU,執行效率較高。 
另外一個須要強調的地方在於咱們時常提到Node是單線程的,這裏的單線程僅僅只是JavaScript執行在單線程中罷了。在Node中,不管是*nix仍是Windows平臺,內部完成I/O任務的另有線程池。  

06/06

請求對象是異步I/O過程當中的重要中間產物,全部的狀態都保存在這個對象中,包括送入線程 等待執行以及I/O操做完畢後的回調處理。 
事件循環、觀察者、請求對象、I/O線程池這四者共同構成了Node異步I/O模型的基本要素。 Windows下主要經過IOCP來向系統內核發送I/O調用和從內核獲取已完成的I/O操做,配以事件循環,以此完成異步I/O的過程。在Linux下經過epoll實現這個過程,FreeBSD下經過kqueue實現,Solaris下經過Event ports實現。不一樣的是線程池在Windows下由內核(IOCP)直接提供,*nix系列下由libuv自行實現。
每次調用 process.nextTick()方法,只會將回調函數放入隊列中,在下一輪Tick時取出執行。定時器中採用紅黑樹的操做時間複雜度爲O(lg(n)) , nextTick()的時間複雜度爲O(1)。相較之下,process.nextTick() 更高效。
process.nextTick()中的回調函數執行的優先級要高於setImmediate()。這裏的緣由在於事件循環對觀察者的檢查是有前後順序的,process.nextTick()屬於idle觀察者,setImmediate()屬於check觀察者。在每個輪循環檢查中,idle觀察者先於I/O觀察者,I/O觀察者先於check觀察者。

06/09

Node帶來的最大特性莫過於基於事件驅動的非阻塞I/O模型,這是它的靈魂所在。非阻塞I/O可使CPU與I/O並不相互依賴等待,讓資源獲得更好的利用。對於網絡應用而言,並行帶來的想象空間更大,延展而開的是分佈式和雲。並行使得各個單點之間可以更有效地組織起來,這也是Node在雲計算廠商中廣受青睞的緣由。  
流程控制:
事件發佈/訂閱模式相對算是一種較爲原始的方式,Promise/Deferred模式貢獻了一個很是不錯的異步任務模型的抽象。而上述的這些異步流程控制方案與Promise/Deferred模式的思路不一樣,Promise/Deferred的重頭在於封裝異步的調用部分,流程控制庫則顯得沒有模式,將處理重點放置在回調函數的注入上。從自由度上來說,async、Step這類流控庫要相對靈活得多。EventProxy庫則主要借鑑事件發佈/訂閱模式和流程控制庫經過高階函數生成回調函數的方式實現。  

06/10

在通常的後端開發語言中,在基本的內存使用上沒有什麼限制,然而在Node中經過JavaScript使用內存時就會發現只能使用部份內存(64位系統下約爲1.4 GB,32位系統下約爲0.7 GB)。至於V8爲什麼要限制堆的大小,表層緣由爲V8最初爲瀏覽器而設計,不太可能遇到用大量內存的場景。對於網頁來講,V8的限制值已經綽綽有餘。深層緣由是V8的垃圾回收機制的限制。按官方的說法,以1.5 GB的垃圾回收堆內存爲例,V8作一次小的垃圾回收須要50毫秒以上,作一次非增量式的垃圾回收甚至要1秒以上。這是垃圾回收中引發JavaScript線程暫停執行的時間,在這樣的時間花銷下,應用的性能和響應能力都會直線降低。這樣的狀況不只僅後端服務沒法接受,前端瀏覽器也沒法接受。所以,在當時的考慮下直接限制堆內存是一個好的選擇。
V8內存管理:
在V8中,主要將內存分爲新生代和老生代兩代。新生代中的對象爲存活時間較短的對象,老生代中的對象爲存活時間較長或常駐內存的對象。
 在分代的基礎上,新生代中的對象主要經過Scavenge算法進行垃圾回收。在Scavenge的具體實現中,主要採用了Cheney算法。 Cheney算法是一種採用複製的方式實現的垃圾回收算法。它將堆內存一分爲二,每一部分空間稱爲semispace。在這兩個semispace空間中,只有一個處於使用中,另外一個處於閒置狀態。處於使用狀態的semispace空間稱爲From空間,處於閒置狀態的空間稱爲To空間。當咱們分配對象時,先是在From空間中進行分配。當開始進行垃圾回收時,會檢查From空間中的存活對象,這些存活對象將被複制到To空間中,而非存活對象佔用的空間將會被釋放。完成複製後,From空間和To空間的角色發生對換。簡而言之,在垃圾回收的過程當中,就是經過將存活對象在兩個semispace空間之間進行復制。
對象晉升的條件主要有兩個,一個是對象是否經歷過Scavenge回收,一個是To空間的內存佔用比超過限制。
對於老生代中的對象,因爲存活對象佔較大比重,再採用Scavenge的方式會有兩個問題:一個是存活對象較多,複製存活對象的效率將會很低;另外一個問題依然是浪費一半空間的問題。這兩個問題致使應對生命週期較長的對象時Scavenge會顯得捉襟見肘。爲此,V8在老生代中主要採用了Mark-Sweep和Mark-Compact相結合的方式進行垃圾回收。
Mark-Sweep是標記清除的意思,它分爲標記和清除兩個階段。Mark-Sweep在標記階段遍歷堆中的全部對象,並標記活着的對象,在隨後的清除階段中,只清除沒有被標記的對象。活對象在新生代中只佔較小部分,死對象在老生代中只佔較小部分,這是兩種回收方式能高效處理的緣由。
Mark-Compact是標記整理的意思,是在Mark-Sweep的基礎上演變而來的。它們的差異在於對象在標記爲死亡後,在整理的過程當中,將活着的對象往一端移動,移動完成後,直接清理掉邊界外的內存。 
爲了不出現JavaScript應用邏輯與垃圾回收器看到的不一致的狀況,垃圾回收的3種基本算法都須要將應用邏輯暫停下來,待執行完垃圾回收後再恢復執行應用邏輯,這種行爲被稱爲「全停頓」(stop-the-world)。在V8的分代式垃圾回收中,一次小垃圾回收只收集新生代,因爲新生代默認配置得較小,且其中存活對象一般較少,因此即使它是全停頓的影響也不大。但V8的老生代一般配置得較大,且存活對象較多,須要較多的時間。爲了下降全堆垃圾回收帶來的停頓時間,V8先從標記階段入手,將本來要一口氣停頓完成的動做改成增量標記(incremental marking),也就是拆分爲許多小「步進」,每作完一「步進」就讓JavaScript應用邏輯執行一小會兒,垃圾回收與應用邏輯交替執行直到標記階段完成。 V8後續還引入了延遲清理(lazy sweeping)與增量式整理(incremental compaction),讓清理與整理動做也變成增量式的。同時還計劃引入並行標記與並行清理,進一步利用多核性能下降每次停頓的時間。 
若是變量是全局變量(不經過var聲明或定義在global變量上),因爲全局做用域須要直到進程退出才能釋放,此時將致使引用的對象常駐內存(常駐在老生代中)。若是須要釋放常駐內存的對象,能夠經過delete操做來刪除引用關係。或者將變量從新賦值,讓舊的對象脫離引用關係。在接下來的老生代內存清除和整理的過程當中,會被回收釋放。
一般,形成內存泄漏的緣由有以下幾個。 緩存。 隊列消費不及時。 做用域未釋放。
直接將內存做爲緩存的方案要十分慎重。外部的緩存軟件有着良好的緩存過時淘汰策略以及自有的內存管理,不影響Node進程的性能。 將緩存轉移到外部,減小常駐內存的對象的數量,讓垃圾回收更高效。 進程之間能夠共享緩存。

06/11

upgrade事件:當客戶端要求升級鏈接的協議時,須要和服務器端協商,客戶端會在請求頭中帶上Upgrade字段,服務器端會在接收到這樣的請求時觸發該事件。這在後文的WebSocket部分有詳細流程的介紹。若是不監聽該事件,發起該請求的鏈接將會關閉。  
除此以外,WebSocket與傳統HTTP有以下好處。 客戶端與服務器端只創建一個TCP鏈接,可使用更少的鏈接。 WebSocket服務器端能夠推送數據到客戶端,這遠比HTTP請求響應模式更靈活、更高效。 有更輕量級的協議頭,減小數據傳送量。

06/13

SSL做爲一種安全協議,它在傳輸層提供對網絡鏈接加密的功能。
Node在網絡安全上提供了3個模塊,分別爲crypto 、 tls 、 https 。
Node基於事件驅動和非阻塞設計,在分佈式環境中尤爲能發揮出它的特長,基於事件驅動能夠實現與大量的客戶端進行鏈接,非阻塞設計則讓它能夠更好地提高網絡的響應吞吐。Node提供了相對底層的網絡調用,以及基於事件的編程接口,使得開發者在這些模塊上十分輕鬆地構建網絡應用。

06/14

採用第三方緩存來存儲Session引發的一個問題是會引發網絡訪問。理論上來講訪問網絡中的數據要比訪問本地磁盤中的數據速度要慢,由於涉及到握手、傳輸以及網絡終端自身的磁盤I/O等,儘管如此但依然會採用這些高速緩存的理由有如下幾條: Node與緩存服務保持長鏈接,而非頻繁的短鏈接,握手致使的延遲隻影響初始化。 高速緩存直接在內存中進行數據存儲和訪問。 緩存服務一般與Node進程運行在相同的機器上或者相同的機房裏,網絡速度受到的影響較小。

06/15

模板引擎 : 
語法分解。 處理表達式。將標籤表達式轉換成普通的語言表達式。 生成待執行的語句。 與數據一塊兒執行,生成最終字符串。
模板編譯:
爲了可以最終與數據一塊兒執行生成字符串,咱們須要將原始的模板字符串轉換成一個函數對象。生成的中間函數只與模板字符串相關,與具體的數據無關。
  一些模板引擎的優化步驟,主要有以下幾種。 緩存模板文件。 緩存模板文件編譯後的函數。  優化模板中的執行表達式 。

 06/18

爲了解決高併發問題,基於事件驅動的服務模型出現了,像Node與Nginx均是基於事件驅動的方式實現的,採用單線程避免了沒必要要的內存開銷和上下文切換開銷。 在PHP中沒有線程的支持。它的健壯性是由它給每一個請求都創建獨立的上下文來實現的。
因爲全部處理都在單線程上進行,影響事件驅動服務模型性能的點在於CPU的計算能力,它的上限決定這類服務模型的性能上限,但它不受多進程或多線程模式中資源上限的影響,可伸縮性遠比前二者高。若是解決掉多核CPU的利用問題,帶來的性能上提高是可觀的。

06/20

IPC的全稱是Inter-Process Communication,即進程間通訊。進程間通訊的目的是爲了讓不一樣的進程可以互相訪問資源並進行協調工做。實現進程間通訊的技術有不少,如命名管道、匿名管道、socket、信號量、共享內存、消息隊列、Domain Socket等。
Node中實現IPC通道的是管道(pipe)技術。但此管道非彼管道,在Node中管道是個抽象層面的稱呼,具體細節實現由libuv提供,在Windows下由命名管道(named pipe)實現,*nix系統則採用Unix Domain Socket實現。
子進程根據message.type建立對應TCP服務器對象,而後監聽到文件描述符上。因爲底層細節不被應用層感知,因此在子進程中,開發者會有一種服務器就是從父進程中直接傳遞過來的錯覺。值得注意的是,Node進程之間只有消息傳遞,不會真正地傳遞對象,這種錯覺是抽象封裝的結果。
獨立啓動的進程中,TCP服務器端socket套接字的文件描述符並不相同,致使監聽到相同的端口時會拋出異常。  但對於send()發送的句柄還原出來的服務而言,它們的文件描述符是相同的,因此監聽相同端口不會引發異常。 多個應用監聽相同端口時,文件描述符同一時間只能被某個進程所用。

06/23

Node默認提供的機制是採用操做系統的搶佔式策略。所謂的搶佔式就是在一堆工做進程中,閒着的進程對到來的請求進行爭搶,誰搶到誰服務。  

06/27

Node產品的性能與許多因素相關,這裏咱們將範疇縮減到Web應用中來,只評估一些常見的提高性能的方法。對於Web應用而言,最直接有效的莫過於動靜分離、多進程架構、分佈式,其中涉及的幾個拆分原則以下所示。 作專注的事。 讓擅長的工具作擅長的事情。 將模型簡化。 將風險分離。 除此以外,緩存也能帶來很大的性能提高。  
若是進程中存在內存泄漏,又一時沒有排查解決,有一種方案能夠解決這種情況。這種方案應用於多進程架構的服務集羣,讓每一個工做進程指定服務多少次請求,達到請求數以後進程就再也不服務新的鏈接,主進程啓動新的工做進程來服務客戶,舊的進程等全部鏈接斷開後就退出。這樣即便存在內存泄漏的風險,也能有效地規避內存泄漏帶來的影響。但這屬於規避問題,只解決了問題的表象,不推薦使用。  
相關文章
相關標籤/搜索