黑人生命也是命。
支持平等正義倡議.

生產環境最佳實務:效能和可靠性

概觀

本文探討部署到生產環境的 Express 應用程式的效能和可靠性最佳實務。

這個主題明顯屬於「devops」的世界,涵蓋傳統的開發和營運。因此,資訊分為兩部分

在您的程式碼中可以做的事

以下是您可以在程式碼中執行的某些操作,以改善應用程式的效能

使用 gzip 壓縮

Gzip 壓縮可以大幅縮小回應主體的大小,進而提高 Web 應用程式的速度。在您的 Express 應用程式中使用 compression 中介軟體進行 gzip 壓縮。例如

const compression = require('compression')
const express = require('express')
const app = express()
app.use(compression())

對於製作中的高流量網站,實作壓縮的最佳方式是在反向代理層級實作(請參閱 使用反向代理)。在這種情況下,您不需要使用壓縮中介軟體。有關在 Nginx 中啟用 gzip 壓縮的詳細資訊,請參閱 Nginx 文件中的 模組 ngx_http_gzip_module

不要使用同步函式

同步函式和方法會在傳回之前綁定執行程序。呼叫同步函式一次可能會在幾微秒或毫秒內傳回,然而在高流量網站中,這些呼叫會累積並降低應用程式的效能。在製作中避免使用它們。

儘管 Node 和許多模組提供同步和非同步版本的函式,但在製作中務必使用非同步版本。唯一可以證明使用同步函式合理的時間是在初始啟動時。

如果您使用的是 Node.js 4.0+ 或 io.js 2.1.0+,您可以使用 --trace-sync-io 命令列旗標,在您的應用程式使用同步 API 時列印警告和堆疊追蹤。當然,您不希望在製作中使用它,而是確保您的程式碼已準備好進行製作。請參閱 node 命令列選項文件 以取得更多資訊。

正確執行記錄

一般來說,從應用程式記錄有兩個原因:除錯和記錄應用程式活動(基本上就是其他所有事情)。在開發中,使用 console.log()console.error() 將記錄訊息列印到終端機是很常見的做法。但 這些函式在目的地是終端機或檔案時是同步的,因此不適合用於製作,除非您將輸出導向到另一個程式。

用於除錯

如果您記錄的目的是除錯,那麼請不要使用 console.log(),而要使用特殊除錯模組,例如 debug。此模組讓您可以使用 DEBUG 環境變數來控制哪些除錯訊息(如果有)會傳送到 console.error()。為了讓您的應用程式完全非同步,您仍需要將 console.error() 導向到另一個程式。但這樣一來,您在製作時並不會真正除錯,對吧?

用於應用程式活動

如果您記錄的是應用程式活動(例如追蹤流量或 API 呼叫),請不要使用 console.log(),而要使用記錄函式庫,例如 WinstonBunyan。如需這兩個函式庫的詳細比較,請參閱 StrongLoop 部落格文章 比較 Winston 和 Bunyan Node.js 記錄

適當處理例外狀況

當 Node 應用程式遇到未捕捉的例外狀況時,會發生當機。不處理例外狀況,也不採取適當的動作,會讓 Express 應用程式當機並離線。如果您遵循下方 確保您的應用程式自動重新啟動 中的建議,您的應用程式將會從當機中復原。幸運的是,Express 應用程式的啟動時間通常很短。儘管如此,您還是希望一開始就能避免當機,而要做到這一點,您需要適當地處理例外狀況。

要確保您處理所有例外狀況,請使用下列技巧

在深入探討這些主題之前,您應該對 Node/Express 錯誤處理具備基本認識:使用錯誤優先回呼,以及在中間件中傳播錯誤。Node 使用「錯誤優先回呼」慣例,從非同步函數傳回錯誤,其中回呼函數的第一個參數是錯誤物件,後續參數則是結果資料。要表示沒有錯誤,請將 null 傳遞為第一個參數。回呼函數必須相應地遵循錯誤優先回呼慣例,才能有意義地處理錯誤。而在 Express 中,最佳做法是使用 next() 函數,透過中間件鏈傳播錯誤。

如需瞭解錯誤處理基本原理的更多資訊,請參閱

不該做的事

應該做的一件事是偵聽uncaughtException事件,當例外狀況一路冒泡回事件迴圈時會發出此事件。為uncaughtException新增事件偵聽器會變更遇到例外狀況的程序的預設行為;該程序會繼續執行,儘管有例外狀況。這聽起來像是防止您的應用程式當機的好方法,但繼續執行應用程式在發生未捕捉的例外狀況之後是一種危險的做法,不建議這麼做,因為程序狀態會變得不可靠且無法預測。

此外,使用uncaughtException已正式認定為粗糙。因此,偵聽uncaughtException只是一個壞主意。這就是我們建議使用多個程序和監督程序的原因:當機和重新啟動通常是從錯誤中復原最可靠的方法。

