在上一篇文章“Node JS 内部结构”中,我们讨论了 Node JS 内部架构,并讨论了为什么我们应该增加节点线程池大小以同时处理多个请求。我已经告诉过你,可扩展性和性能与线程池大小无关。
为了可扩展性和高性能,我们可以使用集群和工作线程。
假设您正在参加一场盛大的婚礼,有数千名宾客参加婚礼。有一间厨房,一名厨师正在为所有这些客人准备食物。听起来不可预测,对吧?如果您只有一名厨师,您就没有充分利用厨房的全部资源。
这正是在多核 CPU 上运行的 Node JS 应用程序中发生的情况,当仅使用一个核心来处理所有请求时。因此,即使我们的机器具有多核的能力,如果没有集群,我们的应用程序也只能在单核上运行。一个核心负责处理所有工作。
当你的厨房里有多个厨师正在工作时,这就是集群。
集群是一种使单个 Node JS 应用程序能够有效利用多个 CPU 核心的技术。
要实现集群,您必须使用 Node JS 中的集群模块。
const cluster = require('cluster');
通过使用此集群模块,您可以创建 Node JS 应用程序的多个实例。这些实例称为工人。所有工作人员共享相同的服务器端口并同时处理传入请求。
集群架构中有两种类型的进程。
1.主流程:
Master进程就像厨房里的主厨管理工人。它初始化应用程序,设置集群环境,并将任务委托给工作进程。它不直接处理应用程序请求。
Master进程是做什么的?
使用 cluster.fork() 方法创建多个工作进程。如果工作人员意外崩溃或退出,它还会重新启动工作人员。
它确保传入请求分布在所有工作进程中。在 Linux 上,这是由操作系统处理的,在 Windows 上,Node JS 本身充当负载均衡器。
它可以通过IPC(进程间通信)在worker之间进行通信。
2.工作进程:
工作进程是主进程创建的 Node JS 应用程序的实例。每个进程在单独的 CPU 核心上独立运行并处理传入请求。
工作进程无法直接相互通信,它们通过主进程进行通信。
工作进程处理传入的请求并执行一些任务,例如数据库查询、计算或任何应用程序逻辑。
const cluster = require('cluster');
在这里,我们首先检查这是主进程。如果是,那么它将创建工作进程。
在我们的代码中,我使用 cluster.fork() 创建一个工作进程。
但是,这不是创建工作进程的理想方式。
假设您正在创建 4 个工作进程,并且您的系统有两个核心。
为了解决这个问题,而不是创建硬编码的工作进程,首先找到 CPU 核心,然后考虑数据创建工作进程。
const cluster = require('cluster'); const os = require('os'); if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); // Fork workers cluster.fork(); cluster.fork(); cluster.fork(); cluster.fork(); } else { console.log(`Worker ${process.pid} is running`); // Worker logic (e.g., server setup) goes here }
我使用双核系统,所以输出将如下所示。
const cluster = require('cluster'); const os = require('os'); if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); const numCPUs = os.cpus().length; // Fork workers for (let i = 0; i < numCPUs; i++) { cluster.fork(); } } else { console.log(`Worker ${process.pid} is running`); // Worker logic (e.g., server setup) goes here }
现在,您有一个问题,如果您有双核 CPU,那么为什么要创建 4 个工作进程?
这是因为逻辑核心数为 4,我的 CPU 支持超线程或同步多线程 (SMT)。
在餐厅里,服务员接受订单并将订单交给厨师团队,因为烹饪需要一些时间。如果有清洁桌子或任何其他与服务员相关的工作,那么服务员就会这样做。当订单准备好后,厨师将食物返还给服务员,服务员将这些食物提供给顾客。
这与工作线程相关的场景相同。如果出现任何计算量大的任务,例如大规模数据处理、复杂计算或繁重算法,则主线程会将此任务委托给工作线程。该任务由工作线程执行,而不是主线程。
*为什么,这有帮助? *
我们知道 Node JS 事件循环是单线程的,如果这种繁重的计算工作由主线程完成,那么事件循环将被阻塞。如果您使用这些工作线程,那么这些繁重的任务将交给工作线程,工作线程执行这些任务,而不是主线程,因此事件循环不会被阻塞。
工作线程可以通过消息传递系统与主线程通信,并且可以使用结构化克隆(深复制)在线程之间发送数据。
现在,我们正在尝试模仿工作线程的工作。
main.js(主线程)
Master 12345 is running Worker 12346 is running Worker 12347 is running Worker 12348 is running Worker 12349 is running
worker.js(工作线程)
const { Worker } = require('worker_threads'); function startWorker() { const worker = new Worker('./worker.js'); // Create a worker using worker.js // Listen for messages from the worker worker.on('message', (message) => { console.log('Message from worker:', message); }); // Handle errors in the worker worker.on('error', (error) => { console.error('Worker error:', error); }); // Handle worker exit worker.on('exit', (code) => { console.log(`Worker exited with code ${code}`); }); // Send a message to the worker worker.postMessage({ num: 100 }); } startWorker();
如果数据包含大型结构,它将被深度克隆并传递,这可能会产生一些性能开销。
代码的工作
Worker 类用于生成新线程。
您可以使用worker.postMessage向worker发送数据,并使用worker.on('message',callback)监听消息。
在工作线程中,parentPort 是与主线程通信的主要接口。
您可以监听来自主线程(parentPort.on('message'))的消息并使用parentPort.postMessage发回消息。
输出将是:
const cluster = require('cluster');
现在,您还有一个问题:为什么我们不创建数百个工作线程?
但是,原因是如果您创建的线程多于核心数量,线程将竞争 CPU 时间,导致上下文切换,这会导致成本高昂并降低整体性能。
什么时候应该在 Node.js 中使用集群、工作线程或两者?
1。何时使用工作线程?
涉及大量计算的任务,例如图像/视频处理、数据压缩或加密、机器学习推理、科学计算
您需要在线程之间有效地共享数据而不重复数据。
如果您的应用程序只需要在单个进程内扩展,但仍然需要 CPU 密集型任务的并行性。
2.什么时候使用聚类?
任务涉及处理大量客户端请求,例如 Web、服务器、聊天应用程序和 API。集群通过在所有 CPU 核心之间分配请求来帮助水平扩展。
您的应用程序不需要在进程之间共享大量数据。
您希望通过生成多个 Node.js 进程来利用所有可用的核心。
3.什么时候同时使用集群和工作线程?
应用程序处理 HTTP 请求,但卸载计算密集型任务。示例:网络服务器处理文件上传并执行图像大小调整或视频转码。
您需要进程级和线程级并行性才能实现高吞吐量。在电子商务网站中,集群可确保多个进程处理传入请求。工作线程处理后台任务,例如生成个性化推荐。
谢谢。
欢迎提出问题或提出任何建议。
如果您发现本文内容丰富,请点赞。
以上是集群和工作线程 - Node JS的详细内容。更多信息请关注PHP中文网其他相关文章!