上一篇[專欄](http://www.infoq.com/cn/articles/nodejs-connect-module)簡單介紹了Connect模塊的基本架構,它的執行模型十分簡單,中間件機制也使得它十分易于擴展,具備良好的可伸縮性。在Connect的良好機制下,我們本章開始將逐步解開Connect生態圈中中間件部分,這部分給予Connect良好的功能擴展。
## 靜態文件中間件
也許你還記得我曾經寫過的[Node.js靜態文件服務器實戰](http://www.infoq.com/cn/news/2011/11/tyq-nodejs-static-file-server),那篇文章中我敘述了如何利用Node.js實現一個靜態文件服務器的許多技術細節,包括路由實現,MIME,緩存控制,傳輸壓縮,安全、歡迎頁、斷點續傳等。但是這里我們不需要去親自處理細節,`Connect`的`static`中間件為我們提供上述所有功能。代碼只需寥寥3行即可:
~~~
var connect = require('connect');
var app = connect();
app.use(connect.static(__dirname + '/public'));
~~~
在項目中需要臨時搭建靜態服務器,也無需安裝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());
app.use(“/public”, connect.static(__dirname + '/public', {maxAge: 86400000}));
~~~
這是一個提供上層緩存功能的中間件,能夠將磁盤中的文件加載到內存中,以提高響應速度和提高性能。
它的官方測試數據如下:
~~~
static(): 2700 rps
node-static: 5300 rps
static() + staticCache(): 7500 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實例進程中轉移到第三方成熟的緩存中去即可。這可以保證:
1. 緩存內容不冗余。
2. 集中式緩存,減少不一致性的發生。
3. 緩存的算法更優秀以保持較高的命中率。
4. 讓Node.js保持輕量,以解決它更擅長的問題。
Connect推薦服務器端緩存采用`varnish`這樣的成熟緩存代理。而筆者目前的項目則是通過`Redis`來完成后端緩存的任務。
## 參考內容
* [https://www.varnish-cache.org/releases](https://www.varnish-cache.org/releases)
* [http://www.senchalabs.org/connect/static.html](http://www.senchalabs.org/connect/static.html)
* [http://www.senchalabs.org/connect/staticCache.html](http://www.senchalabs.org/connect/staticCache.html)