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) هو من يؤدّي هذا الدور - عملية واحدة لكل حاوية، وتصبح 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 ينشئ عاملًا واحدًا لكل نواة، وreload يعيد التحميل دون توقّف.

مزلقان في Docker خاصّان بـ Node. الأول: ينبغي أن تعمل الصورة بمستخدم node غير المتميّز، الذي توفّره الصور الرسمية. الثاني: لم تُصمَّم Node لتكون PID 1 - فهي لا تحصد العمليات الشبحية (zombie)؛ فأطلِق الحاوية بـ docker run --init (أو init: true في Compose) لإدراج init بسيط. وأخيرًا، اكتب CMD بـ الصيغة التنفيذية (exec form): فالصيغة الصدفية (shell form) تُدرِج /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، وCMD بالصيغة التنفيذية.

تستخدم عملية Node الواحدة نواةً واحدة فقط. وتُنشئ الوحدة cluster عملية عامل (worker) واحدة لكل نواة: إذ تستمع العملية الرئيسية (primary) إلى المنفذ وتوزّع الاتصالات على العُمّال بالتناوب الدوري (round-robin) (وهو السلوك الافتراضي، عدا على Windows). ولكل عامل ذاكرته الخاصة وحلقة أحداثه الخاصة: لا حالة مشتركة - فالجلسات والذاكرات المؤقّتة يجب أن تعيش في 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);
}
عامل واحد لكل نواة، يُعاد تشغيله تلقائيًا إذا مات.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. ماذا يُغيِّر NODE_ENV=production فعليًا؟
    • تُفعِّل Node.js مُصرِّف JIT أكثر عدوانية
    • تُفعِّل مكتبات مثل Express تحسيناتها (التخزين المؤقّت للعروض، والأخطاء غير المُسهَبة)
    • ترفض Node.js البدء دون HTTPS
  2. لماذا نفضّل الصيغة التنفيذية CMD ["node", "server.js"] في ملف Dockerfile؟
    • الصيغة التنفيذية تبدأ أسرع
    • الصيغة التنفيذية تسمح بإدراج المتغيرات
    • الصيغة الصدفية تمرّ عبر /bin/sh، التي لا تُمرِّر SIGTERM إلى عملية Node
  3. مع cluster و8 عُمّال، ماذا يحدث للذاكرة ولحالة التطبيق؟
    • كل عامل عملية منفصلة: الذاكرة مضروبة في 8، ولا حالة مشتركة
    • يتشارك العُمّال كومة V8 نفسها
    • تدمج العملية الرئيسية كومات العُمّال دوريًا