本文討論部署至正式作業之 Express 應用程式的效能與可靠性最佳作法。
顯然地,這個主題屬於 “devops” 領域,涵蓋了傳統開發和作業兩者。因此,資訊分為兩大部分:
以下是您可以在程式碼中執行的一些作法,藉以改良您應用程式的效能:
Gzip 壓縮可以大幅減少回應內文的大小,從而提高 Web 應用程式的速度。請使用 compression 中介軟體,在您的 Express 應用程式中進行 gzip 壓縮。例如:
const compression = require('compression')
const express = require('express')
const app = express()
app.use(compression())
在正式作業中,如果網站的資料流量極高,落實執行壓縮最好的作法是在反向 Proxy 層次實作它(請參閱使用反向 Proxy)。在該情況下,就不需使用壓縮中介軟體。如需在 Nginx 中啟用 gzip 壓縮的詳細資料,請參閱 Nginx 說明文件中的 ngx_http_gzip_module 模組。
同步函數和方法直到傳回前,會阻礙執行程序的進行。單次呼叫同步函數,可能在數微秒或毫秒傳回,不過,在高資料流量的網站中,這些呼叫往往會累加,並降低應用程式效能。請避免在正式作業中使用它們。
雖然 Node 和許多模組會提供其函數的同步與非同步版本,在正式作業中,請一律使用非同步版本。唯一有理由使用同步函數的時機是在最初啟動之時。
如果您使用 Node.js 4.0+ 或 io.js 2.1.0+,每當您的應用程式使用同步 API 時,您可以使用 --trace-sync-io
指令行旗標,來列印警告和堆疊追蹤。當然,在正式作業中您其實不會想使用此旗標,但這可確保您的程式碼可準備用於正式作業中。如需相關資訊,請參閱 io.js 2.1.0 每週更新。
在開發中,您可以使用 res.sendFile() 來提供靜態檔案。但是在正式作業中卻不能這樣做,因為此函數得讀取檔案系統,才能取得每一個檔案要求,如此會遇到明顯的延遲,並影響應用程式的整體效能。請注意,res.sendFile()
並非透過更具效率的 sendfile 系統呼叫來實作。
請改用 serve-static 中介軟體(或同等項目),此中介軟體能有效提供 Express 應用程式的檔案。
甚至更好的作法是使用反向 Proxy 來提供靜態檔案;如需相關資訊,請參閱使用反向 Proxy。
一般而言,從您的應用程式進行記載的原因有二:為了除錯,以及為了記載應用程式活動(其實就是除錯之外的每一項)。使用 console.log()
或 console.err()
將日誌訊息列印至終端機,在開發中是常見作法。但是當目的地是終端機或檔案時,這些函數是同步的,除非您將輸出引導至另一個程式,這些函數並不適用於正式作業。
如果您為了除錯而記載,則不要使用 console.log()
,請改用 debug 之類的特殊除錯模組。這個模組可讓您使用 DEBUG 環境變數,來控制哪些除錯訊息(若有的話)要送往 console.err()
。為了讓應用程式完全維持非同步,您仍得將 console.err()
引導至另一個程式。但之後在正式作業中,實際上您並不會進行除錯,不是嗎?
如果您要記載應用程式活動(例如,追蹤資料流量或 API 呼叫),則不要使用 console.log()
,請改用 Winston 或
Bunyan 之類的記載程式庫。如需這兩種程式庫的詳細比較,請參閱 StrongLoop 部落格文章 Comparing Winston and Bunyan Node.js Logging。
Node 應用程式一旦遇到未捕捉到的異常狀況,就會當機。如果不處理異常狀況,並採取適當的動作,您的 Express 應用程式會當機並且離線。如果您遵循下方確定您的應用程式自動重新啟動中的建議,應用程式就能從當機回復。幸好 Express 應用程式的啟動時間通常很短。然而,您會希望一開始就避免當機,如果要這樣做,您需要適當處理異常狀況。
為了確保您能處理所有的異常狀況,請使用下列技術:
在分別討論這兩個主題之前,您對 Node/Express 錯誤處理方式應有基本的瞭解:使用「錯誤優先回呼」,並將錯誤傳播至中介軟體。Node 從非同步函數傳回錯誤時,會採用「錯誤優先回呼」慣例,其中,回呼函數的第一個參數是錯誤物件,接著是後續參數中的結果資料。如果要指出無錯誤,會傳遞 null 作為第一個參數。回呼函數必須同樣遵循「錯誤優先回呼」慣例,才能實際處理錯誤。在 Express 中,最佳作法是使用 next() 函數,透過中介軟體鏈來傳播錯誤。
如需進一步瞭解錯誤處理的基本概念,請參閱:
有一件事不能做,就是接聽 uncaughtException
事件,此事件是在回歸事件迴圈期間不斷引發異常狀況時產生的。新增 uncaughtException
的事件接聽器,會使遇到異常狀況的程序變更其預設行為;儘管發生異常狀況,該程序會繼續執行。阻止應用程式當機,似乎是個好辦法,但是在未捕捉到異常狀況之後,又繼續執行應用程式,卻是危險作法而不建議這麼做,因為程序的狀態會變得不可靠且無法預測。
此外,使用 uncaughtException
被公認為拙劣作法,這裡有一份提案,指出如何將它從核心移除。因此,接聽 uncaughtException
並不可取。這是我們建議採取多重程序和監督程式等事項的原因:當機再重新啟動,通常是從錯誤回復最可靠的作法。
我們也不建議使用 domains。它通常不能解決問題,並且是個已淘汰的模組。
try-catch 是一種 JavaScript 語言建構,可用來捕捉同步程式碼中的異常狀況。例如,如以下所示,利用 try-catch 來處理 JSON 剖析錯誤。
使用 JSHint 或 JSLint 之類的工具,有助您尋找隱含的異常狀況,例如參照未定義變數中的錯誤。
下列範例顯示如何使用 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 不會捕捉大量的異常狀況。
只要非同步程式碼區塊使用 then()
,promise 就會處理其中的任何異常狀況(包括明確和隱含)。只需在 promise 鏈尾端新增 .catch(next)
即可。例如:
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
})
現在,所有非同步與同步錯誤都會傳播到錯誤中介軟體。
不過,請注意下列兩項警告:
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)
}))
如需使用 promise 來處理錯誤的相關資訊,請參閱:
以下是您可以在系統環境中執行的一些作法,藉以改良您應用程式的效能:
NODE_ENV 環境變數用來指定應用程式的執行環境(通常是開發或正式作業)。若要改良效能,其中一個最簡單的作法是將 NODE_ENV 設為 “production”。
將 NODE_ENV 設為 “production”,可讓 Express:
測試指出單單這樣做,就能提高 3 倍的應用程式效能!
如果您需要撰寫環境特定的程式碼,您可以使用 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 Intro, Cookbook and Best Practices。
採用 systemd 時,請在單位檔案中使用 Environment
指引。例如:
# /etc/systemd/system/myservice.service
Environment=NODE_ENV=production
如需相關資訊,請參閱 Using Environment Variables In systemd Units。
如果您使用 StrongLoop Process Manager,您也可以在將 StrongLoop PM 安裝成服務時,設定環境變數。
在正式作業中,您始終不希望您的應用程式離線。也就是說,不論是應用程式當機,或是伺服器本身當機,您都需要確保它會重新啟動。儘管最好這些事件都不要發生,您仍必須務實看待這兩種可能的情況,其作法如下:
Node 應用程式一旦遇到未捕捉到的異常狀況,就會當機。首要之務是確定您的應用程式已妥善測試,且已處理所有的異常狀況(請參閱適當處理異常狀況,以取得詳細資料)。但是萬全的作法是落實執行機制,以確保萬一您的應用程式當機,它會自動重新啟動。
在開發中,只需從指令行使用 node server.js
或類似指令,就會啟動應用程式。但在正式作業中這樣做,卻會成為禍因。如果應用程式當機,就會離線直到您重新啟動它為止。為了確保應用程式會在當機時重新啟動,請使用程序管理程式。程序管理程式是一個應用程式的「儲存器」,有助於部署、提供高可用性,並可讓您在執行時期管理應用程式。
除了在應用程式當機時重新啟動它,程序管理程式還可讓您:
最普及的 Node 程序管理程式如下:
有關這三種程序管理程式的特性比較,請參閱 http://strong-pm.io/compare/。
即使您的應用程式不時發生當機,這些程序管理程式不論哪一個都足以讓您的應用程式維持作用中。
不過,StrongLoop PM 有許多特性明確以正式作業部署為目標。您可以使用它和相關的 StrongLoop 工具來:
如同以下說明,當您使用 init 系統將 StrongLoop PM 安裝成作業系統服務時,它會自動隨系統一起重新啟動。因此,它會讓您的應用程式程序和叢集永遠維持作用中。
接下來的可靠性層級是確保您的應用程式會隨伺服器一起重新啟動。系統仍可能因各種不同的原因而關閉。為了確保您的應用程式會在伺服器當機時重新啟動,請使用您作業系統內建的 init 系統。現今兩個通行的主要 init 系統是 systemd 和 Upstart。
init 系統若要與 Express 應用程式搭配使用,其作法有二:
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 Process Manager 安裝成 systemd 服務很簡單。完成之後,當伺服器重新啟動時,就會自動重新啟動 StrongLoop PM,之後它就會重新啟動其所管理的所有應用程式。
將 StrongLoop PM 安裝成 systemd 服務:
$ sudo sl-pm-install --systemd
然後使用下列指令來啟動服務:
$ sudo /usr/bin/systemctl start strong-pm
如需相關資訊,請參閱 Setting up a production host(StrongLoop 說明文件)。
Upstart 是一個可在許多 Linux 發行套件中使用的系統工具,它會在系統啟動期間啟動作業和服務、在關機期間停止它們,並且監督它們。您可以將 Express 應用程式或程序管理程式配置成服務,之後 Express 應用程式或程序管理程式一旦發生當機,Upstart 就會自動重新啟動它。
Upstart 服務定義在工作配置檔(亦稱為 “job”)中,其副名結尾是 .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
附註:這個 Script 需要 Upstart 1.4 或更新版本,且 Ubuntu 12.04-14.10 支援該 Upstart 版本。
由於工作是配置成在系統啟動時執行,您的應用程式會隨作業系統一起啟動,並在應用程式當機或系統關閉時自動重新啟動。
除了自動重新啟動應用程式,Upstart 可讓您使用下列指令:
start myapp
– 啟動應用程式restart myapp
– 重新啟動應用程式stop myapp
– 停止應用程式。如需 Upstart 的相關資訊,請參閱 Upstart Intro, Cookbook and Best Practises。
將 StrongLoop Process Manager 安裝成 Upstart 服務很簡單。完成之後,當伺服器重新啟動時,就會自動重新啟動 StrongLoop PM,之後它就會重新啟動其所管理的所有應用程式。
將 StrongLoop PM 安裝成 Upstart 1.4 服務:
$ sudo sl-pm-install
然後使用下列指令來執行服務:
$ sudo /sbin/initctl start strong-pm
附註:在不支援 Upstart 1.4 的系統上,指令略有不同。如需相關資訊,請參閱 Setting up a production host(StrongLoop 說明文件)。
在多核心系統中,您可以啟動程序叢集,多次提高 Node 應用程式的效能。叢集會執行該應用程式的多個實例,理論上,每一個 CPU 核心上各有一個實例,因此負載和作業會分散在這些實例之間。
重要事項:由於應用程式實例是以個別程序形式執行,因此不會共用相同的記憶體空間。也就是說,物件位於每一個應用程式實例本端。因此,您無法在應用程式碼中維護狀態。不過,您可以利用 Redis 等之類的記憶體內資料儲存庫,來儲存階段作業的相關資料和狀態。不論叢集是由多個程序或多部實體伺服器組成,這項警告其實適用於所有的水平調整形式。
在叢集化的應用程式中,工作者程序可個別當機,而不會影響其餘的程序。除了效能優點,執行應用程式程序叢集的另一個原因是,可將失效隔離。只要有工作者程序當機,一律要確定會記載事件,並利用 cluster.fork() 來衍生新程序。
利用 Node 的叢集模組,即可達成叢集作業。這可讓主要程序衍生工作者程序,並將送入的連線分散在這些工作者之間。不過,與其直接使用這個模組,更好的作法是使用其中提供的一個工具,自動為您執行叢集作業; 例如 node-pm 或 cluster-service。
如果您將應用程式部署至 StrongLoop Process Manager (PM),您可以善用叢集作業,且不需修改應用程式碼。
當 StrongLoop Process Manager (PM) 執行應用程式時,它會自動在叢集中執行它,且該叢集中的工作者數目等於系統上的 CPU 核心數。您可以使用 slc 指令行工具,直接手動變更工作者程序數目,而不需停止應用程式。
舉例來說,假設您將應用程式部署至 prod.foo.com,且 StrongLoop PM 是在埠 8701(預設值)接聽,請使用 slc 將叢集大小設為 8:
$ slc ctl -C http://prod.foo.com:8701 set-size my-app 8
如需利用 StrongLoop PM 執行叢集作業的相關資訊,請參閱 StrongLoop 說明文件中的叢集作業。
在正式作業中改良效能的另一項策略是快取要求的結果,這樣您的應用程式就不會重複執行作業而反覆處理相同的要求。
使用 Varnish 或 Nginx(另請參閱 Nginx 快取)等之類的快取伺服器,可大幅改良您應用程式的速度與效能。
不論如何將應用程式最佳化,單一實例所能處理的負載量與資料流量有限。調整應用程式的其中一個作法是執行其多個實例,並透過負載平衡器來分散資料流量。設定負載平衡器可改良您應用程式的效能和速度,且透過其單一實例,使該應用程式得以多次調整。
負載平衡器通常是一個反向 Proxy,負責協調與多個應用程式實例和伺服器之間的資料流量。利用 Nginx 或 HAProxy,就能輕鬆設定您應用程式的負載平衡器。
如果進行負載平衡,您可能得確定與特定階段作業 ID 相關聯的要求,會連接至發出該要求的程序。這就是所謂的階段作業親緣性或組合階段作業,如果要解決此情況,可按照上述建議,使用 Redis 等之類的資料儲存庫來儲存階段作業資料(視您的應用程式而定)。相關討論請參閱使用多個節點。
StrongLoop Process Manager 整合了 Nginx Controller,因此配置多主機正式作業環境配置更簡單。如需相關資訊,請參閱 Scaling to multiple servers(StrongLoop 說明文件)。
反向 Proxy 位於 Web 應用程式前面,除了將要求引導至應用程式,也會對要求執行支援的作業。除此之外,它還可以處理錯誤頁面、壓縮、快取、提供的檔案,以及負載平衡。
將不需要瞭解應用程式狀態的作業移交給反向 Proxy,使 Express 更有餘裕執行特殊的應用程式作業。基於此因,在正式作業中,建議讓 Express 在 Nginx 或 HAProxy 等之類的反向 Proxy 背後執行。