Coolify logoCoolify

Node.js 多核扩展

问题所在

JavaScript 运行时在每个进程的单线程上执行事件循环。一个 node app.js(或 bun/deno)进程会占满一个 CPU 核心,无论主机容量如何——无论是处理 HTTP 请求、处理队列、运行定时任务,还是执行 CPU 密集型工作。

这适用于所有主要的 JS 运行时:

运行时引擎单线程事件循环
Node.jsV8
BunJavaScriptCore
DenoV8

这是运行时的特性,而非平台限制——无论是裸机、Docker、Kubernetes 还是任何 PaaS 平台,都受此约束。

Coolify 容器无 CPU 限制

默认情况下,Coolify 不会限制容器 CPU,因此所有主机核心均可用——你只需告知运行时使用它们即可。

解决方案

在容器内运行多个工作进程。每种运行时都有其首选的机制:

运行时方案代码改动说明
Node.jsPM2 集群模式最简单;包装你现有的启动命令
Node.jsnode:cluster 模块应用层内置,无需额外依赖
BunBun.serve({ reusePort: true })一行代码改动通过 SO_REUSEPORT 由内核负载均衡
DenoDeno.serve({ reusePort: true })一行代码改动与 Bun 相同的内核机制

本指南涵盖 PM2 集群模式(Node.js)和 reusePort(Bun、Deno),并提供 DockerfileNixpacks 构建包的示例。

技术背景

  • V8(及 JavaScriptCore)的事件循环是每个进程单线程的。
  • libuv 的线程池(Node 中默认为 4 个线程)会卸载部分 I/O 工作,但你的 JavaScript 代码仍运行在单个核心上。
  • 要使用多个核心,你需要在容器内运行多个进程,或者使用支持多进程监听器的运行时(Bun 和 Deno 通过 reusePort 实现)。

Dockerfile 示例(PM2 集群模式)

当你的应用使用 Dockerfile 构建包 构建时,请使用此配置。

FROM node:lts-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm install -g pm2

COPY . .

EXPOSE 3000
CMD ["pm2-runtime", "-i", "max", "dist/index.js"]

关键点:

  • pm2-runtime -i max 会为每个可用的 CPU 核心派生一个工作进程,并使 PM2 保持在前台运行(这是必需的,因为 Docker 的 PID 1 进程不能退出)。
  • dist/index.js 替换为你的实际入口文件。
  • 在你的平台网络设置中暴露应用监听的端口(此处为 3000)——在 Coolify 中,这是 Ports Exposes 字段。

Nixpacks 示例

当你的应用使用 Nixpacks 构建包 构建时,请使用此配置。

选项 A — 将 NIXPACKS_START_CMD 设置为环境变量

在你的应用中设置以下环境变量(在 Coolify 中:打开你的应用 → 环境变量):

NIXPACKS_START_CMD=pm2-runtime -i max dist/index.js

然后将 pm2 添加到 package.jsondependencies 部分,以便 Nixpacks 在构建期间安装它。

选项 B — 在仓库根目录放置 nixpacks.toml

[phases.setup]
nixPkgs = ["nodejs", "pm2"]

[start]
cmd = "pm2-runtime -i max dist/index.js"

这将使多核配置保留在版本控制中,并适用于本地和远程构建。

Bun 替代方案(一行代码实现多核)

Bun 的 HTTP 服务器可以使用 SO_REUSEPORT 在多个进程中绑定同一端口。运行 N 个 Bun 进程,内核会在它们之间对传入连接进行负载均衡。

应用代码

Bun.serve({
  port: 3000,
  reusePort: true,
  fetch(req) {
    return new Response("Hello from Bun");
  },
});

Dockerfile

FROM oven/bun:alpine

WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --production

COPY . .

EXPOSE 3000
# Spawn one Bun process per available core.
CMD ["sh", "-c", "for i in $(seq 1 $(nproc)); do bun run server.js & done; wait"]

Nixpacks (nixpacks.toml)

[phases.setup]
nixPkgs = ["bun"]

[start]
cmd = "sh -c 'for i in $(seq 1 $(nproc)); do bun run server.js & done; wait'"

说明:

  • reusePort: true 需要 Linux 内核 ≥ 3.9(所有现代发行版均支持)。
  • 每个 Bun 进程都是独立的——没有主/从进程间的 IPC,因此请使用 Redis 或数据库来管理共享状态。
  • 比 PM2 更简单,但没有内置的每个工作进程自动重启功能;建议配合 Docker 的 restart: unless-stopped(Coolify 的默认设置)使用,以便在父 Shell 崩溃时进行恢复。

Deno 替代方案(一行代码实现多核)

Deno 的 Deno.serve 接受相同的 reusePort 标志,因此你可以派生 N 个进程并让它们绑定同一端口,由内核分配连接。

应用代码

Deno.serve({ port: 3000, reusePort: true }, (_req) => {
  return new Response("Hello from Deno");
});

Dockerfile

FROM denoland/deno:alpine

WORKDIR /app
COPY . .
RUN deno cache server.ts

EXPOSE 3000
# Spawn one Deno process per available core.
CMD ["sh", "-c", "for i in $(seq 1 $(nproc)); do deno run --allow-net server.ts & done; wait"]

Nixpacks (nixpacks.toml)

[phases.setup]
nixPkgs = ["deno"]

[start]
cmd = "sh -c 'for i in $(seq 1 $(nproc)); do deno run --allow-net server.ts & done; wait'"

与 Bun 相同的注意事项:进程间无 IPC,无内置的每个工作进程自动重启功能。

注意事项

进程内状态无法扩展

工作进程不共享进程内内存。会话、内存缓存、速率限制计数器和 WebSocket 连接映射必须迁移到 Redis 或其他外部存储。

  • **WebSocket:**连接会分散到各个工作进程中。请在代理层使用粘性会话,或将发布/订阅迁移到 Redis,以便任何工作进程都能投递消息。
  • **优雅关闭:**在每个工作进程中处理 SIGTERM 信号。PM2 会自动处理此问题。
  • **内存:**大致呈线性扩展——N 个工作进程 ≈ N × 单进程 RSS。请据此调整服务器规格。
  • **日志输出:**PM2 会合并跨工作进程的日志;而使用 Bun 的 reusePort 方案时,每个进程独立写入,日志可能会交错。建议使用结构化的 JSON 日志。

验证多核使用情况

部署后,SSH 登录到运行容器的宿主机并检查进程:

docker exec -it <container> top

你应该能看到多个 nodebundeno 进程。在负载下访问应用(例如使用 autocannon -c 200 http://<host>:3000/),并确认 htop/top 显示负载分散在所有主机核心上,而不是集中在某一个核心上。

On this page