我們也不建議使用網域。它通常不會解決問題,而且是一個已棄用的模組。

使用 try-catch

Try-catch 是您可以用來捕捉同步程式碼中例外狀況的 JavaScript 語言建構。例如,使用 try-catch 來處理 JSON 分析錯誤,如下所示。

使用JSHintJSLint等工具來協助您找出隱含的例外狀況,例如未定義變數的參考錯誤

以下是使用 try-catch 來處理潛在程序當機例外狀況的範例。這個中間件函式接受一個名為「params」的查詢欄位參數,它是一個 JSON 物件。

app.get('/search', (req, res) => {
  // Simulating async operation
  setImmediate(() => {
    const jsonStr = req.query.params
    try {
      const jsonObj = JSON.parse(jsonStr)
      res.send('Success')
    } catch (e) {
      res.status(400).send('Invalid JSON string')
    }
  })
})

不過,try-catch 僅適用於同步程式碼。由於 Node 平台主要是非同步的(特別是在生產環境中),try-catch 無法捕捉很多例外狀況。

使用承諾

Promise 會處理使用then()的非同步程式碼區塊中的任何例外狀況(明確和隱含)。只要將.catch(next)新增到 Promise 鏈的結尾即可。例如

app.get('/', (req, res, next) => {
  // do some sync stuff
  queryDb()
    .then((data) => makeCsv(data)) // handle data
    .then((csv) => { /* handle csv */ })
    .catch(next)
})

app.use((err, req, res, next) => {
  // handle error
})

現在所有非同步和同步錯誤都會傳播到錯誤中介軟體。

不過,有兩個注意事項

  1. 所有非同步程式碼都必須傳回 Promise(發射器除外)。如果特定函式庫不傳回 Promise,請使用輔助函式(例如 Bluebird.promisifyAll())轉換基本物件。
  2. 事件發射器(例如串流)仍可能導致未捕捉的例外狀況。因此請務必妥善處理錯誤事件;例如
const wrap = fn => (...args) => fn(...args).catch(args[2])

app.get('/', wrap(async (req, res, next) => {
  const company = await getCompanyById(req.query.id)
  const stream = getLogoStreamById(company.id)
  stream.on('error', next).pipe(res)
}))

wrap() 函式是一個包裝器,它會捕捉被拒絕的 Promise,並呼叫 next(),並將錯誤作為第一個引數。如需詳細資訊,請參閱 使用 Promise、產生器和 ES7 在 Express 中進行非同步錯誤處理

如需有關使用 Promise 進行錯誤處理的更多資訊,請參閱 使用 Q 在 Node.js 中進行 Promise – 回呼函式的替代方案

在您的環境/設定中要執行的事項

以下是您可以在系統環境中執行的部分事項,以改善應用程式的效能

將 NODE_ENV 設定為「production」

NODE_ENV 環境變數會指定應用程式執行的環境(通常是開發或生產)。您可以執行的最簡單改善效能方法之一,就是將 NODE_ENV 設定為「生產」。

將 NODE_ENV 設定為「生產」會讓 Express

測試指出,只要執行此動作,就能將應用程式效能提升三倍!

如果您需要撰寫特定於環境的程式碼,可以使用 process.env.NODE_ENV 檢查 NODE_ENV 的值。請注意,檢查任何環境變數的值會產生效能損失,因此應謹慎執行。

在開發階段,您通常會在互動式 shell 中設定環境變數,例如使用 export.bash_profile 檔案。但一般來說,您不應該在生產伺服器上執行此動作;請改用作業系統的 init 系統 (systemd 或 Upstart)。下一段落將提供有關一般使用 init 系統的更多詳細資訊,但設定 NODE_ENV 對效能而言非常重要 (且容易執行),因此在此特別強調。

使用 Upstart 時,請在工作檔案中使用 env 關鍵字。例如

# /etc/init/env.conf
 env NODE_ENV=production

如需更多資訊,請參閱 Upstart 簡介、食譜和最佳實務

使用 systemd 時,請在單元檔案中使用 Environment 指令。例如

# /etc/systemd/system/myservice.service
Environment=NODE_ENV=production

如需更多資訊,請參閱 在 systemd 單元中使用環境變數

確保你的應用程式自動重新啟動

在生產環境中,您不希望應用程式離線,永遠不希望。這表示您需要確保應用程式在應用程式崩潰和伺服器本身崩潰時都能重新啟動。儘管您希望這兩種情況都不會發生,但實際上您必須透過以下方式考量這兩種可能性

如果 Node 應用程式遇到未捕捉的例外狀況,就會崩潰。您需要做的第一件事是確保您的應用程式經過良好測試,並處理所有例外狀況 (有關詳細資訊,請參閱 妥善處理例外狀況)。但作為一個安全措施,請建立一個機制來確保您的應用程式在崩潰時能自動重新啟動。

