為什麼Node.js叢集是優化應用程式的關鍵?

為什麼Node.js叢集是優化應用程式的關鍵?

Node.js 是一種伺服器端 JavaScript 執行時,使用事件驅動、無阻塞輸入輸出(I/O)模型。它在構建快速、可擴充套件的網路應用程式方面廣受認可。它還擁有一個龐大的社羣和豐富的模組庫,可簡化各種任務和流程。

叢集可使 Node.js 應用程式在多個程序上執行,從而提高其效能。這種技術可讓應用程式充分發揮多核系統的潛力。

本文將全面介紹 Node.js 中的叢集及其對應用程式效能的影響。

什麼是叢集?

預設情況下,Node.js 應用程式在單執行緒上執行。這種單執行緒特性意味著 Node.js 無法使用多核系統中的所有核心,而目前大多數系統都是如此。

通過利用非阻塞 I/O 操作和非同步程式設計技術,Node.js 仍然可以同時處理多個請求。

但是,繁重的計算任務會阻塞事件迴圈,導致應用程式無響應。因此,Node.js 提供了一個本地叢集模組(無論其單執行緒性質如何),以充分利用多核系統的總處理能力。

執行多個程序可充分利用多箇中央處理器(CPU)核心的處理能力,從而實現並行處理、縮短響應時間並提高吞吐量。這反過來又提高了 Node.js 應用程式的效能和可擴充套件性。

叢集是如何工作的?

Node.js 群集模組允許 Node.js 應用程式建立一個由併發執行的子程序組成的群集,每個子程序處理應用程式的部分工作負載。

在初始化叢集模組時,應用程式會建立主程序,然後將子程序分叉為工作程序。主程序充當負載平衡器,將工作負載分配給工作程序,同時每個工作程序監聽傳入的請求。

Node.js 群集模組有兩種分配傳入連線的方法。

  • 輪循方法 – 主程序在埠上監聽,接受新連線並平均分配工作量,以確保沒有程序超載。這是 Windows 以外所有作業系統的預設方法。
  • 第二種方法 – 主程序建立監聽套接字,並將其傳送給 “感興趣” 的工作程序,後者直接接受傳入的連線。

從理論上講,第二種方法更為複雜,應該能提供更好的效能。但實際上,連線的分佈非常不平衡。Node.js 文件中提到,所有連線的 70% 都在八個程序中的兩個程序中結束。

如何對 Node.js 應用程式進行叢集

現在,讓我們來看看叢集在 Node.js 應用程式中的效果。本教程使用一個 Express 應用程式,該程式有意執行一項繁重的計算任務,以阻塞事件迴圈。

首先,在不使用叢集的情況下執行此應用程式。然後,使用基準測試工具記錄效能。然後,在應用程式中實施聚類,並重復進行基準測試。最後,比較結果,看看聚類如何提高應用程式的效能。

開始

要理解本教程,您必須熟悉 Node.js 和 Express。設定您的 Express 伺服器

  1. 首先建立專案。
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    mkdir cluster-tutorial
    mkdir cluster-tutorial
    mkdir cluster-tutorial
  2. 導航至應用程式目錄,執行以下命令建立兩個檔案:no-cluster.jscluster.js
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    cd cluster-tutorial && touch no-cluster.js && touch cluster.js
    cd cluster-tutorial && touch no-cluster.js && touch cluster.js
    cd cluster-tutorial && touch no-cluster.js && touch cluster.js
  3. 在專案中初始化 NPM:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    npm init -y
    npm init -y
    npm init -y
  4. 最後,執行下面的命令安裝 Express:
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    npm install express
    npm install express
    npm install express

建立非叢集應用程式

