我後悔了,真的永遠不要在生產中直接執行Node.js

一定要賺錢發表於2020-10-09

有時候我也在想我是否真的知道很多東西。

就在幾周前,我正在和一個朋友談話,他不經意間提到,“你永遠都不會在生產中直接使用 Node 來執行程式”。我強烈點頭,表示我不會在生產中直接執行 Node,原因可能每個人都知道。但是我並不知道,我應該知道原因嗎?我還能繼續寫程式碼嗎?

如果讓我繪製一個維恩圖來表示我所知道的和其他人都知道的東西,它看起來就像這樣:

[圖片上傳失敗...(image-65a5fe-1602237568053)]

順便一提,我年紀越大,那個小點就會越小。

Alicia Liu 建立了一個更好的圖表,改變了我的生活。她說這種情況更像是…

[圖片上傳失敗...(image-5fbc40-1602237568053)]

我非常喜歡這個圖表,因為我希望它是真實的。我不想把餘生都過得像一個微不足道的萎縮藍點一樣。

太戲劇化了。怪潘多拉。當我寫這篇文章的時候,我無法控制接下來要發生什麼。而 Dashboard Confessional 真是一副毒藥。

好吧,假設 Alicia 的圖表是真的,我想與大家分享一下我現在對在生產中執行 Node 應用程式的一些認識。也許在這個問題上我們的相對維恩圖沒有重疊。

首先,讓我們弄清楚“永遠不要在生產中直接通過 Node 執行程式”的說法。

永遠不要直接執行生產中的 Node

這個說法可能對,也可能不對。我們一起來探討下這個說法是怎麼得到的。首先,讓我們看看為什麼不對。

假設我們有一個簡單的 Express 伺服器。我能想到的最簡單的 Express 伺服器如下:

const express = require("express");
const app = express();const port = process.env.PORT || 3000;
// 檢視 http://localhost:3000
app.get("/", function(req, res) {
  res.send("Again I Go Unnoticed");
});app.listen(port, () => console.log(`Example app listening on port ${port}!`));

通過定義在 package.json 中的啟動指令碼來執行它。

"scripts": {
  "start": "node index.js",
  "test": "pfffft"
}

[圖片上傳失敗...(image-18464e-1602237568053)]

這裡會有兩個問題。第一個是開發問題,還有一個是生產問題。

開發問題指的是當我們修改程式碼的時候,我們必須停止並且再次啟動應用程式來得到我們更改後的效果。

我們通常會使用某種 Node 程式管理器,例如 supervisor 或 nodemon 來解決這個問題。這些包會監聽我們的專案,並會在我們提交更改時重啟伺服器。我通常都會這樣做。

"scripts": {
  "dev": "npx supervisor index.js",
  "start": "node index.js"
}

然後執行 npm run dev。請注意,我在這裡執行 npx supervisor,它允許我們在未安裝 supervisor 包的情況下使用它。

我們的另外一個問題是我們仍然直接在針對 Node 執行,我們已經說過這很糟糕,現在我們將要找出原因。

我要在這裡新增另一個路由,嘗試從不存在的磁碟讀取檔案。這是一個很容易在任何實際應用程式中出現的錯誤。

const express = require("express");
const app = express();const fs = require("fs");
const port = process.env.PORT || 3000;
// 檢視 http://localhost:3000
app.get("/", function(req, res) {
  res.send("Again I Go Unnoticed");
});app.get("/read", function(req, res) {
  // 這個路由不存在  fs.createReadStream("my-self-esteem.txt");
});app.listen(port, () => console.log(`Example app listening on port ${port}!`));

如果我們直接針對 Node 執行 npm start 並且導航到 read 端點,頁面會報錯,因為這個檔案不存在。

[圖片上傳失敗...(image-4f4f49-1602237568053)]

這沒什麼大不了的,對吧?這只是一個錯誤,碰巧發生了。

不。這很重要。如果你返回終端,你會看到應用程式已經完全關閉了。

[圖片上傳失敗...(image-c1e315-1602237568053)]

這意味著,如果你回到瀏覽器並嘗試訪問站點的根 URL,你會得到相同的錯誤頁面。一個方法中的錯誤導致應用中所有的路由都失效了。

這是很糟糕的。這就是人們說 “永遠都不要直接在生產中執行 Node”

好。如果我們無法在生產中直接執行 Node,那麼在生產中執行 Node 的正確方法是什麼?

生產中 Node 的選項

我們有幾個選擇。

