用集群优化Node.js性能

382 阅读8分钟

在过去的几年里,Node.js已经获得了很大的知名度。像LinkedIn、eBay和Netflix这样的大公司都在使用它,这证明它已经经过了良好的战斗考验。在本教程中,我们将学习如何在Node.js中使用集群,通过使用所有可用的CPU获得巨大的性能优势。让我们开始吧。

Node.js中对集群的需求

Node.js的一个实例运行在一个单线程上。Node.js的官方 "关于 "页面指出。"Node.js的设计没有线程,但这并不意味着你不能在你的环境中利用多核的优势"。这就是它指向集群模块的地方。

集群模块的文档补充道。"为了利用多核系统的优势,用户有时会想启动一个Node.js进程的集群来处理负载。"因此,为了利用运行Node.js的系统上的多个处理器,我们应该使用集群模块。

利用可用的核心在它们之间分配负载,使我们的Node.js应用程序的性能得到提升。由于大多数现代系统都有多个内核,我们应该在Node.js中使用集群模块,以便从这些较新的机器中获得最大的性能汁。

Node.js集群模块是如何工作的?

简而言之,Node.js集群模块作为一个负载平衡器,将负载分配给同时在一个共享端口上运行的子进程。Node.js对阻塞代码并不擅长,这意味着如果只有一个处理器,而它被一个繁重的、CPU密集型的操作所阻塞,其他请求就只能在队列中等待这个操作的完成。

在多进程的情况下,如果一个进程忙于一个相对CPU密集型的操作,其他进程可以占用进来的其他请求,利用其他可用的CPU/cores。这就是集群模块的威力,工作者分担负载,应用程序不会因为高负载而停顿。

主进程可以通过两种方式将负载分配给子进程。第一种(也是默认的)是轮流的方式。第二种方式是主进程监听一个套接字,并将工作发送给感兴趣的工作者。然后,这些工作者处理传入的请求。

然而,第二种方法不像基本的轮流方式那样超级清晰和容易理解。

理论上的东西已经够多了,接下来让我们在深入研究代码之前看看一些先决条件。

前提条件

要遵循这个关于Node.js集群的指南,你应该具备以下条件。

  • 在你的机器上运行Node.js
  • 对Node.js和Express的工作知识
  • 关于进程和线程如何工作的基本知识
  • 有关Git和GitHub的工作知识

现在让我们进入本教程的代码。

构建一个没有集群的简单Express服务器

我们将从创建一个简单的Express服务器开始。这个服务器将做一个相对较重的计算任务,它将故意阻塞事件循环。我们的第一个例子是没有任何聚类的。

为了在一个新的项目中设置Express,我们可以在CLI上运行以下程序。

mkdir nodejs-cluster
cd nodejs-cluster
npm init -y
npm install --save express

然后,我们将在项目的根部创建一个名为no-cluster.js 的文件,如下所示。

Screenshot of a node.js file system

no-cluster.js 文件的内容将如下。

   js
const express = require('express');
const port = 3001;

const app = express();
console.log(`Worker ${process.pid} started`);

app.get('/', (req, res) => {
  res.send('Hello World!');
})

app.get('/api/slow', function (req, res) {
  console.time('slowApi');
  const baseNumber = 7;
  let result = 0;   
  for (let i = Math.pow(baseNumber, 7); i >= 0; i--) {       
    result += Math.atan(i) * Math.tan(i);
  };
  console.timeEnd('slowApi');

  console.log(`Result number is ${result} - on process ${process.pid}`);
  res.send(`Result number is ${result}`);
});

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

让我们看看这段代码在做什么。我们从一个简单的Express服务器开始,它将运行在端口3001 。它有两个URI (/),显示Hello World! 和另一个路径/api/slow

慢速API GET方法有一个长循环,循环77次,也就是823,543次。在每个循环中,它做一个math.atan() ,或一个数字的正切(弧度),以及一个math.tan() ,一个数字的正切。它将这些数字添加到结果变量中。之后,它记录并返回这个数字作为响应。

是的,它已经被故意弄得很耗时,而且处理器很密集,以后要用集群来看它的效果。我们可以用node no-cluser.js ,然后点击 [http://localhost:3001/api/slow](http://localhost:3001/api/slow)这将为我们提供以下输出。

Screenshot of function output that reads "result number is: -4951863.0970"

Node.js进程运行的CLI看起来像下面的屏幕截图。

Screenshot of CLI with same number as before

如上所示,根据我们添加了console.timeconsole.timeEnd 调用的剖析,API完成823,543次循环需要37.432毫秒。

到此为止的代码可以以拉动请求的形式访问,供你参考。接下来,我们将创建另一个看起来类似的服务器,但其中有集群模块。

在Express服务器上添加Node.js集群

我们将添加一个index.js 文件,该文件看起来与上面的no-cluster.js 文件类似,但在这个例子中它将使用集群模块。index.js 文件的代码看起来像下面这样。

const express = require('express');
const port = 3000;
const cluster = require('cluster');
const totalCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`Number of CPUs is ${totalCPUs}`);
  console.log(`Master ${process.pid} is running`);

  // Fork workers.
  for (let i = 0; i < totalCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`worker ${worker.process.pid} died`);
    console.log("Let's fork another worker!");
    cluster.fork();
  });

} else {
  startExpress();
}

