JavaScript >> Javascript 文檔 >  >> Node.js

如何向 Node.js 添加集群支持

如何在生產環境中使用 Node.js 集群模塊來利用多核處理器。

從本質上講,JavaScript 是一種單線程語言。這意味著當您告訴 JavaScript 完成一組指令時(例如,創建 DOM 元素、處理按鈕單擊或在 Node.js 中從文件系統中讀取文件),它會處理這些指令中的每一個時間,以線性方式。

無論它在哪台計算機上運行,它都會執行此操作。如果您的計算機具有 8 核處理器和 64GB 內存,那麼您在該計算機上運行的任何 JavaScript 代碼都將在單個線程或內核中運行。

相同的規則適用於 Node.js 應用程序。因為 Node.js 基於 V8 JavaScript 引擎,所以適用於 JavaScript 的相同規則也適用於 Node.js。

在構建 Web 應用程序時,這可能會讓人頭疼。隨著您的應用程序越來越受歡迎(或複雜性)並需要處理更多的請求和額外的工作,如果您只依賴單個線程來處理該工作,您將遇到瓶頸——請求丟失、服務器無響應、或中斷已經在服務器上運行的工作。

幸運的是,Node.js 有一個解決方法:cluster 模塊。

cluster 模塊通過分散 Node.js 應用程序的工作負載,幫助我們利用計算機(服務器)的全部處理能力。例如,如果我們有一個 8 核處理器,我們可以將工作分散到所有 8 個內核,而不是把我們的工作孤立在一個內核上。

使用 cluster ,我們的第一個核心成為“主”,所有額外的核心成為“工人”。當請求進入我們的應用程序時,主進程會執行輪詢式檢查,詢問“哪個工作人員現在可以處理這個請求?”第一個滿足要求的工人獲得請求。沖洗並重複。

設置示例服務器

為了開始並給我們一些上下文,我們將使用 Express 作為 HTTP 服務器來設置一個簡單的 Node.js 應用程序。我們想在我們的電腦上新建一個文件夾,然後運行:

npm init --force && npm i express

這將使用 NPM(Node.js 包管理器)初始化我們的項目,然後安裝 express NPM 包。

完成後,我們將要創建一個 index.js 在我們的新項目文件夾中的文件:

/index.js

import express from "express";

const app = express();

app.use("/", (req, res) => {
  res.send(
    `"Sometimes a slow gradual approach does more good than a large gesture." - Craig Newmark`
  );
});

app.listen(3000, () => {
  console.log("Application running on port 3000.");
});

在這裡,我們 import express from 'express'express 進入我們的代碼。接下來,我們創建一個 express 的實例 通過將該導入作為函數調用並將其分配給變量 app .

接下來,我們在根 / 處定義一個簡單的路由 我們的應用程序的 app.use() 並返回一些文本以確保一切正常(這只是為了展示,不會對我們的集群實現產生任何實際影響)。