no-cluster.js 檔案中,新增以下程式碼塊:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const express = require("express");
const PORT = 3000;
const app = express();
app.get("/", (req, res) => {
res.send("Response from server");
});
app.get("/slow", (req, res) => {
//Start timer
console.time("slow");
// Generate a large array of random numbers
let arr = [];
for (let i = 0; i < 100000; i++) {
arr.push(Math.random());
}
// Perform a heavy computation on the array
let sum = 0;
for (let i = 0; i {
console.log(`Server listening on port ${PORT}`);
});
const express = require("express"); const PORT = 3000; const app = express(); app.get("/", (req, res) => { res.send("Response from server"); }); app.get("/slow", (req, res) => { //Start timer console.time("slow"); // Generate a large array of random numbers let arr = []; for (let i = 0; i < 100000; i++) { arr.push(Math.random()); } // Perform a heavy computation on the array let sum = 0; for (let i = 0; i { console.log(`Server listening on port ${PORT}`); });
const express = require("express");
const PORT = 3000;
const app = express();
app.get("/", (req, res) => {
res.send("Response from server");
});
app.get("/slow", (req, res) => {
//Start timer 
console.time("slow");
// Generate a large array of random numbers
let arr = [];
for (let i = 0; i < 100000; i++) {
arr.push(Math.random());
}
// Perform a heavy computation on the array
let sum = 0;
for (let i = 0; i  {
console.log(`Server listening on port ${PORT}`);
});

上面的程式碼塊建立了一個在 3000 埠執行的 express 伺服器。伺服器有兩個路由,一個是根( / )路由,另一個是 /slow 路由。根路由會向客戶端傳送一條響應資訊: “來自伺服器的響應”。

不過, /slow 路由會故意進行一些繁重的計算,以阻斷事件迴圈。該路由啟動一個計時器,然後使用 for 迴圈將 100,000 個隨機數填入一個陣列。

然後,使用另一個 for 迴圈,對生成陣列中的每個數字進行平移並相加。完成後定時器結束,伺服器響應結果。

執行下面的命令啟動伺服器:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
node no-cluster.js
node no-cluster.js
node no-cluster.js

然後向 localhost:3000/slow 發起 GET 請求。

在此期間,如果您嘗試向伺服器發出任何其他請求(例如向根路由 ( / ) 發出請求),響應速度都會很慢,因為 /slow 路由阻塞了事件迴圈。

建立叢集應用程式

使用叢集模組生成子程序,以確保您的應用程式在執行繁重的計算任務時不會出現無響應和阻塞後續請求的情況。

每個子程序執行其事件迴圈,並與父程序共享伺服器埠,從而更好地利用可用資源。

首先,在 cluster.js 檔案中匯入 Node.js 叢集和 os 模組。叢集模組允許建立子程序,以便在多個 CPU 核心之間分配工作量。

os 模組提供有關計算機作業系統的資訊。您需要使用該模組來獲取系統上可用的核心數量,確保建立的子程序數量不會超過系統核心數量。

注:建立的子程序數量超過系統核心數量會導致系統在程序間切換花費更多時間。這會增加開銷並降低效能。

新增下面的程式碼塊,匯入這些模組並獲取系統核心的數量:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const cluster = require("node:cluster");
const numCores = require("node:os").cpus().length;
const cluster = require("node:cluster"); const numCores = require("node:os").cpus().length;
const cluster = require("node:cluster");
const numCores = require("node:os").cpus().length;

接下來,將下面的程式碼塊新增到 cluster.js 檔案中:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
console.log(`This machine has ${numCores} cores`);
// Fork workers.
for (let i = 0; i {
console.log(`worker ${worker.process.pid} died`);
// Replace the dead worker
console.log("Starting a new worker");
cluster.fork();
});
}
if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); console.log(`This machine has ${numCores} cores`); // Fork workers. for (let i = 0; i { console.log(`worker ${worker.process.pid} died`); // Replace the dead worker console.log("Starting a new worker"); cluster.fork(); }); }
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
console.log(`This machine has ${numCores} cores`);
// Fork workers.
for (let i = 0; i  {
console.log(`worker ${worker.process.pid} died`);
// Replace the dead worker
console.log("Starting a new worker");
cluster.fork();
});
}

上面的程式碼塊會檢查當前程序是主程序還是工作程序。如果為真,程式碼塊將根據系統核心數生成子程序。接下來,它會監聽程序的退出事件,並通過生成新程序來替換它們。

最後,用 else 程式碼塊封裝所有相關的表達邏輯。完成後的 cluster.js 檔案應與下面的程式碼塊相似。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
//cluster.js
const express = require("express");
const PORT = 3000;
const cluster = require("node:cluster");
const numCores = require("node:os").cpus().length;
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
console.log(`This machine has ${numCores} cores`);
// Fork workers.
for (let i = 0; i {
console.log(`worker ${worker.process.pid} died`);
// Replace the dead worker
console.log("Starting a new worker");
cluster.fork();
});
} else {
const app = express();
app.get("/", (req, res) => {
res.send("Response from server");
});
app.get("/slow", (req, res) => {
console.time("slow");
// Generate a large array of random numbers
let arr = [];
for (let i = 0; i < 100000; i++) {
arr.push(Math.random());
}
// Perform a heavy computation on the array
let sum = 0;
for (let i = 0; i {
console.log(`Server listening on port ${PORT}`);
});
}
//cluster.js const express = require("express"); const PORT = 3000; const cluster = require("node:cluster"); const numCores = require("node:os").cpus().length; if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); console.log(`This machine has ${numCores} cores`); // Fork workers. for (let i = 0; i { console.log(`worker ${worker.process.pid} died`); // Replace the dead worker console.log("Starting a new worker"); cluster.fork(); }); } else { const app = express(); app.get("/", (req, res) => { res.send("Response from server"); }); app.get("/slow", (req, res) => { console.time("slow"); // Generate a large array of random numbers let arr = []; for (let i = 0; i < 100000; i++) { arr.push(Math.random()); } // Perform a heavy computation on the array let sum = 0; for (let i = 0; i { console.log(`Server listening on port ${PORT}`); }); }
//cluster.js
const express = require("express");
const PORT = 3000;
const cluster = require("node:cluster");
const numCores = require("node:os").cpus().length;
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
console.log(`This machine has ${numCores} cores`);
// Fork workers.
for (let i = 0; i  {
console.log(`worker ${worker.process.pid} died`);
// Replace the dead worker
console.log("Starting a new worker");
cluster.fork();
});
} else {
const app = express();
app.get("/", (req, res) => {
res.send("Response from server");
});
app.get("/slow", (req, res) => {
console.time("slow");
// Generate a large array of random numbers
let arr = [];
for (let i = 0; i < 100000; i++) {
arr.push(Math.random());
}
// Perform a heavy computation on the array
let sum = 0;
for (let i = 0; i  {
console.log(`Server listening on port ${PORT}`);
});
}

實施叢集后,多個程序將處理請求。這意味著,即使在計算任務繁重的情況下,您的應用程式也能保持響應速度。

如何使用 loadtest 對效能進行基準測試

要在 Node.js 應用程式中準確演示和顯示叢集的效果,請使用 npm 軟體包 loadtest 比較叢集前後應用程式的效能。

執行以下命令全域性安裝 loadtest

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm install -g loadtest
npm install -g loadtest
npm install -g loadtest

loadtest 軟體包會在指定的 HTTP/WebSockets URL 上執行負載測試。

接下來,在終端例項中啟動 no-cluster.js 檔案。然後,開啟另一個終端例項,執行下面的負載測試:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
loadtest http://localhost:3000/slow -n 100 -c 10
loadtest http://localhost:3000/slow -n 100 -c 10
loadtest http://localhost:3000/slow -n 100 -c 10

上面的命令向未叢集的應用程式傳送了 100 個併發量為 10 的請求。執行該命令會產生以下結果:

非群集應用程式負載測試結果

非群集應用程式負載測試結果。

根據結果,在沒有群集的情況下,完成所有請求大約需要 100 秒,最長的請求需要 12 秒才能完成。

測試結果因系統而異。

接下來,停止執行 no-cluster.js 檔案,在終端例項上啟動 cluster.js 檔案。然後,開啟另一個終端例項並執行此負載測試:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
loadtest http://localhost:3000/slow -n 100 -c 10
loadtest http://localhost:3000/slow -n 100 -c 10
loadtest http://localhost:3000/slow -n 100 -c 10

上述命令將向叢集應用程式傳送 100 個併發量為 10 的請求。

執行該命令會產生以下結果:

叢集應用程式負載測試結果

叢集應用程式負載測試結果。

使用叢集后,請求只需 0.13 秒(136 毫秒)即可完成,比未叢集的應用程式需要 100 秒的時間大大縮短。此外,叢集應用程式上最長的請求完成時間為 41 毫秒。

這些結果表明,實施叢集可以顯著提高應用程式的效能。請注意,您應該使用 PM2 等流程管理軟體來管理生產環境中的叢集。

小結

Node.js 中的叢集可以建立多個工作程序來分配工作負載,從而提高 Node.js 應用程式的效能和可擴充套件性。正確實施叢集對於充分發揮這項技術的潛力至關重要。

在 Node.js 中實施叢集時,設計架構、管理資源分配和儘量減少網路延遲都是至關重要的因素。這種實現方式的重要性和複雜性正是 PM2 等程序管理器應在生產環境中使用的原因。

您如何看待 Node.js 叢集?您以前使用過嗎?請在評論區分享!

評論留言