function startExpress() {
  const app = express();
  console.log(`Worker ${process.pid} started`);

  app.get('/', (req, res) => {
    res.send('Hello World!');
  });

  app.get('/api/slow', function (req, res) {
    console.time('slowApi');
    const baseNumber = 7;
    let result = 0; 
    for (let i = Math.pow(baseNumber, 7); i >= 0; i--) {     
      result += Math.atan(i) * Math.tan(i);
    };
    console.timeEnd('slowApi');

    console.log(`Result number is ${result} - on process ${process.pid}`);
    res.send(`Result number is ${result}`);
  });

  app.listen(port, () => {
    console.log(`App listening on port ${port}`);
  });
}

让我们看看这段代码在做什么。我们首先需要express 模块,然后我们需要cluster 模块。之后,我们通过require('os').cpus().length ,得到可用的CPU数量。在我的例子中,在运行Node.js 14的Macbook pro上,它是8个。

因此,我们检查集群是否是主控。经过几个console.logs ,我们分叉工人的次数与可用CPU的数量相同。我们只是在一个工作者退出时抓紧时间记录并分叉另一个工作者。

如果它不是主进程,它就是子进程,在那里我们调用startExpress 。这个函数和前面例子中没有集群的Express服务器是一样的。

当我们用node index.js 来运行上述index.js 文件时,我们会看到以下输出。

Screenshot of Nodejs cluster module

正如我们所看到的,所有八个CPU都有八个相关的工作者在运行,准备接受任何进来的请求。如果我们点击 [http://localhost:3000/api/slow](http://localhost:3000/api/slow)我们将看到以下输出。

Screenshot of previously calculated number on port 3000 with "API slow"

带有集群模块的服务器的代码在这个pull request中。接下来,我们将对有集群和无集群的Express服务器进行负载测试,以评估其响应时间和每秒可处理的请求数的差异。

负载测试有集群和无集群的服务器

为了对有集群和无集群的Node.js服务器进行负载测试,我们将使用Vegeta负载测试工具。其他选择也可以是负载测试NPM包或Apache基准工具。我发现Vegeta更容易安装和使用,因为它是一个Go二进制文件,预编译的可执行文件可以无缝安装并开始使用。

在我们的机器上运行Vegeta后,我们可以运行以下命令,在不启用任何集群的情况下启动Node.js服务器。

node no-cluster.js

在另一个CLI标签中,我们可以运行以下命令,用Vegeta每秒钟发送50个请求,持续30秒。

echo "GET http://localhost:3001/api/slow" | vegeta attack -duration=30s -rate=50 | vegeta report --type=text

大约30秒后会出现如下输出。如果你在Node.js运行时检查另一个标签,你会看到大量的日志流动。

Screenshot of logs from no clustering test

从上面的负载测试中可以快速了解到一些情况。总共发送了1500个(50*30)请求,服务器的最大良好响应为每秒27.04个请求。最快的响应时间是96.998微秒,最慢的是21.745秒。同样,只有1104个请求返回了200 响应代码,这意味着在没有集群模块的情况下成功率为73.60%。

让我们停止该服务器,并运行另一台带有集群模块的服务器。

node index.js

如果我们运行同样的50RPS的测试,持续30秒,在这第二台服务器中,我们可以看到差异。我们可以通过运行负载测试。

echo "GET http://localhost:3000/api/slow" | vegeta attack -duration=30s -rate=50 | vegeta report --type=text

30秒后,输出将看起来像这样。

Screenshot of logs for clustering test

我们可以清楚地看到这里有很大的不同,因为服务器可以利用所有可用的CPU,而不是只有一个。所有1500个请求都是成功的,回来的是200 响应代码。最快的响应是31.608毫秒,最慢的只有42.883毫秒,而没有集群模块的情况下是27秒。

吞吐量也是50,所以这次服务器在30秒内处理每秒50个请求(RPS)没有问题。由于所有八个核心都可以处理,它可以轻松地处理比之前的27个RPS更高的负载。

如果你看一下带有集群的Node.js服务器的CLI标签,它应该显示这样的内容。

Screenshot of CLI from cluster test

这告诉我们,至少有两个处理器被用来处理请求。如果我们尝试一下,比如每秒100个请求,它将根据需要使用更多的CPU和进程。你当然可以用100RPS试一下,持续30秒,看看它的表现如何。在我的机器上,它最高达到了102 RPS左右。

从没有集群的27RPS到有集群的102RPS,集群模块的响应成功率提高了近四倍。这就是使用集群模块使用所有可用的CPU资源的优势。

接下来的步骤

如上所述,在我们自己身上使用集群对性能是有益的。对于一个生产级的系统,最好使用像PM2这样经过战斗考验的软件。 它内置了集群模式,并包括其他伟大的功能,如进程管理和日志。

同样,对于在Kubernetes上的容器中运行的生产级Node.js应用程序,资源管理部分可能由Kubernetes处理得更好。

这些都是你和你的软件工程团队需要做出的决定和权衡,以拥有一个在生产环境中运行的更具扩展性、性能和弹性的Node.js应用程序。

总结

在这篇文章中,我们学习了如何利用Node.js集群模块,充分利用可用的CPU核心,从我们的Node.js应用程序中获取更好的性能。在其他方面,集群可以成为Node.js武器库中另一个有用的工具,以获得更好的吞吐量。

The postOptimize Node.js performance with clusteringappeared first onLogRocket Blog.