這將是一個分爲兩部分,內容是關於在生產環境下,跑Express
應用的最佳實踐。第一部分會關注安全性,第二部分則會關注性能和可靠性。當你讀這篇文章時,會假設你已經對Node.js
和web開發有所瞭解,而且對生產環境有了概念。html
關於第一部分,請參閱Express在生產環境下的最佳實踐 - 安全性。node
正如第一部分所說,生產環境是供你的最終用戶們所使用的,而開發環境則是供你開發和測試代碼所用。故對於和兩個環境的要求,是很是不一樣的。例如,在開發環境下,你沒必要考慮伸縮性和可靠性還有性能的問題,但這些在生產環境下都很是重要。linux
接下來,咱們會將此文分爲兩大部分:web
須要對代碼作的事,即開發部分。redis
須要對環境作的事,即運維部分,shell
爲了提高你應用的性能,你能夠經過:數據庫
使用gzip
壓縮express
禁止使用同步方法npm
使用中間件來提供靜態文件json
適當地打印日誌
合理地處理異常
gzip
壓縮Gzip
壓縮能夠顯著地減小你web應用的響應體大小,從而提高你的web應用的響應速度。在Express
中,你可使用compression中間件來啓用gzip
:
var compression = require('compression'); var express = require('express'); var app = express(); app.use(compression());
對於在生產環境中,流量十分大的網站,最好是在反向代理層處理壓縮。若是這樣作,那麼就不就須要使用compression
了,而是須要參閱Nginx
的ngx_http_gzip_module
模塊的文檔。
同步方法會在它返回以前都一直阻塞線程。一次單獨的調用可能影響不大,但在流量很是巨大的生產環境中,它是不可接受的,可能會致使嚴重的性能問題。
雖然大多數的Node.js
和其第三方庫都同時提供了一個方法的同步和異步版本,但在生產環境下,請老是使用它的異步版本。惟一可能例外的場景多是,若是這個方法只在應用初始化時調用一次,那麼使用它的同步版本也是能夠接受的。
若是你使用的是Node.js
4.0+ 或 io.js
2.1.0+ ,你能夠在啓動應用時附上--trace-sync-io
參數來檢查你的應用中哪裏使用了同步API。更多關於這個參數的信息,你能夠參閱io.js
2.1.0的更新日誌。
在開發環境下,你可使用res.sendFile()
來提供靜態文件。但在生產環境下,這是不被容許的,由於這個方法會在每次請求時都會對文件系統進行讀取。res.sendFile()
並非經過系統方法sendfile
實現的。
對應的,你可使用serve-static中間件來爲你的Express
應用提供靜態文件。
更好的選擇則是在反向代理層上提供靜態文件。
總得來講,爲你的應用打印日誌的目的有兩個:調試和操做記錄。在開發環境下,咱們一般使用console.log()
或console.err()
來作這些事。可是,當這些方法的輸出目標是終端或文件時,它們是同步的,因此它們並不適用於生產環境,除非你將輸出導流至另外一個程序中。
若是你正在爲了調試而打印日誌。那麼你可使用一些專用於調試的庫如debug,用於代替console.log()
。這個庫能夠經過設置DEBUG
環境變量來控制具體哪些信息會被打印。雖然這些方法也是同步的,但你必定不會在生產環境下進行調試吧?
若是你正在爲了記錄應用的活動而打印日誌。那麼你可使用一些日誌庫如winston或Bunyan,來替代console.log()
。更多關於這兩個庫的詳情,能夠參閱這裏。
Node.js
在遇到未處理的異常時就會退出。若是沒有合理地捕獲並處理異常,這會使你的應用崩潰和離線。若是你使用了一個自動重啓的工具,那麼你的應用則會在崩潰後馬上重啓,並且幸運的是,Express
應用的重啓時間一般都很快。可是無論怎樣,你都想要儘可能避免這種崩潰。
爲了保證你合理處理異常,請聽從如下指示:
使用try-catch
使用promise
你不該該監聽全局事件uncaughtException
。監聽該事件將會致使進程遇到未處理異常時的行爲被改變:進程將會忽略此異常並繼續運行。這聽上去很好,可是若是你的應用中存在未處理異常,繼續運行它是很是危險的,由於應用的狀態開始變得不可預測。
因此,監聽uncaughtException
並非一個好主意,它已被官方地列爲了避免推薦的作法,而且之後可能會移除這個接口。咱們更推薦的是,使用多進程和自動重啓。
咱們一樣不推薦使用domains
。它一般也並不能解決問題,而且已經是一個被標識爲棄用的模塊。
try-catch
Try-catch
是一個JavaScript
語言自帶的捕獲同步代碼的結構。使用try-catch
,你能夠捕獲例如JSON解析錯誤這樣的異常。
使用JSHint
或JSLint
這樣的工具則可讓你遠離引用錯誤或未定義變量這種隱式的異常。
一個使用try-catch
來避免進程退出的例子:
// Accepts a JSON in the query field named "params" // for specifying the parameters app.get('/search', function (req, res) { // Simulating async operation setImmediate(function () { var jsonStr = req.query.params; try { var jsonObj = JSON.parse(jsonStr); res.send('Success'); } catch (e) { res.status(400).send('Invalid JSON string'); } }) });
可是,try-catch
只能捕獲同步代碼的異常。可是Node.js
世界主要是異步的,因此,對於大多數的異常它都無能爲力。
promise
Promise
能夠經過then()
處理異步代碼裏的一切異常(顯式和隱式)。記得在promise
鏈的最後加上.catch(next)
。例子:
app.get('/', function (req, res, next) { // do some sync stuff queryDb() .then(function (data) { // handle data return makeCsv(data) }) .then(function (csv) { // handle csv }) .catch(next) }) app.use(function (err, req, res, next) { // handle error })
如今全部的同步代碼和異步代碼的異常都傳遞到了異常處理中間件中。
可是,仍有兩點須要提醒:
全部你的異步代碼都必須返回一個promise
(除了emitter
)。若是你正在使用的庫沒有返回一個promise
,那麼就使用一些工具方法(如Bluebird.promisifyAll()
)來轉換它。Event emitter
(如stream
)仍會形成未處理的異常。因此你必須合理地監聽它們的error
事件。例子:
app.get('/', wrap(async (req, res, next) =>; { let company = await getCompanyById(req.query.id) let stream = getLogoStreamById(company.id) stream.on('error', next).pipe(res) }))
更多關於使用promise
處理異常的信息,請參閱這裏。
如下是一些你能夠對你的系統環境作的事,用於提高你應用的性能:
將NODE_ENV
設置爲「production」
保證你的應用在發生錯誤後自動重啓
使用集羣模式運行你的應用
緩存請求結果
使用負載均衡
使用反向代理
NODE_ENV
設置爲「production」
NODE_ENV
環境變量指明瞭應用當前的運行環境(開發或生產)。你能夠作的爲你的Express
提高性能的最簡單的事情之一,就是將NODE_ENV
設置爲「production」
。
將NODE_ENV
設置爲「production」
將使Express
:
緩存視圖模板
緩存CSS文件
生成更簡潔的錯誤信息
若是你想寫環境相關的代碼,你能夠經過process.env.NODE_ENV
來獲取運行時NODE_ENV
的值。不過須要注意的,檢查環境變量的值會形成少量的性能損失,因此不要有太多這類操做。
你可能已經習慣了SHELL
中設置環境變量,例如使用export
或.bash_profile
文件。可是你不該該在你的生產服務器上這麼作。你應該使用操做系統的初始化系統(systemd
或systemd
)。下一個章節將會更詳細的講述初始化系統,可是因爲設置NODE_ENV
是如此的重要以及簡單,因此咱們在這裏就列出它:
當使用Upstart
時,請在任務文件中使用env
關鍵字。例子:
# /etc/init/env.conf env NODE_ENV=production
更多信息,請參閱這裏。
當使用systemd
時,請在你的單元文件中使用Environment
指令。例子:
# /etc/systemd/system/myservice.service Environment=NODE_ENV=production
更多信息,請參閱這裏。
若是你正在使用StrongLoop Process Manager
,你也能夠參閱這篇文章。
在生產環境下,你必定不但願你的應用離線。因此你須要保證在你的應用發生錯誤時或你的服務器自身崩潰時,你的應用能夠自動重啓。雖然你可能不指望它們的發生,可是咱們須要更現實得預防它們,能夠經過:
使用一個進程管理員(process manager)庫來重啓你的應用
當你的操做系統崩潰時,使用它提供的初始化系統來重啓你的進程管理員。
Node.js
應用在遇到未處理異常時就會退出。你的首要任務是保證你的代碼的測試健全而且合理地處理了全部的異常。可是若有萬一,請準備一個機制來確保它的自動重啓。
在開發環境下,你能夠簡單地使用node server.js
這樣的命令來啓動你的應用。當時在生產環境下這麼作將是不被容許的。若是應用崩潰了,在你手動重啓它以前,它都會處於離線狀態。爲了保證你應用的自動重啓,請使用一個進程管理員,它能夠幫助你管理正在運行的應用。
除了保證你的應用的自動重啓,一個進程管理員還可使你:
獲取當前運行環境的性能表現和資源消耗狀況。
自動地修改環境設置
管理集羣(StrongLoop PM
和pm2
)
Node.js
世界裏比較流行的進程管理員有:
StrongLoop Process Manager
PM2
Forever
更多的它們之間的比較,你能夠參閱這裏。關於它們三者的簡介,你能夠參閱這篇文章。
接下來要保證的就是,在你的服務器重啓時,你的應用也會相應的重啓。儘管咱們認爲咱們的服務器是十分穩定的,但它們仍有掛掉的可能。因此爲了保證在你的服務器時重啓時你的應用也會重啓,請使用你操做系統內建的初始化系統。現在比較主流的是systemd
和Upstart
。
如下是經過你的Express
應用來使用初始化系統的兩種方法:
將你的應用運行於一個進程管理員中,而後將進程管理員設置爲系統的一個服務。這個是比較推薦的作法。
直接經過初始化系統運行你的應用。這個方法更爲簡單,但你卻享受不到進程管理員帶來的福利。
Systems
是一個linux
系統的服務管理員。大多數的linux
發行版都將它做爲默認的初始化系統。
一個systems
服務的配置文件也被稱爲一個單元文件,有一個.service
後綴。如下是一個直接管理Node.js
應用的例子:
[Unit] Description=Awesome Express App [Service] Type=simple ExecStart=<strong>/usr/local/bin/node /projects/myapp/index.js</strong> WorkingDirectory=<strong>/projects/myapp</strong> User=nobody Group=nogroup # Environment variables: Environment=<strong>NODE_ENV=production</strong> # Allow many incoming connections LimitNOFILE=infinity # Allow core dumps for debugging LimitCORE=infinity StandardInput=null StandardOutput=syslog StandardError=syslog Restart=always [Install] WantedBy=multi-user.target
更多關於systemd
的信息,請參閱這裏。
Upstart
是一個大多數linux
發行版均可用的系統工具,用於在系統啓動時啓動任務和服務,在系統關閉時中止它們,而且監控它們。你能夠先將你的Express
應用或進程管理員配置爲一個服務,而後Upstart
會自動地在系統重啓後重啓它們。
一個Upstart
服務被定義在一個任務配置文件中,有一個.conf
後綴。下面的例子展現瞭如何建立一個名爲「myapp」
的任務,且應用的入口是/projects/myapp/index.js
。
在/etc/init/
下建立一個名爲myapp.conf
的文件:
# When to start the process start on runlevel [2345] # When to stop the process stop on runlevel [016] # Increase file descriptor limit to be able to handle more requests limit nofile 50000 50000 # Use production mode env <strong>NODE_ENV=production</strong> # Run as www-data setuid www-data setgid www-data # Run from inside the app dir chdir <strong>/projects/myapp</strong> # The process to start exec <strong>/usr/local/bin/node /projects/myapp/index.js</strong> # Restart the process if it is down respawn # Limit restart attempt to 10 times within 10 seconds respawn limit 10 10
注意:這個腳本要求Upstart
1.4 或更新的版本,支持於Ubuntu
12.04-14.10。
除了自動重啓你的應用,Upstart
還爲你提供瞭如下命令:
start myapp – 手動啓動應用
restart myapp – 手動重啓應用
stop myapp – 手動退出應用
更多關於Upstart
的信息,請參閱這裏。
在多核的系統裏,你能夠經過啓動一個進程集羣來成倍了提高你應用的性能。一個集羣運行了你的應用的多個實例,理想狀況下,一個CPU覈對應一個實例。這樣,即可以在多個實例件進行負載均衡。
值得注意的是,因爲應用實例跑在不一樣的進程裏,因此它們並不分享同一塊內存空間。由於,應用裏的全部對象都是本地的,你不能夠在應用代碼裏維護狀態。不過,你可使用如redis
這樣的內存數據庫來存儲session
這樣的數據和狀態。
在集羣中,一個工做進程的崩潰不會影響到其餘的工做進程。因此除了性能因素以外,單獨工做進程崩潰的相互不影響也是另外一個使用集羣的好處。一個工做進程崩潰後,請確保記錄下日誌,而後從新經過cluster.fork()
建立一個新的工做進程。
Node.js
的cluster
模塊Node.js
提供了cluster
模塊來支持集羣。它使得一個主進程能夠建立出多個工做進程。可是,比起直接使用這個模塊,許多的庫已經爲你封裝了它,並提供了更多自動化的功能:如node-pm或cluser-service。
另外一個提高你應用性能的途徑是緩存請求的結果,這樣一來,對於同一個請求,你的應用就沒必要作多餘的重複動做。
使用一個如Varnish
或Nginx
這樣的緩存服務器能夠極大地提高你應用的響應速度。
不論一個應用優化地多麼好,一個單獨的實例老是有它的負載上限的。一個很好的解決辦法就是將你的應用跑上多個實例,而後在它們以前加上一個負載均衡器。
一個負載均衡器一般是一個反向代理,它接受負載,並將其均勻得分配給各個實例或服務器。你能夠經過Nginx
或HAProxy
十分方便地架設一個負載均衡器。
使用了負載均衡後,你能夠保證每一個請求都根據它的來源被設置了獨特session id
。固然,你也可使用如Redis
這樣的內存數據庫來存儲session
。更多詳情,能夠參閱這裏。
負載均衡是一個至關複雜的話題,更加細緻的討論已超過了本文的範疇。
一個反向代理被設置與web應用以前,用於支持各種對於請求的操做,如將請求發送給應用,自動處理錯誤頁,壓縮,緩存,提供靜態文件,負載均衡,等等。
在生產環境中,這裏推薦將Express
應用跑在Nginx
或HAProxy
以後。