压力测试节点网络服务器的瓶颈

问题描述

过去几天这个问题真的困扰着我,我对此没有任何合乎逻辑的解释。在我测试了 epoll 和 libuv 通常在 Node 中的工作原理并通过试验 express / http 服务器的可扩展性之后,它出现了。我的测试分为 2 个部分。

  1. 使用单个 Node 进程创建 http 服务器。
  2. 通过集群创建具有多个 Node 进程的 http 服务器。

我还有第二台压力测试服务器,它使用 JMeter 对每秒可以处理的并发连接数进行压力测试。

单节点进程代码

const express = require('express')
var bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));


setInterval(function () {

    console.log("Per sec: "+ currentSec);
    currentSec = 0

    console.log("------------------------")

},1000);


app.get('/',(req,res) => {
 currentSec++
 res.send("OK")
})
app.listen(3000,"XXX.XXX.XXX.XXX");

多个 Node 进程

const express = require('express')
var bodyParser = require('body-parser')

var cluster = require('cluster');
var numcpus = require('os').cpus().length;


if (cluster.isMaster) {


    setInterval(function () { //syncronizer,ram - file
    // i had some logic here for counting global per second
    },1000);


    for (var i = 0; i < numcpus; i++) {
        const worker = cluster.fork();
    }

} else {
    var app = express();

    app.get('/',async function (req,res) {
        res.send("ok")
        return
    });

    app.listen(3000,'XXX.34.XXX.121');
}

两个代码都在计算每秒传入的请求数,但我也可以选择在 JMeter 中查看它,因此这不是真正的问题。 在此之前,我想澄清一下,我故意制作了一个非常简单的 GET 路由,因为我想测试简单 Web 服务器上的负载,该服务器不使用任何 libuv 线程的帮助,这可能会更多地强调机器上的内核,而是使用只有主事件循环线程来处理套接字写入,这是测试的实际目的(了解瓶颈是在内核中还是在主事件循环处理应用程序数据中)并尽可能平衡每个进程的 1 个内核。>

我已经阅读了一些安静有趣的文档,其中介绍了 Node 如何处理网络 I/O 通信以及它使用 epoll 收集文件描述符的方式,在系统调用的帮助下将应用程序数据附加到套接字后使用该描述符。我已经理解了它的复杂性部分以及所有套接字写入和读取都是由内核本身而不是 Node 完成的事实,只剩下 Node 来处理应用程序数据层,它稍后会进行非阻塞系统调用完成简单的处理,之后内核使用它来触发对网络适配器的驱动程序调用。从客户端接收数据后,内核会被硬件中断触发,然后通知 epoll 文件描述符的变化,使整个写入/读取过程非常简单,至少是抽象的。

我得到的结果有点令人震惊,因为我在开始时猜测 Node 在两种情况下都会有相同的结果(使用多进程或单进程)。我的假设是,读取文件描述符、将数据附加到套接字以及在某些事件时从内核获取信息的复杂性,如果有多个内核参与处理,那么打开这样的新套接字将导致相同的结果,而不是只有 1 个,这使得集群在以下方面非常冗余像这样处理简单的 http 请求。我的理论是,与通过集群的多核架构相比,通过管道进行 ICP 通信的开销会进一步减慢它的速度,使一个线程、1 个进程节点更快或相同。在我看来,这有点类似于 for loop 的多样化,它对 8 个内核进行 100k 次数学计算,而不是首先使用 1 个核来执行此操作,假设这个 100k 周期操作是由某个 http 事件触发的,并且然后围绕主进程的其他核心进行多样化。在实践中,如果没有来自单个进程(在我们的案例中的主服务器)的通信开销,该进程将工作多样化到其他 8 个内核,从而基于某些事件触发器(如 GET 请求)使用 1 个内核进行处理会更快如果我们为此使用多核,我们的情况是由于通信开销造成的。在处理 TCP 套接字和执行系统调用时,主事件循环正在执行的操作对我来说在 cpu 复杂性方面显得微不足道,我认为在多核使用方面实现并行性是非常多余的,因为最终瓶颈是内核正在做所有最后的接触并将套接字发送到网络驱动程序,而 Node 的进程只是在做一些小的非阻塞部分。

我想象的过程如下(在这个例子中,让我们看看第一个场景,只使用 1 个节点进程)。

场景 1(单进程):

JMeter 触发一定数量的请求,比如说 N。服务器端的网络适配器接收它并向内核执行硬件中断,内核然后通知 epoll 有一些新的事件需要处理,然后主事件循环一些时间(比如 K)开始经历所有过来的事件并开始向套接字写入应用程序数据。

Wait for epoll explained

