?
This document uses PHP Chinese website manual Release
本文討論部署到生產(chǎn)環(huán)境中的Express應(yīng)用程序的性能和可靠性最佳實踐。
這個話題顯然屬于“devops”世界,跨越傳統(tǒng)的發(fā)展和運營。因此,信息分為兩部分:
你的代碼要做的事情(開發(fā)部分):
使用gzip壓縮
不要使用同步功能
記錄正確
正確處理異常
在你的環(huán)境/設(shè)置(操作部分)中要做的事情:
將NODE_ENV設(shè)置為“生產(chǎn)”
確保您的應(yīng)用程序自動重啟
在集群中運行您的應(yīng)用程序
緩存請求結(jié)果
使用負(fù)載平衡器
使用反向代理
以下是您可以在代碼中執(zhí)行以提高應(yīng)用程序性能的一些事情:
使用gzip壓縮
不要使用同步功能
記錄正確
正確處理異常
Gzip壓縮可以大大減少響應(yīng)主體的大小,從而提高Web應(yīng)用程序的速度。在Express應(yīng)用程序中使用壓縮中間件進(jìn)行g(shù)zip壓縮。例如:
var compression = require('compression')var express = require('express')var app = express()app.use(compression())
對于生產(chǎn)中的高流量網(wǎng)站來說,實施壓縮的最佳方法是在反向代理級別實施它(請參閱使用反向代理)。在這種情況下,您不需要使用壓縮中間件。有關(guān)在Nginx中啟用gzip壓縮的詳細(xì)信息,請參閱Nginx文檔中的模塊ngx_http_gzip_module。
同步函數(shù)和方法將執(zhí)行過程捆綁在一起,直到它們返回。對同步功能的單個調(diào)用可能會在幾微秒或幾毫秒內(nèi)返回,但在高流量網(wǎng)站中,這些調(diào)用會加起來并降低應(yīng)用程序的性能。避免在生產(chǎn)中使用它們。
盡管節(jié)點和許多模塊提供了其功能的同步和異步版本,但始終在生產(chǎn)中使用異步版本。唯一一次可以證明同步功能是初次啟動時。
如果您使用的是Node.js 4.0+或io.js 2.1.0+,則--trace-sync-io
只要應(yīng)用程序使用同步API ,就可以使用命令行標(biāo)志來打印警告和堆棧跟蹤。當(dāng)然,你不想在生產(chǎn)中使用它,而是為了確保你的代碼已經(jīng)準(zhǔn)備好用于生產(chǎn)。有關(guān)更多信息,請參閱節(jié)點命令行選項文檔。
一般來說,從您的應(yīng)用程序進(jìn)行日志記錄有兩個原因:用于調(diào)試和記錄應(yīng)用程序活動(實質(zhì)上是其他所有內(nèi)容)。開發(fā)中使用console.log()
或console.error()
打印日志消息到終端是常見的做法。但是,當(dāng)目的地是終端或文件時,這些功能是同步的,所以它們不適合生產(chǎn),除非將輸出管道輸送到另一個程序。
如果你登錄用于調(diào)試的目的,而不是再使用console.log()
,使用特殊的調(diào)試模塊像調(diào)試。此模塊使您能夠使用DEBUG環(huán)境變量來控制發(fā)送給哪些調(diào)試消息console.err()
(如果有)。要保持你的應(yīng)用程序純粹是異步的,你仍然想要管道console.err()
到另一個程序。但是,那么你不是真的要在生產(chǎn)中進(jìn)行調(diào)試,對嗎?
如果您記錄應(yīng)用程序活動(例如跟蹤流量或API調(diào)用),而不是使用console.log()
,請使用Winston或Bunyan等日志庫。有關(guān)這兩個庫的詳細(xì)比較,請參閱StrongLoop博客文章比較Winston和Bunyan Node.js日志記錄。
節(jié)點應(yīng)用遇到未捕獲的異常時會崩潰。未處理例外情況并采取適當(dāng)?shù)拇胧鼓腅xpress應(yīng)用程序崩潰并脫機。如果您按照以下建議確保您的應(yīng)用在下面自動重新啟動,那么您的應(yīng)用將從崩潰中恢復(fù)。幸運的是,Express應(yīng)用程序的啟動時間通常很短。盡管如此,你首先要避免崩潰,要做到這一點,你需要正確處理異常。
為確保處理所有異常,請使用以下技術(shù):
使用try-catch
使用promises
在深入探討這些主題之前,您應(yīng)該對Node / Express錯誤處理有一個基本的了解:使用錯誤優(yōu)先回調(diào)以及在中間件中傳播錯誤。節(jié)點使用“錯誤優(yōu)先回調(diào)”約定來返回來自異步函數(shù)的錯誤,其中回調(diào)函數(shù)的第一個參數(shù)是錯誤對象,隨后是后續(xù)參數(shù)中的結(jié)果數(shù)據(jù)。要指示沒有錯誤,請將null作為第一個參數(shù)?;卣{(diào)函數(shù)必須相應(yīng)地遵循錯誤優(yōu)先回調(diào)約定來有意義地處理錯誤。在Express中,最佳做法是使用next()函數(shù)通過中間件鏈傳播錯誤。
有一件事你應(yīng)該不會做的是偵聽uncaughtException
事件,發(fā)出異常時氣泡回到事件循環(huán)的所有道路。添加事件偵聽器uncaughtException
將改變遇到異常的進(jìn)程的默認(rèn)行為; 盡管例外,該過程仍將繼續(xù)運行。這可能聽起來像是一種防止應(yīng)用程序崩潰的好方法,但在未捕獲的異常之后繼續(xù)運行應(yīng)用程序是一種危險的做法,并且不推薦使用,因為進(jìn)程的狀態(tài)變得不可靠且不可預(yù)知。
此外,使用uncaughtException
被官方認(rèn)定為原油。所以傾聽uncaughtException
是一個壞主意。這就是為什么我們推薦像多個進(jìn)程和主管一樣的東西:崩潰和重啟通常是從錯誤中恢復(fù)的最可靠的方法。
我們也不建議使用域名。它通常不能解決問題并且是不推薦的模塊。
Try-catch是一種JavaScript語言結(jié)構(gòu),可用于捕獲同步代碼中的異常。例如,使用try-catch來處理JSON解析錯誤,如下所示。
使用的工具,如JSHint或JSLint的幫你找到像隱含例外未定義變量引用錯誤。
以下是使用try-catch處理潛在的進(jìn)程崩潰異常的示例。這個中間件函數(shù)接受一個名為“params”的查詢字段參數(shù),它是一個JSON對象。
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平臺主要是異步的(特別是在生產(chǎn)環(huán)境中),try-catch不會捕獲很多異常。
Promises將處理使用的異步代碼塊中的任何異常(顯式和隱式)then()
。只需添加.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})
現(xiàn)在,異步和同步的所有錯誤都會傳播到錯誤中間件。
但是,有兩個警告:
您的所有異步代碼都必須返回promise(發(fā)射器除外)。如果某個特定的庫沒有返回promise,請使用Bluebird.promisifyAll()等助手函數(shù)轉(zhuǎn)換基礎(chǔ)對象。
事件發(fā)射器(如流)仍可能導(dǎo)致未捕獲的異常。所以確保你正確處理錯誤事件; 例如:
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)}))
有關(guān)使用承諾進(jìn)行錯誤處理的更多信息,請參閱:
帶有Promise,Generators和ES7的Express中的異步錯誤處理
使用Q的Node.js中的承諾 - 回調(diào)的替代方法
您可以在系統(tǒng)環(huán)境中執(zhí)行以下操作來提高應(yīng)用的性能:
將NODE_ENV設(shè)置為“生產(chǎn)”
確保您的應(yīng)用程序自動重啟
在集群中運行您的應(yīng)用程序
緩存請求結(jié)果
使用負(fù)載平衡器
使用反向代理
NODE_ENV環(huán)境變量指定應(yīng)用程序運行的環(huán)境(通常是開發(fā)或生產(chǎn))。您可以通過將NODE_ENV設(shè)置為“生產(chǎn)”來提高性能,這是最簡單的事情之一。
將NODE_ENV設(shè)置為“生產(chǎn)”使Express:
緩存視圖模板。
緩存從CSS擴展中生成的CSS文件。
生成較少的詳細(xì)錯誤消息。
測試表明,這樣做可以將應(yīng)用程序性能提高三倍!
如果您需要編寫特定于環(huán)境的代碼,則可以使用以下命令檢查NODE_ENV的值process.env.NODE_ENV
。請注意,檢查任何環(huán)境變量的值會導(dǎo)致性能損失,因此應(yīng)該謹(jǐn)慎執(zhí)行。
在開發(fā)中,您通常在交互式shell中設(shè)置環(huán)境變量,例如通過使用export
或您的.bash_profile
文件。但總的來說,你不應(yīng)該在生產(chǎn)服務(wù)器上這樣做; 相反,使用你的操作系統(tǒng)的init系統(tǒng)(systemd或Upstart)。下一節(jié)提供了有關(guān)通常使用init系統(tǒng)的更多詳細(xì)信息,但設(shè)置NODE_ENV對性能非常重要(并且易于執(zhí)行),因此它在此處突出顯示。
隨著Upstart,env
在你的工作文件中使用關(guān)鍵字。例如:
# /etc/init/env.conf env NODE_ENV=production
欲了解更多信息,請參閱Upstart簡介,食譜和最佳實踐。
使用systemd時,請Environment
在單元文件中使用該指令。例如:
# /etc/systemd/system/myservice.service Environment=NODE_ENV=production
有關(guān)更多信息,請參閱在systemd單元中使用環(huán)境變量。
在生產(chǎn)中,您不希望應(yīng)用程序脫機。這意味著如果應(yīng)用程序崩潰并且服務(wù)器本身崩潰,則需要確保它重新啟動。雖然你希望這些事件都不會發(fā)生,但實際上你必須通過以下方式來解釋兩種可能性:
使用進(jìn)程管理器在崩潰時重新啟動應(yīng)用程序(和節(jié)點)。
操作系統(tǒng)崩潰時,使用操作系統(tǒng)提供的init系統(tǒng)重新啟動進(jìn)程管理器。也可以在沒有進(jìn)程管理器的情況下使用init系統(tǒng)。
節(jié)點應(yīng)用程序遇到未捕獲的異常時會崩潰。您需要做的最重要的事情是確保您的應(yīng)用程序經(jīng)過充分測試并處理所有異常(有關(guān)詳細(xì)信息,請參閱正確處理異常)。但作為失敗保險箱,建立一個機制來確保當(dāng)你的應(yīng)用程序崩潰時,它會自動重啟。
在開發(fā)中,您只需從命令行node server.js
或類似的東西開始您的應(yīng)用程序。但在生產(chǎn)中這樣做是一種災(zāi)難。如果應(yīng)用程序崩潰,它將脫機,直到您重新啟動它。要確保您的應(yīng)用在崩潰時重新啟動,請使用進(jìn)程管理器。流程管理器是便于部署,提供高可用性并使您能夠在運行時管理應(yīng)用程序的應(yīng)用程序的“容器”。
除了在應(yīng)用程序崩潰時重新啟動應(yīng)用程序外,進(jìn)程管理器還可以使您:
深入了解運行時性能和資源消耗。
動態(tài)修改設(shè)置以提高性能。
控制集群(StrongLoop PM和pm2)。
最受歡迎的Node進(jìn)程管理器如下所示:
StrongLoop Process Manager
PM2
Forever
有關(guān)三個進(jìn)程管理器的逐個功能比較,請參閱http://strong-pm.io/compare/。有關(guān)這三者的更詳細(xì)介紹,請參閱Express應(yīng)用程序的流程管理器。
使用這些流程管理器中的任何一個都足以讓您的應(yīng)用程序保持運行,即使它不時崩潰。
但是,StrongLoop PM具有許多專門針對生產(chǎn)部署的功能。您可以使用它和相關(guān)的StrongLoop工具來:
在本地構(gòu)建和打包應(yīng)用,然后將其安全地部署到生產(chǎn)系統(tǒng)。
如果因任何原因崩潰,請自動重新啟動您的應(yīng)用。
遠(yuǎn)程管理您的群集。
查看CPU配置文件和堆快照以優(yōu)化性能并診斷內(nèi)存泄漏。
查看您的應(yīng)用程序的性能指標(biāo)。
通過Nginx負(fù)載均衡器集成控制輕松擴展到多臺主機。
如下所述,當(dāng)您使用init系統(tǒng)將StrongLoop PM作為操作系統(tǒng)服務(wù)安裝時,系統(tǒng)將在系統(tǒng)重新啟動時自動重新啟動。因此,它將使您的應(yīng)用程序進(jìn)程和群集永遠(yuǎn)存活。
下一層可靠性是確保您的應(yīng)用程序在服務(wù)器重新啟動時重新啟動。由于各種原因,系統(tǒng)仍然可能停機。要確保您的應(yīng)用程序在服務(wù)器崩潰時重新啟動,請使用內(nèi)置于您的操作系統(tǒng)中的init系統(tǒng)。目前使用的兩個主要init系統(tǒng)是systemd和Upstart。
There are two ways to use init systems with your Express app:
在進(jìn)程管理器中運行您的應(yīng)用程序,并使用init系統(tǒng)將進(jìn)程管理器作為服務(wù)安裝。當(dāng)應(yīng)用程序崩潰時,進(jìn)程管理器將重新啟動您的應(yīng)用程序,并且當(dāng)系統(tǒng)重新啟動時,init系統(tǒng)將重新啟動進(jìn)程管理器。這是推薦的方法。
使用init系統(tǒng)直接運行您的應(yīng)用程序(和節(jié)點)。這更簡單一些,但是您沒有獲得使用流程管理器的額外優(yōu)勢。
Systemd是一個Linux系統(tǒng)和服務(wù)管理器。大多數(shù)主要的Linux發(fā)行版都采用systemd作為其默認(rèn)的init系統(tǒng)。
系統(tǒng)服務(wù)配置文件稱為單元文件,文件名以.service結(jié)尾。以下是直接管理Node應(yīng)用程序的示例單元文件(將粗體文本替換為系統(tǒng)和應(yīng)用程序的值):
[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=nullStandardOutput=syslog StandardError=syslog Restart=always[Install]WantedBy=multi-user.target
有關(guān)systemd的更多信息,請參閱systemd引用(手冊頁)。
您可以輕松地將StrongLoop Process Manager作為systemd服務(wù)安裝。完成后,當(dāng)服務(wù)器重新啟動時,它將自動重新啟動StrongLoop PM,然后重新啟動所有正在管理的應(yīng)用程序。
要將StrongLoop PM安裝為systemd服務(wù),請執(zhí)行以下操作:
$ sudo sl-pm-install --systemd
然后開始服務(wù):
$ sudo /usr/bin/systemctl start strong-pm
有關(guān)更多信息,請參閱設(shè)置生產(chǎn)主機(StrongLoop文檔)。
Upstart是許多Linux發(fā)行版中可用的系統(tǒng)工具,用于在系統(tǒng)啟動期間啟動任務(wù)和服務(wù),在關(guān)機期間停止它們并監(jiān)督它們。您可以將Express應(yīng)用程序或流程管理器配置為服務(wù),然后Upstart會在崩潰時自動重啟。
Upstart服務(wù)在作業(yè)配置文件(也稱為“作業(yè)”)中定義,文件名以.conf
。結(jié)尾。以下示例顯示如何為名為“myapp”的應(yīng)用程序創(chuàng)建名為“myapp”的作業(yè),主文件位于該處/projects/myapp/index.js
。
使用以下內(nèi)容創(chuàng)建一個名為myapp.conf
at 的文件/etc/init/
(將粗體文本替換為系統(tǒng)和應(yīng)用程序的值):
# 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上受支持。
由于作業(yè)配置為在系統(tǒng)啟動時運行,因此應(yīng)用程序?qū)㈦S操作系統(tǒng)一起啟動,并在應(yīng)用程序崩潰或系統(tǒng)關(guān)閉時自動重新啟動。
除了自動重新啟動應(yīng)用程序,Upstart還允許您使用這些命令:
start myapp
– Start the app
restart myapp
– Restart the app
stop myapp
– Stop the app.
有關(guān)Upstart的更多信息,請參閱Upstart Intro,Cookbook和Best Practices。
您可以輕松安裝StrongLoop Process Manager作為Upstart服務(wù)。完成后,當(dāng)服務(wù)器重新啟動時,它將自動重新啟動StrongLoop PM,然后重新啟動所有正在管理的應(yīng)用程序。
將StrongLoop PM安裝為Upstart 1.4服務(wù):
$ sudo sl-pm-install
然后運行該服務(wù):
$ sudo /sbin/initctl start strong-pm
注意:在不支持Upstart 1.4的系統(tǒng)上,命令略有不同。有關(guān)更多信息,請參閱設(shè)置生產(chǎn)主機(StrongLoop文檔)。
在多核系統(tǒng)中,您可以通過啟動一組進(jìn)程來多次提高Node應(yīng)用程序的性能。群集運行應(yīng)用程序的多個實例,理想的情況是每個CPU核心上有一個實例,從而在實例之間分配負(fù)載和任務(wù)。
重要提示:由于應(yīng)用程序?qū)嵗鳛閱为毜倪M(jìn)程運行,因此它們不共享相同的內(nèi)存空間。也就是說,對象是每個應(yīng)用程序?qū)嵗谋镜貙ο?。因此,您無法在應(yīng)用程序代碼中維護(hù)狀態(tài)。但是,您可以使用像Redis這樣的內(nèi)存數(shù)據(jù)存儲來存儲會話相關(guān)的數(shù)據(jù)和狀態(tài)。此警告適用于基本上所有形式的水平縮放,無論是使用多個進(jìn)程聚集還是多臺物理服務(wù)器。
在集群應(yīng)用程序中,工作進(jìn)程可能會單獨崩潰而不會影響其他進(jìn)程。除了性能優(yōu)勢之外,故障隔離是運行一組應(yīng)用程序進(jìn)程的另一個原因。每當(dāng)工作進(jìn)程崩潰時,務(wù)必確保使用cluster.fork()記錄事件并產(chǎn)生一個新進(jìn)程。
集群可以通過節(jié)點的集群模塊實現(xiàn)。這使主進(jìn)程能夠派生工作進(jìn)程并在工作進(jìn)程間分配傳入連接。但是,不是直接使用這個模塊,而是使用其中的許多工具之一自動完成工作。例如節(jié)點-pm或群集服務(wù)。
如果您將應(yīng)用程序部署到StrongLoop Process Manager(PM),那么您可以利用群集而不修改應(yīng)用程序代碼。
當(dāng)StrongLoop進(jìn)程管理器(PM)運行應(yīng)用程序時,它會自動在具有與系統(tǒng)上的CPU核心數(shù)量相等的工作人員數(shù)量的群集中運行。您可以使用slc命令行工具手動更改集群中工作進(jìn)程的數(shù)量,而無需停止應(yīng)用程序。
例如,假設(shè)您已將應(yīng)用程序部署到prod.foo.com,并且StrongLoop PM正在偵聽端口8701(默認(rèn)值),然后使用slc將群集大小設(shè)置為八:
$ slc ctl -C http://prod.foo.com:8701 set-size my-app 8
有關(guān)使用StrongLoop PM進(jìn)行群集的更多信息,請參閱StrongLoop文檔中的群集。
提高生產(chǎn)性能的另一個策略是緩存請求的結(jié)果,以便您的應(yīng)用不會重復(fù)該操作來重復(fù)提供相同的請求。
使用像Varnish或Nginx這樣的緩存服務(wù)器(另請參閱Nginx緩存)可以大大提高應(yīng)用程序的速度和性能。
無論應(yīng)用程序的優(yōu)化程度如何,單個實例只能處理有限數(shù)量的負(fù)載和流量。擴展應(yīng)用程序的一種方法是運行它的多個實例并通過負(fù)載均衡器分配流量。設(shè)置負(fù)載平衡器可以提高應(yīng)用程序的性能和速度,并使其能夠擴展比單個實例更多的功能。
負(fù)載平衡器通常是一種逆向代理,用于協(xié)調(diào)來往于多個應(yīng)用程序?qū)嵗头?wù)器的流量。您可以使用Nginx或HAProxy輕松地為您的應(yīng)用程序設(shè)置負(fù)載平衡器。
通過負(fù)載平衡,您可能必須確保與特定會話ID關(guān)聯(lián)的請求連接到發(fā)起它們的進(jìn)程。這被稱為會話親緣關(guān)系或粘性會話,并且可以通過上面的建議來解決,以使用數(shù)據(jù)存儲(如Redis)作為會話數(shù)據(jù)(取決于您的應(yīng)用程序)。有關(guān)討論,請參閱使用多個節(jié)點。
反向代理位于Web應(yīng)用程序的前端,并對請求執(zhí)行支持操作,而不是將請求發(fā)送到應(yīng)用程序。它可以處理錯誤頁面,壓縮,緩存,服務(wù)文件和負(fù)載平衡等等。
將不需要應(yīng)用程序狀態(tài)知識的任務(wù)移交給反向代理可以釋放Express以執(zhí)行專門的應(yīng)用程序任務(wù)。出于這個原因,建議在生產(chǎn)中使用Nginx或HAProxy之類的反向代理。