Kodokon kodokon.com

上生产:进程管理器、Docker、cluster

发布到生产环境:NODE_ENV、进程管理器、一个正确的 Docker 镜像,以及用 cluster 实现的多核扩展。

10 分钟 · 3 题

在 Kodokon 中打开本课

NODE_ENV 仅仅是一个库的约定:Node 核心几乎完全无视它。但 Express 等库会用它来缓存视图并隐藏堆栈跟踪;某些框架在 NODE_ENV=production 下会快好几倍。在依赖这一侧,npm ci --omit=dev 会精确地按照 lockfile 安装,不含开发依赖 - 可复现,而且比 npm install 更快。自 Node 20.6 起,--env-file 会原生地加载一个 .env 文件,无需任何依赖。

BASH
NODE_ENV=production node server.js

npm ci --omit=dev

node --env-file=.env server.js
上生产的三条基本命令。

一个 Node 进程总有一天会死掉:内存超限、bug、异常。进程管理器的作用就是把它重启。有两种流派:在裸机上,pm2(或 systemd)负责监管、重启和无停机重载;在容器里,扮演这个角色的是编排器(Kubernetes、ECS) - 一个容器一个进程,pm2 就变得毫无用处,甚至有害,因为它会把崩溃对编排器藏起来。

BASH
npm install -g pm2
pm2 start server.js -i max --name api
pm2 reload api
pm2 logs api --lines 100
pm2:-i max 为每个核心 fork 一个 worker,reload 无停机。

有两个 Node 特有的 Docker 陷阱。其一:镜像应当以官方镜像提供的非特权用户 node 运行。其二:Node 并非被设计来当 PID 1 的 - 它不会回收僵尸进程;请用 docker run --init(或 Compose 里的 init: true)来启动容器,以插入一个最简的 init。最后,把 CMD 写成 exec 形式:shell 形式会插入 /bin/sh,而它不会转发 SIGTERM - 上一课的优雅关闭就永远不会被触发。

BASH
FROM node:20-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:20-slim
ENV NODE_ENV=production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
多阶段 Dockerfile:node 用户,exec 形式的 CMD。

单个 Node 进程只会用到一个核心。cluster 模块会为每个核心创建一个 worker 进程:主进程在端口上监听,并以轮询方式(round-robin,默认行为,Windows 上除外)把连接分发给各个 worker。每个 worker 都有自己的内存和自己的事件循环:没有共享状态 - 会话和缓存必须存放在 Redis 或等价物中。对于纯计算,请优先选择 worker_threads,它通过 SharedArrayBuffer 共享内存。

JAVASCRIPT
import cluster from "node:cluster";
import { createServer } from "node:http";
import { availableParallelism } from "node:os";

if (cluster.isPrimary) {
  const count = availableParallelism();
  for (let i = 0; i < count; i += 1) cluster.fork();
  cluster.on("exit", (worker) => {
    console.log(`worker ${worker.process.pid} down`);
    cluster.fork();
  });
} else {
  createServer((req, res) => {
    res.end(`pid ${process.pid}`);
  }).listen(3000);
}
每个核心一个 worker,死掉时自动重启。

知识检测

确认你已牢记本课的重点内容。

  1. NODE_ENV=production 实际改变了什么?
    • Node.js 启用一个更激进的 JIT 编译器
    • Express 这类库会启用它们的优化(视图缓存、非冗长的错误信息)
    • Node.js 拒绝在没有 HTTPS 的情况下启动
  2. 为什么在 Dockerfile 中要优先选择 exec 形式 CMD ["node", "server.js"]?
    • exec 形式启动更快
    • exec 形式允许变量插值
    • shell 形式会经过 /bin/sh,而它不会把 SIGTERM 转发给 Node 进程
  3. 使用 cluster 和 8 个 worker 时,内存和应用状态会怎样?
    • 每个 worker 都是一个独立进程:内存乘以 8,没有共享状态
    • 各个 worker 共享同一个 V8 堆
    • 主进程会周期性地合并各个 worker 的堆