ส่งขึ้นระบบจริง ด้วย NODE_ENV ตัวจัดการโปรเซส อิมเมจ Docker ที่ถูกต้อง และการขยายบนหลายคอร์ด้วย cluster
เปิดบทเรียนนี้ใน KodokonNODE_ENV เป็นเพียง ธรรมเนียมของไลบรารี เท่านั้น แกนหลักของ Node แทบจะเพิกเฉยต่อมันโดยสิ้นเชิง แต่ Express และตัวอื่น ๆ ใช้มันเพื่อแคชวิว (view) และซ่อน stack trace บางเฟรมเวิร์กเร็วขึ้นหลายเท่าเมื่อตั้ง NODE_ENV=production ในด้านของ dependency นั้น npm ci --omit=dev จะติดตั้งตาม lockfile อย่างเป๊ะ ๆ โดยไม่มี dev dependency ทำซ้ำได้และเร็วกว่า npm install ตั้งแต่ Node 20.6 เป็นต้นมา --env-file โหลดไฟล์ .env ได้แบบเนทีฟ โดยไม่ต้องพึ่ง dependency
NODE_ENV=production node server.js
npm ci --omit=dev
node --env-file=.env server.jsโปรเซส Node จะจบลงด้วยการตายเสมอ ไม่ว่าจะเป็นหน่วยความจำล้น บั๊ก หรือ exception บทบาทของ ตัวจัดการโปรเซส (process manager) คือการรีสตาร์ตมัน มีอยู่สองสำนัก บนเครื่องเปล่า pm2 (หรือ systemd) จะดูแล รีสตาร์ต และรีโหลดโดยไม่มีช่วงหยุดทำงาน ส่วนในคอนเทนเนอร์ ตัวจัดการ (Kubernetes, ECS) จะรับบทนี้ คือหนึ่งโปรเซสต่อหนึ่งคอนเทนเนอร์ และ pm2 จะกลายเป็นสิ่งไร้ประโยชน์ ถึงขั้นเป็นโทษ เพราะมันซ่อนการแครชจากตัวจัดการ
npm install -g pm2
pm2 start server.js -i max --name api
pm2 reload api
pm2 logs api --lines 100กับดัก Docker สองอย่างที่เฉพาะเจาะจงกับ Node ข้อหนึ่ง อิมเมจควรรันในฐานะผู้ใช้ node ที่ไม่มีสิทธิพิเศษ ซึ่งอิมเมจทางการมีให้ ข้อสอง Node ไม่ได้ถูกออกแบบมาให้เป็น PID 1 มันไม่เก็บกวาดโปรเซสซอมบี้ จงเปิดคอนเทนเนอร์ด้วย docker run --init (หรือ init: true ใน Compose) เพื่อแทรก init ขั้นต่ำเข้าไป สุดท้าย จงเขียน CMD ในรูปแบบ exec form เพราะรูปแบบ shell form จะแทรก /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 เดียวใช้เพียงคอร์เดียว โมดูล cluster สร้างโปรเซส worker หนึ่งตัวต่อหนึ่งคอร์ โดยตัวหลัก (primary) จะฟังที่พอร์ตและกระจายการเชื่อมต่อไปยัง worker แบบ round-robin (พฤติกรรมเริ่มต้น ยกเว้นบน Windows) แต่ละ worker มีหน่วยความจำและอีเวนต์ลูปเป็นของตัวเอง คือ ไม่มีสถานะที่แชร์กัน เซสชันและแคชจึงต้องอยู่ใน Redis หรือสิ่งที่เทียบเท่า สำหรับการคำนวณล้วน ๆ ให้เลือกใช้ worker_threads ซึ่งแชร์หน่วยความจำผ่าน SharedArrayBuffer
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);
}