本番環境へ出荷しましょう。NODE_ENV、プロセスマネージャー、正しいDockerイメージ、そしてclusterによるマルチコアのスケーリングです。
このレッスンを Kodokon で開くNODE_ENVは単なるライブラリの慣習にすぎません。Nodeのコアはそれをほとんど完全に無視します。しかしExpressをはじめとするライブラリは、それを使ってビューをキャッシュし、スタックトレースを隠します。NODE_ENV=productionで数倍速くなるフレームワークもあります。依存関係の側では、npm ci --omit=devがlockfileのとおり正確に、開発用の依存関係なしでインストールします。再現性があり、npm installより高速です。Node 20.6以降、--env-fileは依存関係なしにネイティブで.envファイルを読み込みます。
NODE_ENV=production node server.js
npm ci --omit=dev
node --env-file=.env server.jsNodeのプロセスは結局いつか死にます。メモリの超過、バグ、例外です。プロセスマネージャーの役割は、それを再起動することです。二つの流派があります。むき出しのマシンでは、pm2(またはsystemd)が監視し、再起動し、ダウンタイムなしでリロードします。コンテナでは、この役割を担うのはオーケストレーター(Kubernetes、ECS)です。コンテナごとに1プロセスとなり、pm2は不要になり、それどころか有害になります。オーケストレーターからクラッシュを隠してしまうからです。
npm install -g pm2
pm2 start server.js -i max --name api
pm2 reload api
pm2 logs api --lines 100Nodeに特有のDockerの落とし穴が二つあります。一つ目。イメージは、公式イメージが提供する特権のないnodeユーザーとして動かすべきです。二つ目。NodeはPID 1になるように設計されていません。ゾンビプロセスを回収しないのです。最小限のinitを差し込むために、docker run --init(またはComposeでのinit: true)でコンテナを起動しましょう。最後に、CMDはexecフォームで書きましょう。シェルフォームは/bin/shを差し込み、それがSIGTERMを転送しないため、前のレッスンのグレースフルシャットダウンは決して発動しません。
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"]単一のNodeプロセスは1コアしか使いません。clusterモジュールは、コアごとに1つのワーカープロセスを作ります。プライマリがポートで待ち受け、接続をラウンドロビンでワーカーに振り分けます(Windowsを除くデフォルトの挙動です)。各ワーカーは自分自身のメモリと自分自身のイベントループを持ちます。共有される状態はありません。セッションやキャッシュはRedisやそれに相当するものに置かなければなりません。純粋な計算には、SharedArrayBufferを介してメモリを共有するworker_threadsのほうを選びましょう。
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);
}