(五):Node.js的異步實現php
專欄的第五篇文章《Node.js的異步實現》。以前介紹了Node.js的事件機制,也許讀者對此尚會以爲意猶未盡,由於僅僅只是簡單的事件機制,並不能道盡Node.js的神奇。若是Node.js是一盤別開生面的磁帶,那麼事件與異步分別是其A面和B面,它們共同組成了Node.js的別樣之處。本文將翻轉Node.js到B面,與你共同聆聽。html
異步I/O前端
在操做系統中,程序運行的空間分爲內核空間和用戶空間。咱們經常提起的異步I/O,其實質是用戶空間中的程序不用依賴內核空間中的I/O操做實際完成,便可進行後續任務。如下僞代碼模仿了一個從磁盤上獲取文件和一個從網絡中獲取文件的操做。異步I/O的效果就是getFileFromNet的調用不依賴於getFile調用的結束。node
getFile("file_path"); |
若是以上兩個任務的時間分別爲m和n。採用同步方式的程序要完成這兩個任務的時間總花銷會是m + n。可是若是是採用異步方式的程序,在兩種I/O能夠並行的情況下(好比網絡I/O與文件I/O),時間開銷將會減少爲max(m, n)。linux
異步I/O的必要性nginx
有的語言爲了設計得使應用程序調用方便,將程序設計爲同步I/O的模型。這意味着程序中的後續任務都須要等待I/O的完成。在等待I/O完成的過程當中,程序沒法充分利用CPU。爲了充分利用CPU,和使I/O能夠並行,目前有兩種方式能夠達到目的:git
多線程單進程github
多線程的設計之處就是爲了在共享的程序空間中,實現並行處理任務,從而達到充分利用CPU的效果。多線程的缺點在於執行時上下文交換的開銷較大,和狀態同步(鎖)的問題。一樣它也使得程序的編寫和調用複雜化。算法
單線程多進程apache
爲了不多線程形成的使用不便問題,有的語言選擇了單線程保持調用簡單化,採用啓動多進程的方式來達到充分利用CPU和提高整體的並行處理能力。 它的缺點在於業務邏輯複雜時(涉及多個I/O調用),由於業務邏輯不能分佈到多個進程之間,事務處理時長要遠遠大於多線程模式。
前者在性能優化上還有迴旋的餘地,後者的作法純粹是一種加三倍服務器的行爲。
並且如今的大型Web應用中,單機的情形是十分稀少的,一個事務每每須要跨越網絡幾回才能完成最終處理。若是網絡速度不夠理想,m和n值都將會變大,這時同步I/O的語言模型將會露出其最脆弱的狀態。
這種場景下的異步I/O將會體現其優點,max(m, n)的時間開銷能夠有效地緩解m和n值增加帶來的性能問題。而當並行任務更多的時候,m + n + …與max(m, n, …)之間的孰優孰劣更是一目瞭然。從這個公式中,能夠了解到異步I/O在分佈式環境中是多麼重要,而Node.js自然地支持這種異步I/O,這是衆多雲計算廠商對其青睞的根本緣由。
操做系統對異步I/O的支持
咱們聽到Node.js時,咱們經常會聽到異步,非阻塞,回調,事件這些詞語混合在一塊兒。其中,異步與非阻塞聽起來彷佛是同一回事。從實際效果的角度說,異步和非阻塞都達到了咱們並行I/O的目的。可是從計算機內核I/O而言,異步/同步和阻塞/非阻塞實際上時兩回事。
I/O的阻塞與非阻塞
阻塞模式的I/O會形成應用程序等待,直到I/O完成。同時操做系統也支持將I/O操做設置爲非阻塞模式,這時應用程序的調用將可能在沒有拿到真正數據時就當即返回了,爲此應用程序須要屢次調用才能確認I/O操做徹底完成。
I/O的同步與異步
I/O的同步與異步出如今應用程序中。若是作阻塞I/O調用,應用程序等待調用的完成的過程就是一種同步情況。相反,I/O爲非阻塞模式時,應用程序則是異步的。
異步I/O與輪詢技術
當進行非阻塞I/O調用時,要讀到完整的數據,應用程序須要進行屢次輪詢,才能確保讀取數據完成,以進行下一步的操做。
輪詢技術的缺點在於應用程序要主動調用,會形成佔用較多CPU時間片,性能較爲低下。現存的輪詢技術有如下這些:
read
select
poll
epoll
pselect
kqueue
read是性能最低的一種,它經過重複調用來檢查I/O的狀態來完成完整數據讀取。select是一種改進方案,經過對文件描述符上的事件狀態來進行判斷。操做系統還提供了poll、epoll等多路複用技術來提升性能。
輪詢技術知足了異步I/O確保獲取完整數據的保證。可是對於應用程序而言,它仍然只能算時一種同步,由於應用程序仍然須要主動去判斷I/O的狀態,依舊花費了不少CPU時間來等待。
上一種方法重複調用read進行輪詢直到最終成功,用戶程序會佔用較多CPU,性能較爲低下。而實際上操做系統提供了select方法來代替這種重複read輪詢進行狀態判斷。select內部經過檢查文件描述符上的事件狀態來進行判斷數據是否徹底讀取。可是對於應用程序而言它仍然只能算是一種同步,由於應用程序仍然須要主動去判斷I/O的狀態,依舊花費了不少CPU時間等待,select也是一種輪詢。
理想的異步I/O模型
理想的異步I/O應該是應用程序發起異步調用,而不須要進行輪詢,進而處理下一個任務,只需在I/O完成後經過信號或是回調將數據傳遞給應用程序便可。
幸運的是,在Linux下存在一種這種方式,它原生提供了一種異步非阻塞I/O方式(AIO)便是經過信號或回調來傳遞數據的。
不幸的是,只有Linux下有這麼一種支持,並且還有缺陷(AIO僅支持內核I/O中的O_DIRECT方式讀取,致使沒法利用系統緩存。參見:http://forum.nginx.org/read.php?2,113524,113587#msg-113587
以上都是基於非阻塞I/O進行的設定。另外一種理想的異步I/O是採用阻塞I/O,但加入多線程,將I/O操做分到多個線程上,利用線程之間的通訊來模擬異步。Glibc的AIO即是這樣的典型http://www.ibm.com/developerworks/linux/library/l-async/。然而遺憾在於,它存在一些難以忍受的缺陷和bug。能夠簡單的概述爲:Linux平臺下沒有完美的異步I/O支持。
所幸的是,libev的做者Marc Alexander Lehmann從新實現了一個異步I/O的庫:libeio。libeio實質依然是採用線程池與阻塞I/O模擬出來的異步I/O。
那麼在Windows平臺下的情況如何呢?而實際上,Windows有一種獨有的內核異步IO方案:IOCP。IOCP的思路是真正的異步I/O方案,調用異步方法,而後等待I/O完成通知。IOCP內部依舊是經過線程實現,不一樣在於這些線程由系統內核接手管理。IOCP的異步模型與Node.js的異步調用模型已經十分近似。
以上兩種方案則正是Node.js選擇的異步I/O方案。因爲Windows平臺和*nix平臺的差別,Node.js提供了libuv來做爲抽象封裝層,使得全部平臺兼容性的判斷都由這一層次來完成,保證上層的Node.js與下層的libeio/libev及IOCP之間各自獨立。Node.js在編譯期間會判斷平臺條件,選擇性編譯unix目錄或是win目錄下的源文件到目標程序中。
下文咱們將經過解釋Windows下Node.js異步I/O(IOCP)的簡單例子來探尋一下從JavaScript代碼到系統內核之間都發生了什麼。
Node.js的異步I/O模型
不少同窗在碰見Node.js後必然產生過對回調函數究竟如何被調用產生過好奇。在文件I/O這一塊與普通的業務邏輯的回調函數不一樣在於它不是由咱們本身的代碼所觸發,而是系統調用結束後,由系統觸發的。下面咱們以最簡單的fs.open方法來做爲例子,探索Node.js與底層之間是如何執行異步I/O調用和回調函數到底是如何被調用執行的。
fs.open = function(path, flags, mode, callback) { mode = modeNum(mode, 438 /*=0666*/); binding.open(pathModule._makeLong(path), |
fs.open的做用是根據指定路徑和參數,去打開一個文件,從而獲得一個文件描述符,是後續全部I/O操做的初始操做。
在JavaScript層面上調用的fs.open方法最終都透過node_file.cc調用到了libuv中的uv_fs_open方法,這裏libuv做爲封裝層,分別寫了兩個平臺下的代碼實現,編譯以後,只會存在一種實現被調用。
請求對象
在uv_fs_open的調用過程當中,Node.js建立了一個FSReqWrap請求對象。從JavaScript傳入的參數和當前方法都被封裝在這個請求對象中,其中回調函數則被設置在這個對象的oncomplete_sym屬性上。
req_wrap->object_->Set(oncomplete_sym, callback); |
對象包裝完畢後,調用QueueUserWorkItem方法將這個FSReqWrap對象推入線程池中等待執行。
QueueUserWorkItem(&uv_fs_thread_proc, req, WT_EXECUTELONGFUNCTION) |
QueueUserWorkItem接受三個參數,第一個是要執行的方法,第二個是方法的上下文,第三個是執行的標誌。當線程池中有可用線程的時候調用uv_fs_thread_proc方法執行。該方法會根據傳入的類型調用相應的底層函數,以uv_fs_open爲例,實際會調用到fs__open方法。調用完畢以後,會將獲取的結果設置在req->result上。而後調用PostQueuedCompletionStatus通知咱們的IOCP對象操做已經完成。
PostQueuedCompletionStatus((loop)->iocp, 0, 0, &((req)->overlapped)) |
PostQueuedCompletionStatus方法的做用是向建立的IOCP上相關的線程通訊,線程根據執行情況和傳入的參數斷定退出。
至此,由JavaScript層面發起的異步調用第一階段就此結束。
事件循環
在調用uv_fs_open方法的過程當中實際上應用到了事件循環。以在Windows平臺下的實現中,啓動Node.js時,便建立了一個基於IOCP的事件循環loop,並一直處於執行狀態。
uv_run(uv_default_loop()); |
每次循環中,它會調用IOCP相關的GetQueuedCompletionStatus方法檢查是否線程池中有執行完的請求,若是存在,poll操做會將請求對象加入到loop的pending_reqs_tail屬性上。 另外一邊這個循環也會不斷檢查loop對象上的pending_reqs_tail引用,若是有可用的請求對象,就取出請求對象的result屬性做爲結果傳遞給oncomplete_sym執行,以此達到調用JavaScript中傳入的回調函數的目的。 至此,整個異步I/O的流程完成結束。其流程以下:
事件循環和請求對象構成了Node.js的異步I/O模型的兩個基本元素,這也是典型的消費者生產者場景。在Windows下經過IOCP的GetQueuedCompletionStatus、PostQueuedCompletionStatus、QueueUserWorkItem方法與事件循環實。對於*nix平臺下,這個流程的不一樣之處在與實現這些功能的方法是由libeio和libev提供。
(六):Buffer那些事兒
做爲前端的JSer,是一件很是幸福的事情,由於在字符串上歷來沒有出現過任何糾結的問題。咱們來看看PHP對字符串長度的判斷結果:
<? php |
以上三行判斷分別返回十、30、10。對於中國人而言,strlen這個方法對於Unicode的判斷結果是很是讓人疑惑。而看看JavaScript中對字符串長度的判斷,就知道這個length屬性對調用者而言是多麼友好。
console.log("0123456789".length); // 10 |
儘管在計算機內部,一箇中文字和一個英文字佔用的字節位數是不一樣的,但對於用戶而言,它們擁有相同的長度。我認爲這是JavaScript中 String處理得精彩的一個點。正是因爲這個緣由,全部的數據從後端傳輸到前端被調用時,都是這般友好的字符串。因此對於前端工程師而言,他們是沒有字 符串Buffer的概念的。若是你是一名前端工程師,那麼今後在與Node.js打交道的過程當中,必定要當心Buffer啦,由於它比傳統的String 要調皮一點。
你該當心Buffer啦
像許多計算機的技術同樣,都是從國外傳播過來的。那些以英文做爲母語的傳道者們應該沒有考慮過英文之外的使用者,因此你有可能看到以下這樣一段代碼在向你描述如何在data事件中鏈接字符串。
var fs = require('fs'); |
若是這個文件讀取流讀取的是一個純英文的文件,這段代碼是可以正常輸出的。可是若是咱們再改變一下條件,將每次讀取的buffer大小變成一個奇數,以模擬一個字符被分配在兩個trunk中的場景。
var rs = fs.createReadStream('testdata.md', {bufferSize: 11}); |
咱們將會獲得如下這樣的亂碼輸出:
事件循???和請求???象構成了Node.js???異步I/O模型的???個基本???素,這也是典???的消費???生產者場景。 |
形成這個問題的根源在於data += trunk語句裏隱藏的錯誤,在默認的狀況下,trunk是一個Buffer對象。這句話的實質是隱藏了toString的變換的:
data = data.toString() + trunk.toString(); |
因爲漢字不是用一個字節來存儲的,致使有被截破的漢字的存在,因而出現亂碼。解決這個問題有一個簡單的方案,是設置編碼集:
var rs = fs.createReadStream('testdata.md', {encoding: 'utf-8', bufferSize: 11}); |
這將獲得一個正常的字符串響應:
事件循環和請求對象構成了Node.js的異步I/O模型的兩個基本元素,這也是典型的消費者生產者場景。 |
遺憾的是目前Node.js僅支持hex、utf八、ascii、binary、base6四、ucs2幾種編碼的轉換。對於那些由於歷史遺留問題依舊還生存着的GBK,GB2312等編碼,該方法是無能爲力的。
有趣的string_decoder
在這個例子中,若是仔細觀察,會發現一件有趣的事情發生在設置編碼集以後。咱們提到data += trunk等價於data = data.toString() + trunk.toString()。經過如下的代碼能夠測試到一個漢字佔用三個字節,而咱們按11個字節來截取trunk的話,依舊會存在一個漢字被分割在兩個trunk中的情景。
console.log("事件循環和請求對象".length); |
按照猜測的toString()方式,應該返回的是事件循xxx和請求xxx象纔對,其中「環」字應該變成亂碼纔對,可是在設置了encoding(默認的utf8)以後,結果卻正常顯示了,這個結果十分有趣。
在好奇心的驅使下能夠探查到data事件調用了string_decoder來進行編碼補足的行爲。經過string_decoder對象輸出第一個截取Buffer(事件循xx)時,只返回事件循這個字符串,保留xx。第二次經過string_decoder對象輸出時檢測到上次保留的xx,將上次剩餘內容和本次的Buffer進行從新拼接輸出。因而達到正常輸出的目的。
string_decoder,目前在文件流讀取和網絡流讀取中都有應用到,必定程度上避免了粗魯拼接trunk致使的亂碼錯誤。可是,遺憾在於string_decoder目前只支持utf8編碼。它的思路其實還能夠擴展到其餘編碼上,只是最終是否會支持目前尚不可得知。
鏈接Buffer對象的正確方法
那麼萬能的適應各類編碼並且正確的拼接Buffer對象的方法是什麼呢?咱們從Node.js在github上的源碼中找出這樣一段正確讀取文件,並鏈接buffer對象的方法:
var buffers = []; |
在end事件中經過細膩的鏈接方式,最後拿到理想的Buffer對象。這時候不管是在支持的編碼之間轉換,仍是在不支持的編碼之間轉換(利用iconv模塊轉換),都不會致使亂碼。
簡化鏈接Buffer對象的過程
上述一大段代碼僅只完成了一件事情,就是鏈接多個Buffer對象,而這種場景需求將會在多個地方發生,因此,採用一種更優雅的方式來完成該過程是必要的。筆者基於以上的代碼封裝出一個bufferhelper模塊,用於更簡潔地處理Buffer對象。能夠經過NPM進行安裝:
npm install bufferhelper |
下面的例子演示瞭如何調用這個模塊。與傳統data += trunk之間只是bufferHelper.concat(chunk)的差異,既避免了錯誤的出現,又使得代碼能夠獲得簡化而有效地編寫。
var http = require('http'); }).listen(8001); |
因此關於Buffer對象的操做的最佳實踐是:
保持編碼不變,以利於後續編碼轉換
使用封裝方法達到簡潔代碼的目的
(七):Connect模塊解析(之一)
Connect模塊背景
Node.js的願望是成爲一個能構建高速,可伸縮的網絡應用的平臺,它自己具備基於事件,異步,非阻塞,回調等特性,這在前幾篇專欄中有過描述。正是基於這樣的一些特性,Node.js平臺上的Web框架也具備不一樣於其餘平臺的一些特性,其中Connect是衆多Web框架中的佼佼者。
Connect在它的官方介紹中,它是Node的一箇中間件框架。超過18個捆綁的中間件和一些精選第三方中間件。儘管Connect可能不是性能最好的Node.jsWeb框架,但它卻幾乎是最爲流行的Web框架。爲什麼Connect能在衆多框架中勝出,其緣由不外乎有以下幾個:
模型簡單
中間件易於組合和插拔
中間件易於定製和優化
豐富的中間件
Connect自身十分簡單,其做用是基於Web服務器作中間件管理。至於如何如何處理網絡請求,這些任務經過路由分派給管理的中間件們進行處理。它的處理模型僅僅只是一箇中間隊列,進行流式處理而已,流式處理可能性能不是最優,可是倒是最易於被理解和接受。基於中間件能夠自由組合和插拔的狀況,優化它十分容易。
Connect模塊目前在NPM倉庫的MDO(被依賴最多的模塊)排行第八位。但這並無真實反映出它的價值,由於排行第五位的Express框架其實是依賴Connect建立而成的。關於Express的介紹,將會在後續的專欄中一一爲你講解。
中間件
讓咱們回顧一下Node.js最簡單的Web服務器是如何編寫的:
var http = require('http'); |
咱們從最樸素的Web服務器處理流程開始,能夠看到HTTP模塊基於事件處理網絡訪問無外乎兩個主要的因素,請求和響應。同理的是Connect的中間件也是扮演這樣一個角色,處理請求,而後響應客戶端或是讓下一個中間件繼續處理。以下是一箇中間件最樸素的原型:
function (req, res, next) { |
在中間件的上下文中,有着三個變量。分別表明請求對象、響應對象、下一個中間件。若是當前中間件調用了res.end()結束了響應,執行下一個中間件就顯得沒有必要。
流式處理
爲了演示中間件的流式處理,咱們能夠看看中間件的使用形式:
var app = connect(); |
Conncet提供use方法用於註冊中間件到一個Connect對象的隊列中,咱們稱該隊列叫作中間件隊列。
Conncet的部分核心代碼以下,它經過use方法來維護一箇中間件隊列。而後在請求來臨的時候,依次調用隊列中的中間件,直到某個中間件再也不調用下一個中間件爲止。
app.stack = []; // add the middleware return this; |
值得注意的是,必需要有一箇中間件調用res.end()方法來告知客戶端請求已被處理完成,不然客戶端將一直處於等待狀態。
流式處理也是Node.js中用於流程控制的經典模式,Connect模塊是典型的應用了它。流式處理的好處在於,每個中間層的職責都是單一的,開發者經過這個模式能夠將複雜的業務邏輯進行分解。
路由
從前文能夠看到其實app.use()方法接受兩個參數,route和fn,既路由信息和中間件函數,一個完整的中間件,其實包含路由信息和中間件函數。路由信息的做用是過濾不匹配的URL。請求在碰見路由信息不匹配時,直接傳遞給下一個中間件處理。
一般在調用app.use()註冊中間件時,只須要傳遞一箇中間件函數便可。實際上這個過程當中,Connect會將/做爲該中間件的默認路由,它表示全部的請求都會被該中間件處理。
中間件的優點相似於Java中的過濾器,可以全局性地處理一些事務,使得業務邏輯保持簡單。
任何事物均有兩面性,當你調用app.use()添加中間件的時候,須要考慮的是中間件隊列是否太長,由於每一層中間件的調用都是會下降性能的。爲了提升性能,在添加中間件的時候,如非全局需求的,儘可能附帶上精確的路由信息。
以multipart中間件爲例,它用於處理表單提交的文件信息,相對而言較爲耗費資源。它存在潛在的問題,那就是有可能被人在客戶端惡意提交文件,形成服務器資源的浪費。若是不採用路由信息加以限制,那麼任何URL均可以被攻擊。
app.use("/upload", connect.multipart({ uploadDir: path })); |
加上精確的路由信息後,能夠將問題減少。
MVC目錄
藉助Connect能夠自由定製中間件的優點,能夠自行提高性能或是設計出適合本身須要的項目。Connect自身提供了路由功能,在此基礎上,能夠輕鬆搭建MVC模式的框架,以達到開發效率和執行效率的平衡。如下是筆者項目中採用的目錄結構,清晰地劃分目錄結構能夠幫助劃分代碼的職責,此處僅供參考。
├── Makefile // 構建文件,一般用於啓動單元測試運行等操做 |
(八):Connect模塊解析(之二)靜態文件中間件
上一篇專欄簡單介紹了Connect模塊的基本架構,它的執行模型十分簡單,中間件機制也使得它十分易於擴展,具有良好的可伸縮性。在Connect的良好機制下,咱們本章開始將逐步解開Connect生態圈中中間件部分,這部分給予Connect良好的功能擴展。
靜態文件中間件
也許你還記得我曾經寫過的Node.js靜態文件服務器實戰,那篇文章中我敘述瞭如何利用Node.js實現一個靜態文件服務器的許多技術細節,包括路由實現,MIME,緩存控制,傳輸壓縮,安全、歡迎頁、斷點續傳等。可是這裏咱們不須要去親自處理細節,Connect的static中間件爲咱們提供上述全部功能。代碼只需寥寥3行便可:
var connect = require('connect'); |
在項目中須要臨時搭建靜態服務器,也無需安裝apache之類的服務器,經過NPM安裝Connect以後,三行代碼便可解決需求。
這裏須要說起的是在使用該模塊的一點性能相關的細節。
動靜分離
前一章說起,app.use()方法在沒有指定路由信息時,至關於app.use("/", middleware)。這意味着靜態文件中間件將會在處理全部路徑的請求。在動靜態請求混雜的場景下,靜態中間件會在動態請求時也調用fs.stat來檢測文件系統是否存在靜態文件。這形成了沒必要要的系統調用,使得性能下降。
解決影響性能的方法既是動靜分離。利用路由檢測,避免沒必要要的系統調用,能夠有效下降對動態請求的性能影響。
app.use('/public', connect.static(__dirname + '/public')); |
在大型的應用中,動靜分離一般無需到一個Node.js實例中進行,CDN的方式直接在域名上將請求分離。小型應用中,適當的進行動靜分離便可避免沒必要要的性能損耗。
緩存策略
緩存策略包含客戶端和服務端兩個部分。
客戶端的緩存,主要是利用瀏覽器對HTTP協議響應頭中cache-control和expires字段的支持。瀏覽器在獲得明確的相應頭後,會將文件緩存在本地,依據cache-control和expires的值進行相應的過時策略。這使得重複訪問的過程當中,瀏覽器能夠從本地緩存中讀取文件,而無需從網絡讀取文件,提高加載速度,也能夠下降對服務器的壓力。
默認狀況下靜態中間件的最大緩存時設置爲0,意味着它在瀏覽器關閉後就被清除。這顯然不是咱們所指望的結果。除非是在開發環境能夠無視maxAge的設置外,生產環境請務必設置緩存,由於它能有效節省網絡帶寬。
app.use('/public', connect.static(__dirname + '/public', {maxAge: 86400000})); |
maxAge選項的單位爲毫秒。YUI3的CDN服務器設置過時時間爲10年,是一個值得參考的值。
靜態文件若是在客戶端被緩存,在須要清除緩存的時候,又該如何清除呢?這裏的實現方法較多,一種較爲推薦的作法是爲文件進行md5處理。
http://some.url/some.js?md5 |
當文件內容產生改變時,md5值也將發生改變,瀏覽器根據URL的不一樣會從新獲取靜態文件。md5的方式能夠避免沒必要要的緩存清除,也能精確清除緩存。
因爲瀏覽器自己緩存容量的限制,儘管咱們可能設置了10年的過時時間,可是也許兩天以後就被新的靜態文件擠出了本地緩存。這將持續引發靜態服務器的響應,也即意味着,客戶端緩存並不能徹底解決下降服務器壓力的問題。
爲了解決靜態服務器重複讀取磁盤形成的壓力,這裏須要引出第二個相關的中間件:staticCache。
app.use(connect.staticCache()); |
這是一個提供上層緩存功能的中間件,可以將磁盤中的文件加載到內存中,以提升響應速度和提升性能。
它的官方測試數據以下:
static(): 2700 rps |
另外一個專門用於靜態文件託管的模塊叫node-static,其性能是Connect靜態文件中間件的效率的兩倍。可是在緩存中間件的協助下,能夠彌補性能損失。
事實上,這個中間件在生產環境下並不推薦被使用,並且它將在Connect 3.0版本中被移除。可是它的實現中有值得玩味的地方,這有助於咱們認識Node.js模型的優缺點。
staticCache中間件有兩個主要的選項:maxObjects和maxLength。表明的是能存儲多少個文件和單個文件的最大尺寸,其默認值爲128和256kb。爲什麼會有這兩個選項的設定,緣由在於V8有內存限制的緣由,做爲緩存,若是沒有良好的過時策略,緩存將會無限增長,直到內存溢出。設置存儲數量和單個文件大小後,能夠有效抑制緩存區的大小。
事實上,該緩存還存在的缺陷是單機狀況下,一般爲了有效利用CPU,Node.js實例並不僅有一個,多個實例進程之間將會存在冗餘的緩存佔用,這對於內存使用而言是浪費的。
除此以外,V8的垃圾回收機制是暫停JavaScript線程執行,經過掃描的方式決定是否回收對象。若是緩存對象過大,鍵太多,則掃描的時間會增長,會引發JavaScript響應業務邏輯的速度變慢。
可是這個模塊並不是沒有存在的意義,上述說起的缺陷大多都是V8內存限制和Node.js單線程的緣由。解決該問題的方式則變得明瞭。
風險轉移是Node.js中經常使用於解決資源不足問題的方式,尤爲是內存方面的問題。將緩存點,從Node.js實例進程中轉移到第三方成熟的緩存中去便可。這能夠保證:
緩存內容不冗餘。
集中式緩存,減小不一致性的發生。
緩存的算法更優秀以保持較高的命中率。
讓Node.js保持輕量,以解決它更擅長的問題。
Connect推薦服務器端緩存採用varnish這樣的成熟緩存代理。而筆者目前的項目則是經過Redis來完成後端緩存的任務。