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