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のプロセスは結局いつか死にます。メモリの超過、バグ、例外です。プロセスマネージャーの役割は、それを再起動することです。二つの流派があります。むき出しのマシンでは、pm2(またはsystemd)が監視し、再起動し、ダウンタイムなしでリロードします。コンテナでは、この役割を担うのはオーケストレーター(Kubernetes、ECS)です。コンテナごとに1プロセスとなり、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 はコアごとに1ワーカーをフォークし、reloadはダウンタイムなし

Nodeに特有のDockerの落とし穴が二つあります。一つ目。イメージは、公式イメージが提供する特権のないnodeユーザーとして動かすべきです。二つ目。NodeはPID 1になるように設計されていません。ゾンビプロセスを回収しないのです。最小限のinitを差し込むために、docker run --init(またはComposeでのinit: true)でコンテナを起動しましょう。最後に、CMDexecフォームで書きましょう。シェルフォームは/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プロセスは1コアしか使いません。clusterモジュールは、コアごとに1つのワーカープロセスを作ります。プライマリがポートで待ち受け、接続をラウンドロビンでワーカーに振り分けます(Windowsを除くデフォルトの挙動です)。各ワーカーは自分自身のメモリと自分自身のイベントループを持ちます。共有される状態はありません。セッションやキャッシュはRedisやそれに相当するものに置かなければなりません。純粋な計算には、SharedArrayBufferを介してメモリを共有するworker_threadsのほうを選びましょう。

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);
}
コアごとに1ワーカー。死んだら自動的に再起動される

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. NODE_ENV=production は実際に何を変えますか?
    • Node.jsがより積極的なJITコンパイラを有効にする
    • Expressのようなライブラリが最適化(ビューのキャッシュ、詳細でないエラー)を有効にする
    • Node.jsがHTTPSなしでは起動を拒否する
  2. Dockerfileでexecフォームの CMD ["node", "server.js"] を選ぶのはなぜですか?
    • execフォームのほうが起動が速い
    • execフォームは変数の展開を許す
    • シェルフォームは/bin/shを経由し、それがNodeプロセスにSIGTERMを転送しない
  3. clusterで8つのワーカーを使うとき、メモリとアプリケーションの状態はどうなりますか?
    • 各ワーカーは別々のプロセス。メモリは8倍になり、共有される状態はない
    • ワーカーは同じV8ヒープを共有する
    • プライマリが定期的にワーカーのヒープを統合する