使用 Node.js 和 bullmq 部署 API 的流程布局

Cab*_*bus 5 api-design node.js node-redis pm2 bullmq

让我在问题前言说,我理解这个主题可能不会有明确的、是或否的答案,并且给出的答案可能是观点驱动的。但是,我确实需要并感谢您在部署和操作以下 API 设计方面的建议和/或指导。

我正在做什么

对于我的 SaaS,我想通过 API 向我的客户提供其功能。SaaS 提供的任务是一项长期运行且计算成本高昂的任务。因此,不幸的是,运行一个简单的“同步”API(其中调用者等待此任务的结果作为对其请求的响应来传递)是不合适的。相反,我选择了一种方法,让调用者安排Jobs并定期查询 API 以查看给定的作业是否已完成。我对这个设计非常满意。

到目前为止我做了什么

为了实现,我构建/使用了以下技术:

  • Node.js服务器
  • Express用于 API 和路由的 npm 包
  • Redis用于作业调度的数据库
  • bullmq用于 Redis 连接和队列管理的 npm 包

使用bullmq,我创建一个队列Queue并将其添加Jobs到该队列中,以响应对给定端点的调用。

对我来说,这是api.ts(缩短):

import Express from "express"
import { Queue } from "bullmq"

const api = Express()
const queue = new Queue("com.mysaas.workerQueue")

api.post("/job", async (request, response) => {
    var data, jobId
    ...

    await queue.add("com.mysaas.defaultJob", data, {
        jobId: jobId,
        removeOnComplete: true,
        ...
    })

    response.send({
        status: "success",
        job: jobId
    })
}
...
Run Code Online (Sandbox Code Playgroud)

此外,我还创建了处理预定作业的工作人员,并由包协调,在幕后bullmq使用。Redis

对我来说,这是worker.ts(缩短):

import { Worker } from "bullmq"

const worker = new Worker("com.mysaas.workerQueue", async (job) => {
    await someWork()
    ...
}, {concurrency: 25})
Run Code Online (Sandbox Code Playgroud)

我目前如何在我的开发环境中运行它

tsc将 Typescript 编译为 Javascript。

我用作pm2进程管理器和守护进程。要运行 api,我使用以下命令:

pm2 start -i max build/api/api.js
Run Code Online (Sandbox Code Playgroud)

这将为每个可用的 cpu 核心(在我的开发机器上有 8 个)启动一个节点进程。

为了运行工作程序,我打开另一个终端并执行:

node build/worker/worker.js
Run Code Online (Sandbox Code Playgroud)

我可以通过打开更多终端并重复上面的命令来添加工作人员。这样做将导致多个工作人员分担Jobs工作量Queue并同时完成多个工作,从而更快。

所有这些在我的开发环境中都运行得很好。

我不确定什么

我不知道目前的方法是否适合生产环境。pm2我已经研究了和的文档,bullmq但我似乎找不到如何将这两者结合起来的明确描述。当然,我的目标是最大化性能和 API 吞吐量。

以下几点仍然是我脑海中未解决的问题:

  • 实际上会pm2每个可用核心上运行一个进程,即将某个核心分配给给定进程吗?或者它会产生与核心一样多的进程,并且较低级别的系统机制将负载平衡哪个核心的计算时间用于哪个进程。
  • 如果是前者,我从 API 进程实例启动的子进程是否会分配给父进程正在运行的核心?
  • 如上所述启动工作程序,我们可以提供一个concurrency选项,该选项将告诉 bullmq 生成尽可能多的进程来运行工作程序代码。并发性是否也仅限于分配给主初始工作程序进程的核心(以 开头node build/worker/worker.js)。

鉴于我对上述几点的不确定性,我总结了三种在生产环境中运行流程的方法:

  1. 使用 启动 API 进程和守护进程pm2 start -i max build/api/api.js。使用 启动工作进程和守护进程pm2 start -i max build/worker/worker.js。这将为每个可用核心留下 1 个 api 进程和 1 个工作进程。我认为这种方法可以优化 CPU 负载。concurrency但是,我不知道该为工作人员的参数分配什么值bullmq

  2. 将工作代码移动/导入到 API 进程。使用 启动 API 进程和守护进程pm2 start -i max build/api/api.js。这会给我留下每个核心 1 个进程,其中包括 api 和工作线程。在我看来,这里的缺点是worker和api是同一个进程,这意味着如果api部分由于某种原因导致进程崩溃,worker也会随之关闭。那是对的吗?

  3. 使用 启动 API 进程和守护进程pm2 start -i max build/api/api.js。使用高参数值(100+)启动并妖魔化单个工作进程concurrency。假设系统将并发工作进程负载平衡到可用内核,这也应该会优化 CPU 利用率。缺点是,如果单个工作进程崩溃,则在恢复之前不会完成任何工作。

我的直觉告诉我方法 1. 在我的场景中是最好的。然而,这是我第一次将此类 Node.js 应用程序部署到生产环境中。因此,我真诚地感谢对所述项目的任何帮助、建议、指导或反馈。

更新 03/2022

我们最终采用了一种更加自举的方法,而不是手动部署所有内容pm2,等等。

整体 API 概念保持不变(安排作业、轮询作业状态),但我们转而作为nestjs主要驱动程序和中央 API 框架。Nest 应用程序分为apiworker模块。两者都作为单独可扩展的进程(Heroku dynos)部署到 Heroku。我们最终使用Heroku Redis Add-On进行作业调度bullmq

老实说,在切换到 Heroku 后,我们针对上面提出的工作并发和 CPU 核心问题所做的唯一一件事就是利用 Heroku 提供的WEB_CONCURRENCY环境变量在每个 dyno 上生成适当数量的工作进程。

代码实际上就这么简单(使用throng入口点 .js 文件中的包):

const bootstrap = async () => {
    const app = await NestFactory.create(WorkerMainModule)
}

throng({ workers: process.env.WEB_CONCURRENCY, worker: bootstrap })
Run Code Online (Sandbox Code Playgroud)