其中之一就是在生產中簡單地使用類似 supervisor 或 nodemon 的工具,就像我們在開發中使用它們一樣。這可行,但這些工具有點輕量級。更好的選擇是 pm2。

pm2 支援

pm2 是一個 Node 程式管理器,有很多有用的功能。就像其他的 “JavaScript” 庫一樣,你可以使用 npm 全域性安裝它 —— 或者你也可以再次使用 npx。這裡不再贅述。

有很多使用 pm2 執行程式的方法。最簡單的就是在入口檔案呼叫 pm2 start。

"scripts": {
  "start": "pm2 start index.js",
  "dev": "npx supervisor index.js"
},

終端會顯示這些內容:

在這裡插入圖片描述

這是我們在 pm2 監控的後臺執行的程式。如果你訪問 read 端點把程式搞崩了,pm2 將自動重新啟動它。你在終端不會看到任何內容,因為它在後臺執行。如果你想看到 pm2 正在做什麼,你可以執行 pm2 log 0。這個 0 是我們想要檢視日誌的程式的 ID。

[圖片上傳失敗...(image-d3bd5b-1602237568053)]

接下來!你可以看到,pm2 會在應用程式由於未處理的錯誤而停機時重新啟動應用程式。

我們還可以提取開發命令,併為我們準備PM2監視檔案,在任何更改發生時重新啟動程式。

"scripts": {
  "start": "pm2 start index.js --watch",
  "dev": "npx supervisor index.js"
},

請注意,因為 pm2 在後臺執行,所以你不能只是通過 ctrl+c 來終止正在執行的 pm2 程式。你必須通過傳遞程式的 ID 或者名稱來停止程式。

pm2 stop 0

pm2 stop index

另請注意,pm2 會儲存對程式的引用,以便你可以重新啟動它。

[圖片上傳失敗...(image-fed778-1602237568053)]

如果要刪除該程式引用,則需要執行 pm2 delete。你可以使用一個命令 delete 來停止和刪除程式。

pm2 delete index

我們也可以使用 pm2 來執行我們應用程式的多個程式。pm2 會自動平衡這些例項的負載。

使用 pm2 fork 模式的多個程式

pm2 有很多配置選項,它們包含在一個 “ecosystem” 檔案中。可以通過執行 pm2 init來建立一個。你會得到以下的內容:

module.exports = {
  apps: [
    {
      name: "Express App",
      script: "index.js",
      instances: 4,
      autorestart: true,
      watch: true,
      max_memory_restart: "1G",
      env: {
        NODE_ENV: "development"
      },
      env_production: {
        NODE_ENV: "production"
      }
    }
  ]
};

本文不會去講關於“部署”的內容,因為我對“部署”也不太瞭解。

“應用程式”部分定義了希望 pm2 執行和監視的應用程式。你可以執行不止一個應用程式。許多這些配置設定可能是不言而喻的。這裡我要關注的是例項設定。

pm2 可以執行你的應用程式的多個例項。你可以傳入多個你想執行的例項,pm2 都可以啟動它們。因此,如果我們想執行 4 個例項,我們可以建立以下配置檔案。

module.exports = {
  apps: [
    {
      name: "Express App",
      script: "index.js",
      instances: 4,
      autorestart: true,
      watch: true,
      max_memory_restart: "1G",
      env: {
        NODE_ENV: "development"
      },
      env_production: {
        NODE_ENV: "production"
      }
    }
  ]
};

然後我們使用 pm2 start 來執行它。

[圖片上傳失敗...(image-929de9-1602237568052)]

pm2 現在會以“叢集”模式執行。這些程式中的每一個都會在我的計算機上的不同 CPU 上執行,具體取決於我擁有的核心數量。如果我們想在不知道擁有多個核心的情況下為每個核心執行一個程式,我們就可以將 max 引數傳遞給 instances 引數來進行配置。

{
   ...   instances: "max",
   ...}

讓我們看看我的這臺機器上有幾個核心。

[圖片上傳失敗...(image-6c500b-1602237568052)]

8個核心!哇。我要在我的微軟發行的機器上安裝 Subnautica。別告訴他們我說過。

在分隔的 CPU 上執行程式的好處是,即使有一個程式執行異常,佔用了 100% 的 CPU,其他程式依然可以保持執行。pm2 將根據需要將 CPU 上的程式加倍。

你可以使用 pm2 進行更多操作,包括監視和以其他方式處理那些討厭的 environment variables。

另外一個注意事項:如果由於某種原因,你希望 pm2 執行 npm start 指令。你可以通過執行 npm 作為程式並傳遞 – start。“start” 之前的空格在這裡非常重要。

