使用 Node.js Express 的最佳实践
Production best practices: performance and reliability
本文討論部署到生產的 Express 應用程序的性能和可靠性最佳實踐。
這個話題顯然屬于“devops”世界,涵蓋傳統的開發和運營。 因此,信息分為兩部分:
在您的代碼中要做的事情(開發部分)
- 使用 gzip 壓縮
- 不要使用同步函數
- 正確記錄
- 正確處理異常
在您的環境/設置中要做的事情(操作部分)
- 將 NODE_ENV 設置為“生產”
- 確保您的應用自動重啟
- 在集群中運行您的應用程序
- 緩存請求結果
- 使用負載均衡器
- 使用反向代理
Use gzip compression
Gzip 壓縮可以大大減小響應主體的大小,從而提高 Web 應用程序的速度。 在您的 Express 應用程序中使用 compression 進行 gzip 壓縮。 例如:
var compression = require('compression') var express = require('express') var app = express() app.use(compression())對于生產中的高流量網站,實施壓縮的最佳方法是在反向代理級別實施它。 在這種情況下,您不需要使用 compression 中間件。 有關在 Nginx 中啟用 gzip 壓縮的詳細信息,請參閱 Nginx 文檔中的模塊 ngx_http_gzip_module。
Don’t use synchronous functions
同步函數和方法會在它們返回之前阻塞正在執行的進程。 對同步函數的單個調用可能會在幾微秒或幾毫秒內返回,但是在高流量網站中,這些調用會累加并降低應用程序的性能。 避免在生產中使用它們。
盡管 Node 和許多模塊提供了它們功能的同步和異步版本,但在生產中始終使用異步版本。 唯一可以證明同步功能合理的時間是在初始啟動時。
如果您使用的是 Node.js 4.0+ 或 io.js 2.1.0+,您可以使用 --trace-sync-io 命令行標志在您的應用程序使用同步 API 時打印警告和堆棧跟蹤。 當然,您不想在生產中使用它,而是要確保您的代碼已準備好用于生產。
Do logging correctly
通常,從您的應用程序進行日志記錄有兩個原因:用于調試和記錄應用程序活動(本質上是其他所有內容)。 使用 console.log() 或 console.error() 將日志消息打印到終端是開發中的常見做法。 但是當目標是終端或文件時,這些函數是同步的,因此它們不適合生產,除非您將輸出通過管道傳輸到另一個程序。
如果您出于調試目的進行日志記錄,那么不要使用 console.log(),而是使用像 debug 這樣的特殊調試模塊。 此模塊使您能夠使用 DEBUG 環境變量來控制將哪些調試消息發送到 console.error()(如果有)。 為了保持你的應用完全異步,你仍然希望通過管道將 console.error() 傳遞給另一個程序。
如果您要記錄應用活動(例如,跟蹤流量或 API 調用),請不要使用 console.log(),而是使用像 Winston 或 Bunyan 這樣的日志庫。 有關這兩個庫的詳細比較,請參閱 StrongLoop 博客文章 Comparing Winston and Bunyan Node.js Logging.
Handle exceptions properly
Node 應用程序在遇到未捕獲的異常時崩潰。 不處理異常并采取適當的措施將使您的 Express 應用程序崩潰并下線。 如果您遵循下面確保您的應用程序自動重新啟動中的建議,那么您的應用程序將從崩潰中恢復。 幸運的是,Express 應用程序的啟動時間通常很短。 盡管如此,您首先要避免崩潰,為此,您需要正確處理異常。
為確保您處理所有異常,請使用以下技術:
- try-catch
- promises
在深入研究這些主題之前,您應該對 Node/Express 錯誤處理有一個基本的了解:使用錯誤優先回調,以及在中間件中傳播錯誤。 Node 使用“錯誤優先回調”約定從異步函數返回錯誤,其中回調函數的第一個參數是錯誤對象,后跟參數中的結果數據。 要指示沒有錯誤,請將 null 作為第一個參數傳遞。 回調函數必須相應地遵循錯誤優先回調約定才能有意義地處理錯誤。 而在 Express 中,最佳實踐是使用 next() 函數通過中間件鏈傳播錯誤。
What not to do
您不應該做的一件事是偵聽 uncaughtException 事件,當異常冒泡一直返回到事件循環時會發出該事件。 為 uncaughtException 添加事件監聽器將改變遇到異常的進程的默認行為; 盡管有異常,該進程仍將繼續運行。 這聽起來像是防止您的應用程序崩潰的好方法,但在未捕獲的異常之后繼續運行應用程序是一種危險的做法,不建議這樣做,因為進程的狀態變得不可靠且不可預測。
此外,使用 uncaughtException 被官方認為是粗暴的。所以監聽 uncaughtException 只是一個壞主意。 這就是為什么我們推薦多個進程和主管之類的東西:崩潰和重新啟動通常是從錯誤中恢復的最可靠方法。
我們也不建議使用 domains.它通常不能解決問題,并且是不推薦使用的模塊。
Use try-catch
Try-catch 是一種 JavaScript 語言結構,可用于捕獲同步代碼中的異常。 例如,使用 try-catch 來處理 JSON 解析錯誤,如下所示。
使用諸如 JSHint 或 JSLint 之類的工具來幫助您查找隱式異常,例如未定義變量上的引用錯誤。
以下是使用 try-catch 處理潛在進程崩潰異常的示例。 這個中間件函數接受一個名為“params”的查詢字段參數,它是一個 JSON 對象。
app.get('/search', (req, res) => {// Simulating async operationsetImmediate(() => {var jsonStr = req.query.paramstry {var jsonObj = JSON.parse(jsonStr)res.send('Success')} catch (e) {res.status(400).send('Invalid JSON string')}}) })但是,try-catch 僅適用于同步代碼。 因為 Node 平臺主要是異步的(特別是在生產環境中),try-catch 不會捕獲很多異常。
Use promises
Promise 將處理使用 then() 的異步代碼塊中的任何異常(顯式和隱式)。 只需將 .catch(next) 添加到承諾鏈的末尾即可。 例如:
app.get('/', (req, res, next) => {// do some sync stuffqueryDb().then((data) => makeCsv(data)) // handle data.then((csv) => { /* handle csv */ }).catch(next) })app.use((err, req, res, next) => {// handle error })現在所有異步和同步錯誤都會傳播到錯誤中間件。
但是,有兩個警告:
-
所有異步代碼都必須返回承諾(發射器除外)。 如果特定庫不返回承諾,請使用 Bluebird.promisifyAll() 等輔助函數轉換基礎對象。
-
事件發射器(如流)仍然會導致未捕獲的異常。 因此,請確保正確處理錯誤事件; 例如:
wrap() 函數是一個包裝器,它捕獲被拒絕的承諾并調用 next() 并將錯誤作為第一個參數。
更多細節可以參考這篇博客:Asynchronous Error Handling in Express with Promises, Generators and ES7.
Set NODE_ENV to “production”
NODE_ENV 環境變量指定應用程序運行的環境(通常是開發環境或生產環境)。 為了提高性能,您可以做的最簡單的事情之一是將 NODE_ENV 設置為“production”。
將 NODE_ENV 設置為“production”使得 Express:
- 緩存視圖模板。
- 緩存從 CSS 擴展生成的 CSS 文件。
- 生成不太詳細的錯誤消息。
如果您需要編寫特定于環境的代碼,您可以使用 process.env.NODE_ENV 檢查 NODE_ENV 的值。 請注意,檢查任何環境變量的值都會導致性能下降,因此應謹慎進行。
在開發中,您通常在交互式 shell 中設置環境變量,例如使用 export 或 .bash_profile 文件。 但一般來說,你不應該在生產服務器上這樣做; 相反,請使用您操作系統的初始化系統(systemd 或 Upstart)。 下一節提供了有關使用 init 系統的更多詳細信息,但設置 NODE_ENV 對性能非常重要(并且易于操作),因此在此處突出顯示。
使用 Upstart,在您的作業文件中使用 env 關鍵字。 例如:
# /etc/init/env.confenv NODE_ENV=production使用 systemd,在單元文件中使用 Environment 指令。 例如:
# /etc/systemd/system/myservice.service Environment=NODE_ENV=productionEnsure your app automatically restarts
在生產中,您永遠不希望您的應用程序處于離線狀態。 這意味著您需要確保它在應用程序崩潰和服務器本身崩潰時重新啟動。 盡管您希望這兩種情況都不會發生,但實際上您必須通過以下方式對這兩種情況進行說明:
-
使用進程管理器在崩潰時重新啟動應用程序(和節點)。
-
使用操作系統提供的 init 系統在操作系統崩潰時重新啟動進程管理器。 也可以在沒有進程管理器的情況下使用 init 系統。
如果遇到未捕獲的異常,節點應用程序就會崩潰。 您需要做的最重要的事情是確保您的應用程序經過良好測試并處理所有異常。
但作為一種故障安全措施,應采用一種機制來確保當您的應用程序崩潰時,它會自動重新啟動。
Use a process manager
在開發中,您只需使用 node server.js 或類似的東西從命令行啟動您的應用程序。 但是在生產中這樣做會導致災難。 如果應用程序崩潰,它將處于離線狀態,直到您重新啟動它。 為確保您的應用程序在崩潰時重新啟動,請使用進程管理器。 流程管理器是應用程序的“容器”,可促進部署、提供高可用性并使您能夠在運行時管理應用程序。
除了在應用程序崩潰時重新啟動應用程序之外,進程管理器還可以讓您:
-
深入了解運行時性能和資源消耗。
-
動態修改設置以提高性能。
-
控制集群(StrongLoop PM 和 pm2)。
下面是三個比較流行的進程管理器:
- StrongLoop Process Manager
- PM2
- Forever
有關三個流程管理器的逐個功能比較,請參閱 http://strong-pm.io/compare/。
使用這些進程管理器中的任何一個都足以讓您的應用程序保持正常運行,即使它不時崩潰。
Use an init system
下一層可靠性是確保您的應用程序在服務器重新啟動時重新啟動。 由于各種原因,系統仍可能出現故障。 為確保您的應用程序在服務器崩潰時重新啟動,請使用操作系統內置的 init 系統。 目前使用的兩個主要初始化系統是 systemd 和 Upstart。
有兩種方法可以在 Express 應用程序中使用 init 系統:
-
在進程管理器中運行您的應用程序,并使用 init 系統將進程管理器安裝為服務。 當應用程序崩潰時,進程管理器將重新啟動您的應用程序,當操作系統重新啟動時,init 系統將重新啟動進程管理器。 這是推薦的方法。
-
直接使用 init 系統運行您的應用程序(和 Node)。 這有點簡單,但您無法獲得使用進程管理器的額外優勢。
Systemd
Systemd 是一個 Linux 系統和服務管理器。 大多數主要的 Linux 發行版都采用 systemd 作為它們的默認初始化系統。
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=infinityStandardInput=null StandardOutput=syslog StandardError=syslog Restart=always[Install] WantedBy=multi-user.targetRun your app in a cluster
在多核系統中,您可以通過啟動一組進程來將 Node 應用程序的性能提高許多倍。 一個集群運行應用程序的多個實例,理想情況下每個 CPU 內核上有一個實例,從而在實例之間分配負載和任務。
[圖片]
重要提示:由于應用程序實例作為單獨的進程運行,因此它們不共享相同的內存空間。 也就是說,對象對于應用程序的每個實例都是本地的。 因此,您無法在應用程序代碼中維護狀態。 但是,您可以使用像 Redis 這樣的內存中數據存儲來存儲與會話相關的數據和狀態。 這個警告基本上適用于所有形式的水平擴展,無論是多進程集群還是多物理服務器。
在集群應用程序中,工作進程可以單獨崩潰而不影響其余進程。 除了性能優勢之外,故障隔離是運行應用進程集群的另一個原因。 每當工作進程崩潰時,請始終確保記錄該事件并使用 cluster.fork () 生成一個新進程。
Using 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 并將實例設置為要啟動的工作程序數量來配置。
運行后,應用程序可以像這樣縮放:
# Add 3 more workers $ pm2 scale my-app +3 # Scale to a specific number of workers $ pm2 scale my-app 2Cache request results
在生產中提高性能的另一個策略是緩存請求的結果,這樣您的應用程序就不會重復操作來重復處理相同的請求。
使用 Varnish 或 Nginx 等緩存服務器(另請參閱 Nginx 緩存)可以大大提高應用程序的速度和性能。
Use a load balancer
無論應用程序如何優化,單個實例只能處理有限的負載和流量。 擴展應用程序的一種方法是運行它的多個實例并通過負載均衡器分配流量。 設置負載均衡器可以提高應用程序的性能和速度,并使其能夠比單個實例擴展更多。
負載均衡器通常是一個反向代理,用于協調進出多個應用程序實例和服務器的流量。 您可以使用 Nginx 或 HAProxy 輕松地為您的應用程序設置負載均衡器。
通過負載平衡,您可能必須確保與特定會話 ID 關聯的請求連接到發起它們的進程。 這稱為親緣會話或粘性會話,可以通過上面的建議解決,使用諸如 Redis 之類的數據存儲來存儲會話數據(取決于您的應用程序)。
Use a reverse proxy
反向代理位于 Web 應用程序的前面,除了將請求定向到應用程序之外,還對請求執行支持操作。 它可以處理錯誤頁面、壓縮、緩存、提供文件和負載平衡等。
將不需要應用程序狀態知識的任務移交給反向代理可以釋放 Express 來執行專門的應用程序任務。 出于這個原因,建議在生產中使用反向代理(如 Nginx 或 HAProxy)運行 Express。
總結
以上是生活随笔為你收集整理的使用 Node.js Express 的最佳实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 不如不爱不难受歌词 不如不爱不难受歌词是
- 下一篇: 西风骑瘦马 公子是你吗是什么歌 西风骑瘦