[譯]Express在生產環境下的最佳實踐 - 性能和可靠性

前言

這將是一個分爲兩部分,內容是關於在生產環境下,跑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了,而是須要參閱Nginxngx_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環境變量來控制具體哪些信息會被打印。雖然這些方法也是同步的,但你必定不會在生產環境下進行調試吧?

爲了操做記錄

若是你正在爲了記錄應用的活動而打印日誌。那麼你可使用一些日誌庫如winstonBunyan,來替代console.log()。更多關於這兩個庫的詳情,能夠參閱這裏

合理地處理異常

Node.js在遇到未處理的異常時就會退出。若是沒有合理地捕獲並處理異常,這會使你的應用崩潰和離線。若是你使用了一個自動重啓的工具,那麼你的應用則會在崩潰後馬上重啓,並且幸運的是,Express應用的重啓時間一般都很快。可是無論怎樣,你都想要儘可能避免這種崩潰。

爲了保證你合理處理異常,請聽從如下指示:

  • 使用try-catch

  • 使用promise

不該該作的事

你不該該監聽全局事件uncaughtException。監聽該事件將會致使進程遇到未處理異常時的行爲被改變:進程將會忽略此異常並繼續運行。這聽上去很好,可是若是你的應用中存在未處理異常,繼續運行它是很是危險的,由於應用的狀態開始變得不可預測。

因此,監聽uncaughtException並非一個好主意,它已被官方地列爲了避免推薦的作法,而且之後可能會移除這個接口。咱們更推薦的是,使用多進程和自動重啓。

咱們一樣不推薦使用domains。它一般也並不能解決問題,而且已經是一個被標識爲棄用的模塊。

使用try-catch

Try-catch是一個JavaScript語言自帶的捕獲同步代碼的結構。使用try-catch,你能夠捕獲例如JSON解析錯誤這樣的異常。

使用JSHintJSLint這樣的工具則可讓你遠離引用錯誤或未定義變量這種隱式的異常。

一個使用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文件。可是你不該該在你的生產服務器上這麼作。你應該使用操做系統的初始化系統(systemdsystemd)。下一個章節將會更詳細的講述初始化系統,可是因爲設置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應用在遇到未處理異常時就會退出。你的首要任務是保證你的代碼的測試健全而且合理地處理了全部的異常。可是若有萬一,請準備一個機制來確保它的自動重啓。

使用進程管理員(process manager)

在開發環境下,你能夠簡單地使用node server.js這樣的命令來啓動你的應用。當時在生產環境下這麼作將是不被容許的。若是應用崩潰了,在你手動重啓它以前,它都會處於離線狀態。爲了保證你應用的自動重啓,請使用一個進程管理員,它能夠幫助你管理正在運行的應用。

除了保證你的應用的自動重啓,一個進程管理員還可使你:

  • 獲取當前運行環境的性能表現和資源消耗狀況。

  • 自動地修改環境設置

  • 管理集羣(StrongLoop PMpm2

Node.js世界裏比較流行的進程管理員有:

  • StrongLoop Process Manager

  • PM2

  • Forever

更多的它們之間的比較,你能夠參閱這裏。關於它們三者的簡介,你能夠參閱這篇文章

使用一個初始化系統

接下來要保證的就是,在你的服務器重啓時,你的應用也會相應的重啓。儘管咱們認爲咱們的服務器是十分穩定的,但它們仍有掛掉的可能。因此爲了保證在你的服務器時重啓時你的應用也會重啓,請使用你操做系統內建的初始化系統。現在比較主流的是systemdUpstart

如下是經過你的Express應用來使用初始化系統的兩種方法:

  • 將你的應用運行於一個進程管理員中,而後將進程管理員設置爲系統的一個服務。這個是比較推薦的作法。

  • 直接經過初始化系統運行你的應用。這個方法更爲簡單,但你卻享受不到進程管理員帶來的福利。

Systemd

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

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.jscluster模塊

Node.js提供了cluster模塊來支持集羣。它使得一個主進程能夠建立出多個工做進程。可是,比起直接使用這個模塊,許多的庫已經爲你封裝了它,並提供了更多自動化的功能:如node-pmcluser-service

緩存請求結果

另外一個提高你應用性能的途徑是緩存請求的結果,這樣一來,對於同一個請求,你的應用就沒必要作多餘的重複動做。

使用一個如VarnishNginx這樣的緩存服務器能夠極大地提高你應用的響應速度。

使用負載均衡

不論一個應用優化地多麼好,一個單獨的實例老是有它的負載上限的。一個很好的解決辦法就是將你的應用跑上多個實例,而後在它們以前加上一個負載均衡器。

一個負載均衡器一般是一個反向代理,它接受負載,並將其均勻得分配給各個實例或服務器。你能夠經過NginxHAProxy十分方便地架設一個負載均衡器。

使用了負載均衡後,你能夠保證每一個請求都根據它的來源被設置了獨特session id。固然,你也可使用如Redis這樣的內存數據庫來存儲session。更多詳情,能夠參閱這裏

負載均衡是一個至關複雜的話題,更加細緻的討論已超過了本文的範疇。

使用反向代理

一個反向代理被設置與web應用以前,用於支持各種對於請求的操做,如將請求發送給應用,自動處理錯誤頁,壓縮,緩存,提供靜態文件,負載均衡,等等。

在生產環境中,這裏推薦將Express應用跑在NginxHAProxy以後。

最後

原文連接:https://strongloop.com/strongblog/best-practices-for-express-in-production-part-two-performance-and-reliability/

相關文章
相關標籤/搜索