pm2 start npm -- start

在 Azure AppService 中,預設在後臺包含 pm2。如果要在 Azure 中使用 pm2,則無需將其包含在 package.json 檔案中。你可以新增一個 ecosystem 檔案,然後就可以使用了。

[圖片上傳失敗...(image-6724bd-1602237568052)]

好!既然我們已經瞭解了關於 pm2 的所有內容,那麼讓我們談談為什麼你可能不想使用它,而且它可能確實可以直接針對 Node 執行。

在生產中直接針對 Node 執行

我對此有一些疑問,所以我聯絡了 Tierney Cyren,它是巨大的橙色知識圈的一部分,特別是在 Node 方面。

Tierney 指出使用基於 Node 的程式管理器(如 pm2)有一些缺點。

主要原因是不應該使用 Node 來監視 Node。你肯定不希望用你正在監視的東西監視它自己。就像你在週五晚上讓我十幾歲的兒子監督他自己一樣:結果會很糟糕嗎?可能,也可能不會。。

Tierney 建議你不要使用 Node 程式管理器來執行應用程式。相反,在更高階別上有一些東西可以監視應用程式的多個單獨例項。例如,一個理想的設定是,如果你有一個 Kubernetes 叢集,你的應用程式在不同的容器上執行。然後 Kubernetes 可以監控這些容器,如果其中任何容器發生故障,它可以將它們恢復並且報告健康狀況。

在這種情況下,你可以直接針對Node執行,因為你正在更高階別進行監視。

事實證明,Azure 已經在做這件事了。如果我們不將 pm2 ecosystem 檔案推送到 Azure,它將使用我們的 package.json 檔案執行指令碼來啟動應用程式,我們可以直接針對Node執行。

"scripts": {
  "start": "node index.js"
}

在這種情況下,我們直接針對 Node 執行,沒關係。如果應用程式崩潰,你會發現它又回來了。那是因為在 Azure 中,你的應用程式在一個容器中執行。Azure 負責容器排程,並知道何時去更新。

[圖片上傳失敗...(image-a67aed-1602237568052)]

但這裡仍然只有一個例項。容器崩潰後需要一秒鐘才能重新聯機,這意味著你的使用者可能會有幾秒鐘的停機時間。

理想情況下,你希望執行多個容器。解決方案是將應用程式的多個例項部署到多個 Azure AppService 站點,然後使用 Azure Front Door 在單個 IP 地址下對應用程式進行負載均衡。Front Door 知道容器何時關閉,並且將流量路由到應用程式的其他健康例項。

Azure Front Door Service | Microsoft Azure
使用 Azure Front Door 交付,保護和跟蹤全球分散式微服務應用程式的效能

systemd

Tierney 的另一個建議是使用 systemd 執行 Node。我不是很瞭解(或者說根本不知道)systemd,我已經把這句話弄錯過一次了,所以我會用 Tierney 自己的原話來表述:

只有在部署中訪問 Linux 並控制 Node 在服務級別上啟動的方式時,才有可能實現此選項。如果你在一個長時間執行的 Linux 虛擬機器中執行 node.js 程式,比如說 Azure 虛擬機器,那麼使用 systemd 執行 node.js 是個不錯的選擇。如果你只是將檔案部署到類似於 Azure AppService 或 Heroku 的服務中,或者執行在類似於 Azure 容器例項的容器化環境中,那麼你可以避開此選項。

  • 使用 Systemd 執行 Node.js 應用程式 —— 第一部分
  • 你已經在 Node 中編寫了下一個很棒的應用程式,並且你已經準備釋出它。這意味著你可以…

Node.js 工作執行緒

Tierney 還希望你知道 Node 中有工作執行緒,這可以讓你在多個執行緒上啟動你的應用程式,從而無需像 pm2 這樣的東西。也許吧。我不知道,我沒看過這篇文章。

  • Node.js v11.14.0 Documentation
  • The worker_threads module enables the use of threads that execute JavaScript in parallel. To access it: const worker =…

做個成熟的開發者

Tierney 的最後一個建議就是像一個成熟的開發者一樣處理錯誤和編寫測試。但是誰有時間呢?

小圓圈永駐

現在你知道這個藍色小圓圈裡的大部分東西了。剩下的只是關於 emo 樂隊和啤酒的無用事情。

有關 pm2, Node 和 Azure 的更多資訊,請檢視以下資源:

  • pm2.keymetrics.io/
  • VS Code 上的 Node.js 部署
  • 將簡單的 Node 站點部署到 Azure

相關文章