在收集文件描述符并写入应用程序数据(在这种情况下为“ok”内容)时,它会系统调用内核,套接字已准备好并已准备好发送,然后内核将其发送到网络驱动程序,然后数据包被发送到第一个发出请求的客户端。

在场景 nr 中会发生什么。 2 什么时候涉及到多处理和聚类?

即使每个工作进程都在同一个端口上“侦听”,但实际上并非如此。主进程和主进程的主事件循环是在这个端口上侦听的一个,它所做的一切都是在单独的 tcp 通信通道或管道中注册工人,这取决于稍后卸载套接字应用程序数据附加的实现,使瓶颈只是这个使用与第一个场景相同的 1 个事件循环的主主进程(这里造成了最大的混乱!)。 当客户端通过 HTTP 请求发送时,我认为对于每个请求所发生的情况与第一种情况非常相似。内核接收硬件中断,然后它从第一个主进程通知 epoll 新事件发生,然后主事件循环获取它必须读取的文件描述符,然后发生的事情是分叉所有其他集群的主主进程正在卸载该文件描述符/向其他进程通知他们必须做的一些工作(在这种情况下,它可能为他们提供他们必须操作的文件描述符)。通过这种方式,我们通过使用多核来实现并行性,并实际上尝试提高处理速度非常简单cpu 消耗而言,这是非阻塞系统调用和应用程序数据附加

结果

在提及任何内容之前,我在具有 16GB 内存和 1/1Gbps 带宽的 8 核 cpu 服务器上进行了测试。 JMeter 使用的是另一台完全相同规格的机器。 每个请求都没有 keep-alive 标头,这意味着套接字每次都重新打开。 客户端和服务器之间的 RTT 大约为 8 毫秒。

使用在 1 个节点进程上运行的服务器的第一个场景(带有 1 个主事件循环)。

服务器每秒接收和处理大约 2,500 - 3,500 个并发连接。

使用在 8 个节点进程上运行的服务器的第二个场景(有 8 个主事件循环)。

服务器每秒接收和处理大约 11,00 - 13,000 个并发连接。

混淆总结

基于我研究过的所有事情,并考虑到所有这些进程所做的都是简单的非阻塞系统请求,我的问题是,考虑到瓶颈是问题,8 个进程怎么可能设法取得更好的结果?两个测试一样吗?全局级别的瓶颈是内核 - 操作系统,在我们的应用程序(两种场景)中,它的节点服务器使用 1 个主事件循环和 epoll 轮询就绪套接字来侦听某个特定端口。在第一个场景中,我们有 1 个进程侦听端口 3000(1 个主事件循环),在第二个场景中,我们还有 1 个进程(主主进程),其主事件循环侦听端口 3000 并将每个请求卸载给工作人员以填充该文件描述符- >> 带有应用程序数据的套接字,然后以系统调用结束。对我来说,在第一个场景中 8 个进程的性能比这 1 个进程更好是没有意义的,因为他们在从 epoll 接收事件时必须执行的操作非常微不足道,最终只是非阻塞系统调用

我未经证实的解释为什么会发生?

假设网络适配器由于高带宽和低消息大小每秒可以接受超过 10,000 个连接(在我们的例子中)。现在它让我明白了这一点,唯一的解释是我读过的所有关于集群的文章以及主事件循环正在执行的 I/O 网络的复杂操作方式都是非常不真实的。假设 1 个节点进程实现了 3,000 个并发请求,而 8 个进程实现了 12,000 个,这说明它确实通过在 epoll 收到文件描述符后提供更多的核心能力来处理 something 来处理更多的连接。到目前为止,我读过的每篇文章都解释说,这个进程根本不占用 cpu,因为它只在接收描述符并填充应用程序数据后执行简单的非阻塞系统调用。我唯一的解释是,在系统调用之前,填充应用程序数据在某种程度上确实占用了大量 cpu,我需要有人真正为我指明正确的方向。此外,当他们提到主进程正在向其他工作进程发送文件描述符/套接字位置时,文章可能是错误的,但我认为可能有可能在 Node 启动时它只是注册所有 8 个不同的 epoll 到内核,所以当新请求进来时,它会执行一些循环原则,它会通知 8 个不同的 epoll 侦听器有关新请求的信息,而不是直接通过主进程(只是我的猜测)。通过这种方式,我们实现了多核并行处理昂贵的操作,这可能是数据应用程序附加到套接字。如果这是真的,并且实际上有 8 个不同的 epoll 侦听器,那么这意味着处理 I/O 网络套接字和写入应用程序数据确实很重,而瓶颈在于 Node 应用程序以及它在执行之前如何处理套接字和附加应用程序数据系统调用

有人对此有任何合乎逻辑的解释吗?

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)