使用程序管理員

在開發過程中,您從命令列使用 node server.js 或類似的指令,便可輕鬆啟動應用程式。但這項操作在實際環境中卻是災難的開始。如果應用程式發生異常終止,您必須重新啟動它才能讓它重新上線。為確保應用程式在發生異常終止時能自動重新啟動,請使用處理程序。處理程序是應用程式的「容器」,它能簡化部署、提供高可用性,並讓您在執行階段管理應用程式。

除了在應用程式發生異常終止時重新啟動它之外,處理程序還能讓您

以下為 Node 最受歡迎的處理程序

若要比較這三個處理程序的個別功能,請參閱 http://strong-pm.io/compare/。若要更深入了解這三個處理程序,請參閱 Express 應用程式的處理程序

使用這些處理程序中的任何一個,都能讓您的應用程式保持執行,即使它偶爾會發生異常終止。

不過,StrongLoop PM 擁有許多專門針對實際環境部署的功能。您可以使用它和相關的 StrongLoop 工具來

如下所述,當您使用 init 系統將 StrongLoop PM 安裝為作業系統服務時,它會在系統重新啟動時自動重新啟動。因此,它會讓您的應用程式程序和叢集永遠保持運作。

使用 init 系統

可靠性的下一層是確保您的應用程式在伺服器重新啟動時重新啟動。系統仍然可能因為各種原因而停機。若要確保您的應用程式在伺服器崩潰時重新啟動,請使用內建於作業系統的 init 系統。目前使用中的兩個主要 init 系統為 systemdUpstart

有兩種方法可以使用 init 系統與您的 Express 應用程式

Systemd

Systemd 是 Linux 系統和服務管理員。大多數主要的 Linux 發行版都已採用 systemd 作為其預設的 init 系統。

systemd 服務設定檔稱為單元檔,其檔名以 .service 結尾。以下是管理 Node 應用程式的範例單元檔。請將 <尖括號> 中括起來的值替換為您的系統和應用程式

[Unit]
Description=<Awesome Express App>

[Service]
Type=simple
ExecStart=/usr/local/bin/node </projects/myapp/index.js>
WorkingDirectory=</projects/myapp>

User=nobody
Group=nogroup

# Environment variables:
Environment=NODE_ENV=production

# 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 的更多資訊,請參閱 systemd 參考(手冊頁面)

StrongLoop PM 作為 systemd 服務

您可以輕鬆地將 StrongLoop Process Manager 安裝為 systemd 服務。這樣一來,當伺服器重新啟動時,它會自動重新啟動 StrongLoop PM,然後重新啟動它所管理的所有應用程式。

若要將 StrongLoop PM 安裝為 systemd 服務

$ sudo sl-pm-install --systemd

然後使用下列方式啟動服務

$ sudo /usr/bin/systemctl start strong-pm

如需更多資訊,請參閱 設定生產主機(StrongLoop 文件)

Upstart

Upstart 是一個系統工具,可在許多 Linux 發行版上使用,用於在系統啟動期間啟動任務和服務,在關機期間停止任務和服務,並監督任務和服務。您可以將 Express 應用程式或處理序管理員設定為服務,然後 Upstart 會在應用程式或處理序管理員發生故障時自動重新啟動它。

Upstart 服務定義在作業設定檔(也稱為「作業」)中,其檔案名稱以 .conf 結尾。下列範例顯示如何為名為「myapp」的應用程式建立一個名為「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 NODE_ENV=production

# Run as www-data
setuid www-data
setgid www-data

# Run from inside the app dir
chdir /projects/myapp

# The process to start
exec /usr/local/bin/node /projects/myapp/index.js

# 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 還讓您可以使用下列指令

如需 Upstart 的更多資訊,請參閱 Upstart 簡介、食譜和最佳實務

StrongLoop PM 作為 Upstart 服務

您可以輕鬆地將 StrongLoop Process Manager 安裝為 Upstart 服務。安裝後,當伺服器重新啟動時,它會自動重新啟動 StrongLoop PM,然後 StrongLoop PM 會重新啟動它所管理的所有應用程式。

要將 StrongLoop PM 安裝為 Upstart 1.4 服務

$ sudo sl-pm-install

然後執行服務

$ sudo /sbin/initctl start strong-pm

注意:在不支援 Upstart 1.4 的系統上,指令略有不同。請參閱 設定生產主機 (StrongLoop 文件) 以取得更多資訊。

在叢集中執行你的應用程式

在多核心系統中,您可以透過啟動處理程序叢集來大幅提升 Node 應用程式的效能。叢集會執行多個應用程式執行個體,理想情況下每個 CPU 核心執行一個執行個體,藉此在各個執行個體之間分配負載和工作。