最後,我們調用 app.listen() 通過 3000 作為端口(我們將能夠在 http://localhost:3000 訪問正在運行的應用程序 在我們啟動應用程序後在我們的瀏覽器中)。雖然消息本身並不是非常重要,但作為 app.listen() 的第二個參數 當我們的應用程序啟動時,我們傳遞一個回調函數來註銷一條消息。當我們需要驗證我們的集群支持是否正常工作時,這將派上用場。

為了確保這一切正常,在您的終端中,cd 進入項目文件夾,然後運行 node index.js .如果您看到以下內容,則一切就緒:

$ node index.js
Application running on port 3000.

為 Node.js 添加集群支持

現在我們已經準備好示例應用程序,我們可以開始實現 cluster .好消息是 cluster 包包含在 Node.js 核心中,所以我們不需要安裝任何其他東西。

為了保持整潔,我們將為與集群相關的代碼創建一個單獨的文件,並使用回調模式將其與我們的其餘代碼聯繫起來。

/cluster.js

import cluster from "cluster";
import os from "os";

export default (callback = null) => {
  const cpus = os.cpus().length;

  if (cluster.isMaster) {
    for (let i = 0; i < cpus; i++) {
      const worker = cluster.fork();

      worker.on("message", (message) => {
        console.log(`[${worker.process.pid} to MASTER]`, message);
      });
    }

    cluster.on("exit", (worker) => {
      console.warn(`[${worker.process.pid}]`, {
        message: "Process terminated. Restarting.",
      });

      cluster.fork();
    });
  } else {
    if (callback) callback();
  }
};

從頂部開始,我們導入兩個依賴項(這兩個依賴項都包含在 Node.js 中, 需要單獨安裝):clusteros .前者讓我們可以訪問管理工作集群所需的代碼,後者幫助我們檢測運行代碼的計算機上可用的 CPU 內核數量。

在我們的導入下面,接下來,我們 export 我們將從主 index.js 調用的函數 稍後歸檔。該函數負責設置我們的集群支持。作為參數,記下我們對 callback 的期望 函數被傳遞。這個以後會派上用場的。

在我們的函數內部,我們使用前面提到的 os 包與運行我們的代碼的計算機進行通信。在這裡,我們調用 os.cpus().length 期待 os.cpus() 返回一個數組,然後測量該數組的長度(代表計算機上的 CPU 內核數)。

有了這個數字,我們就可以設置我們的集群了。所有現代計算機都至少有 2-4 個內核,但請記住,在您的計算機上創建的工作人員數量將與下面顯示的有所不同。閱讀:如果您的號碼不同,請不要驚慌。

/cluster.js

[...]

  if (cluster.isMaster) {
    for (let i = 0; i < cpus; i++) {
      const worker = cluster.fork();

      worker.on("message", (message) => {
        console.log(`[${worker.process.pid} to MASTER]`, message);
      });
    }

    cluster.on("exit", (worker) => {
      console.warn(`[${worker.process.pid}]`, {
        message: "Process terminated. Restarting.",
      });

      cluster.fork();
    });
  }

[...]

我們需要做的第一件事是檢查正在運行的進程是否是我們應用程序的主實例,或者,不是 我們接下來要創建的工人之一。如果它 主實例,我們為 cpus 的長度做一個 for 循環 我們在上一步中確定的數組。在這裡,我們說“只要 i 的值 (我們當前的循環迭代)少於我們可用的 CPU 數量,運行以下代碼。”

以下代碼是我們創建工人的方式。對於我們的 for 的每次迭代 循環,我們用 cluster.fork() 創建一個工作實例 .這會 fork 正在運行的主進程,返回一個新的子進程或工作實例。

接下來,為了幫助我們在我們創建的工人和我們的主實例之間傳遞消息,我們為 message 添加了一個事件監聽器 事件給我們創建的worker,給它一個回調函數。

該回調函數表示“如果其中一個工作人員發送消息,則將其轉發給主設備。”所以,在這裡,當worker發送消息時,這個回調函數會在主進程中處理該消息(在這種情況下,我們將消息與pid一起註銷 發送它的工人)。

這可能會令人困惑。請記住,worker 是我們應用程序的運行實例。因此,例如,如果某個事件發生在 worker 內部(我們運行一些後台任務但它失敗了),我們需要一種方法來了解它。

在下一節中,我們將了解如何從將在此回調函數中彈出的工作人員中發送消息。

不過,在我們繼續之前,還有一個細節。我們在這裡添加了一個額外的事件處理程序,但這一次,我們說“如果集群(意味著任何正在運行的工作進程)接收到退出事件,則使用此回調處理它。”這裡的“處理”部分與我們之前所做的類似,但略有不同:首先,我們註銷一條消息以及工人的 pid 讓我們知道工人死了。接下來,為了確保我們的集群恢復(這意味著我們根據我們的 CPU 保持可用的最大運行進程數),我們使用 cluster.fork() 重新啟動進程 .

明確一點:我們只會調用 cluster.fork() 如果一個進程死了,就像這樣。

/cluster.js

import cluster from "cluster";
import os from "os";

export default (callback = null) => {
  const cpus = os.cpus().length;

  if (cluster.isMaster) {
    for (let i = 0; i < cpus; i++) {
      const worker = cluster.fork();

      // Listen for messages FROM the worker process.
      worker.on("message", (message) => {
        console.log(`[${worker.process.pid} to MASTER]`, message);
      });
    }

    cluster.on("exit", (worker) => {
      console.warn(`[${worker.process.pid}]`, {
        message: "Process terminated. Restarting.",
      });

      cluster.fork();
    });
  } else {
    if (callback) callback();
  }
};

還有一個細節。完成我們的集群代碼,在我們導出的函數的底部添加一個 else 聲明說“如果此代碼不是 在主進程中運行,如果有,則調用傳遞的回調。”

我們需要這樣做,因為我們只希望我們的工作進程發生在主進程中,而不是任何工作進程(否則我們會有一個進程創建的無限循環,我們的計算機不會對此感到興奮)。

將 Node.js 集群用於我們的應用程序

好的,現在是簡單的部分。將我們的集群代碼全部設置在另一個文件中,讓我們跳回到我們的 index.js 文件並設置好一切:

/index.js

import express from "express";
import favicon from "serve-favicon";
import cluster from "./cluster.js";

cluster(() => {
  const app = express();

  app.use(favicon("public/favicon.ico"));

  app.use("/", (req, res) => {
    if (process.send) {
      process.send({ pid: process.pid, message: "Hello!" });
    }

    res.send(
      `"Sometimes a slow gradual approach does more good than a large gesture." - Craig Newmark`
    );
  });

  app.listen(3000, () => {
    console.log(`[${process.pid}] Application running on port 3000.`);
  });
});

我們這裡已經添加了不少,讓我們一步一步來吧。

首先,我們已經導入了 cluster.js 歸檔為 cluster .接下來,我們調用該函數,向它傳遞一個回調函數(這將是 callback 的值 cluster.js 導出的函數中的參數 )。

在該函數內部,我們已經將我們編寫的所有代碼放在 index.js 中 較早,稍作修改。

在我們創建 app 之後立即 express() 的實例 ,您會注意到我們正在調用 app.use() , 將另一個調用傳遞給 favicon("public/favicon.ico") . favicon() 是來自 serve-favicon 的函數 在文件頂部的導入中添加了依賴項。

這是為了減少混亂。默認情況下,當我們在瀏覽器中訪問我們的應用程序時,瀏覽器會發出兩個請求:一個請求頁面,一個請求應用程序的 favicon.ico 文件。向前跳,當我們調用 process.send() 在我們路由的回調中,我們要確保我們沒有收到對 favicon.ico 的請求 添加文件 到我們的路線。

當我們從工作人員輸出消息時,這會變得令人困惑。因為我們的路由收到了兩個請求,所以我們最終會收到兩條消息(看起來像是壞了)。

為了解決這個問題,我們導入 favicon 來自 serve-favicon 然後添加對 app.use(favicon("public/favicon.ico")); 的調用 .添加後,您還應該添加 public 文件夾到項目的根目錄並放置一個空的 favicon.ico 該文件夾內的文件 .

現在,當請求進入應用程序時,我們只會收到一條消息,即 favicon.ico 請求將通過 favicon() 處理 中間件。

繼續往下看,您會注意到我們在 res.send() 之上添加了一些內容 調用我們的根 / 路線:

if (process.send) {
  process.send({ pid: process.pid, message: "Hello!" });
}

這個很重要。當我們在 Node.js 中使用集群配置時,我們需要了解 IPC 或進程間通信。這是一個術語,用於描述我們應用的主實例與工作人員之間的通信——或者更確切地說,是通信能力。

這裡,process.send() 是一種從工作實例發送消息的方式返回 到主實例。為什麼這很重要?好吧,因為工作進程是 forks 對於主進程,我們希望將它們視為主進程的子進程。如果工作人員內部發生與集群的健康或狀態相關的事情,那麼有一種方法可以通知主進程。

這可能會讓人感到困惑,因為沒有明確的說明該代碼與工人有關。

您必須記住的是,worker 只是用於描述我們應用程序的附加實例的名稱,或者在這裡,更簡單地說,就是我們的 Express 服務器。

當我們說 process 在這裡,我們指的是當前運行此代碼的 Node.js 進程。那可以 是我們的主實例,或者它可以 成為一個工人實例。

將兩者分開的是 if (process.send) {} 陳述。我們這樣做是因為我們的主實例不會 有一個 .send() 方法可用,只有我們的工作實例。當我們調用這個方法時,我們傳遞給 process.send() 的值 (這裡我們傳遞一個帶有 pid 的對象 和 message , 但你可以傳遞任何你想要的) 在 worker.on("message") 中彈出 我們在 cluster.js 中設置的事件處理程序 :

/cluster.js

worker.on("message", (message) => {
  console.log(`[${worker.process.pid} to MASTER]`, message);
});

現在這應該更有意義了(特別是 to MASTER 部分)。您不必將其保留在自己的代碼中,但它有助於解釋進程是如何通信的。

運行我們的集群服務器

最後一步。為了測試一下,讓我們運行我們的服務器。如果一切設置正確,請從終端的項目文件夾中運行 node index.js (同樣,請注意您正在運行的 Node.js 版本):

$ node index.js
[25423] Application running on port 3000.
[25422] Application running on port 3000.
[25425] Application running on port 3000.
[25426] Application running on port 3000.
[25424] Application running on port 3000.
[25427] Application running on port 3000.

如果一切正常,您應該會看到類似的內容。左側的數字代表生成的每個實例的進程 ID,相對於 CPU 中的內核數。在這裡,我的電腦有一個六核處理器,所以我得到了六個進程。如果你有一個八核處理器,你會期望看到八個進程。

最後,現在我們的服務器正在運行,如果我們打開 http://localhost:3000 在我們的瀏覽器中,然後在我們的終端中查看,我們應該會看到如下內容:

[25423] Application running on port 3000.
[25422] Application running on port 3000.
[25425] Application running on port 3000.
[25426] Application running on port 3000.
[25424] Application running on port 3000.
[25427] Application running on port 3000.
[25423 to MASTER] { pid: 25423, message: 'Hello!' }

最後一條日誌語句是在我們的 worker.on("message") 中收到的消息 事件處理程序,通過我們對 process.send() 的調用發送 在我們的根 / 的回調中 路由處理程序(當我們在 http://localhost:3000 訪問我們的應用程序時運行 )。

就是這樣!

總結

上面,我們學習瞭如何設置一個簡單的 Express 服務器並將其從單個運行的 Node.js 進程轉換為集群的多進程設置。有了這個,現在我們可以利用服務器的全部處理能力,使用更少的硬件來擴展我們的應用程序。


Tutorial JavaScript 教程
  1. DOM 操作:動手

  2. 約旦使用航空堆棧

  3. 我在 Reactathon 2022 學到了什麼

  4. 自動表格,幻影菜單 |模塊星期一 74

  5. Wordpress 作為 JAMStack 網站的 CMS

  6. 如何在 JavaScript 中輸出帶有前導零的數字? (中本聰格式)

  7. 為不同的國家使用不同的地理編碼服務

  1. React UI 動畫變得簡單

  2. NodeJS + Express 第 1 部分:簡介

  3. 將 JavaScript 生成的 SVG 轉換為文件

  4. 算法教程:島嶼的最大面積 (DFS)

  5. JavaScript 面試問題 #48:狗大小的貓

  6. 究竟什麼是“這個”? — 看看 JavaScript 的 this 關鍵字

  7. 像 BOSS 一樣添加 DOM 元素

  1. 對 CSS 中的 rem 和 em 單位感到困惑?

  2. 使用 JavaScript 的 Rails 應用程序

  3. 大 O 表示法簡介

  4. 快速提示:使用純 Javascript 設置 Typescript 類型檢查