Balancing between application instances using the cluster API

重要:由於應用程式執行個體以個別處理程序執行,因此它們不會共用相同的記憶體空間。也就是說,物件是每個應用程式執行個體的區域性物件。因此,您無法在應用程式程式碼中維護狀態。不過,您可以使用類似 Redis 的記憶體內資料儲存庫來儲存與工作階段相關的資料和狀態。此注意事項適用於所有形式的橫向擴充,無論是使用多個處理程序或多個實體伺服器進行叢集。

在叢集應用程式中,工作人員處理程序可能會個別崩潰,而不會影響其他處理程序。除了效能優勢之外,故障隔離是執行應用程式處理程序叢集的另一個原因。每當工作人員處理程序崩潰時,務必記錄事件並使用 cluster.fork() 產生新的處理程序。

使用 Node 的叢集模組

透過 Node 的 叢集模組,可以實現叢集。這使主處理程序能夠產生工作人員處理程序,並在工作人員之間分配接收的連線。不過,與其直接使用這個模組,不如使用許多現成的工具之一,它們會自動為您執行這項工作;例如 node-pmcluster-service

使用 StrongLoop PM

如果您將應用程式部署到 StrongLoop Process Manager (PM),那麼您就可以利用叢集,而無需修改應用程式程式碼。

當 StrongLoop Process Manager (PM) 執行應用程式時,它會自動在叢集中執行它,工作程序的數量等於系統上的 CPU 核心數量。您可以使用 slc 命令列工具手動變更叢集中工作程序的數量,而無需停止應用程式。

例如,假設您已將應用程式部署到 prod.foo.com,且 StrongLoop PM 正在監聽埠 8701(預設),然後使用 slc 將叢集大小設定為八

$ slc ctl -C http://prod.foo.com:8701 set-size my-app 8

有關使用 StrongLoop PM 進行叢集的更多資訊,請參閱 StrongLoop 文件中的叢集

使用 PM2

如果您使用 PM2 部署應用程式,那麼您就可以利用叢集,而無需修改應用程式程式碼。您應該先確保您的應用程式是無狀態的,表示沒有任何本機資料儲存在處理程序中(例如,工作階段、WebSocket 連線等)。

使用 PM2 執行應用程式時,您可以啟用叢集模式,在叢集中執行它,工作階段的數量由您選擇,例如,與機器上可用的 CPU 數量相符。您可以使用 pm2 命令列工具手動變更叢集中處理程序的數量,而無需停止應用程式。

若要啟用叢集模式,請像這樣啟動您的應用程式

# Start 4 worker processes
$ pm2 start npm --name my-app -i 4 -- start
# Auto-detect number of available CPUs and start that many worker processes
$ pm2 start npm --name my-app -i max -- start

這也可以在 PM2 處理程序檔案(ecosystem.config.js 或類似檔案)中設定,方法是將 exec_mode 設定為 cluster,並將 instances 設定為要啟動的工作程序數量。

執行後,應用程式可以這樣擴充

# Add 3 more workers
$ pm2 scale my-app +3
# Scale to a specific number of workers
$ pm2 scale my-app 2

如需更多有關使用 PM2 進行叢集的資訊,請參閱 PM2 文件中的 叢集模式

快取請求結果

另一種改善生產效能的策略是快取要求的結果,讓您的應用程式不會重複執行相同的作業來服務重複的要求。

使用像 VarnishNginx(另請參閱 Nginx 快取)這樣的快取伺服器,大幅改善您應用程式的速度和效能。

使用負載平衡器

不論應用程式最佳化到什麼程度,單一執行個體只能處理有限的負載和流量。擴充應用程式的一種方法是執行多個執行個體,並透過負載平衡器分配流量。設定負載平衡器可以改善應用程式的效能和速度,並讓它擴充到單一執行個體無法達到的程度。

負載平衡器通常是反向代理伺服器,用於協調進出多個應用程式執行個體和伺服器的流量。您可以使用 NginxHAProxy,輕鬆為您的應用程式設定負載平衡器。

使用負載平衡時,您可能必須確保與特定工作階段 ID 相關的請求會連線到產生它們的程序。這稱為工作階段關聯性固定工作階段,可以使用上述建議,為工作階段資料使用資料儲存,例如 Redis(視您的應用程式而定)。如需討論,請參閱 使用多個節點

使用反向代理

反向代理伺服器會置於 Web 應用程式之前,除了將要求導向應用程式之外,還會對要求執行支援操作。它可以處理錯誤頁面、壓縮、快取、提供檔案和負載平衡等事項。

將不需要應用程式狀態知識的任務交給反向代理伺服器,可以釋放 Express 來執行專門的應用程式任務。因此,建議在生產環境中使用 Nginx 或 HAProxy 等反向代理伺服器來執行 Express。