القرار · فصل ٥

كود Cluster جوه bin/www مع pm2 -i

cluster.isMaster · pm2 start -i 10 · EADDRINUSE · Sequelize pool
🎯 الهدف: تعرف إيه اللي بيحصل فعلًا لما ملف فيه كود cluster يتشغّل بـ pm2 start -i، وتنضّف الملف وتسيب الـ clustering لـ PM2.

الملف اللي بنتكلم عنه

ده الشكل المختصر لـ bin/www اللي فيه cluster بإيده، واتشغّل بـ pm2 start ./bin/www -i 10:

bin/www (قبل)
#!/usr/bin/env nodeconst cluster = require("cluster");const os = require("os");const app = require("../app");const http = require("http");const https = require("https");const fs = require("fs");process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";if (cluster.isMaster) {  for (let i = 0; i < os.cpus().length; i++) cluster.fork();  cluster.on("exit", (worker) => console.log(`worker ${worker.process.pid} died`));} else {  const port = normalizePort(process.env.PORT || "4000");  app.set("port", port);  if (process.env.SERVERHTTPS === "2") {    const server = http.createServer(app);    server.listen(port);    server.on("error", onError);    server.on("listening", onListening);  } else {    const options = {      key: fs.readFileSync(process.env.SSL_PUBLIC_KEY),      cert: fs.readFileSync(process.env.SSL_CERT_KEY),    };    const server = https.createServer(options, app);    server.addContext("example.com", options);    server.listen(port);  }}

الخلاصة

كود الـ cluster اللي جوه الملف مش هيشتغل خالص، وهيبقى عندك 10 workers بالظبط، مش 10 × عدد الـ cores.

اللي بيحصل فعليًا

  • -i 10 بيخلي PM2 يشتغل cluster mode ويعمل 10 workers.
  • PM2 نفسه هو الـ primary، وهو اللي بيعمل الـ workers بـ cluster.fork().
  • جوه كل worker من الـ 10، الملف bin/www بيتنفّذ، وcluster.isMaster قيمتها false، لأن الـ process دي أصلًا worker عند PM2.
  • فبيدخل الـ else على طول ويشغّل السيرفر على port 4000.
PM2 (primary - ماسك port 4000) ├─ worker 1  → bin/www → isMaster = false → else → listen(4000) ├─ worker 2  → bin/www → isMaster = false → else → listen(4000) └─ ... لحد 10

يعني الـ if بتاع cluster.isMaster كله كود ميت تحت PM2 بالأمر ده.

إمتى الـ if ده يشتغل؟ وهنا المشكلة

لو شغلت الملف بـ node bin/www أو بـ PM2 في fork mode، ساعتها isMaster هتبقى true وهيعمل workers بعدد الـ cores. ولو اتلخبط حد وشغّله fork بـ 10 instances:

terminal
pm2 start ./bin/www -i 10 -x   # -x = fork mode في الـ CLI
  • الـ CLI مفيهوش --exec-mode، والـ flag اللي بيجبر fork اسمه -x. والنتيجة نفسها لو كتبت exec_mode: "fork" مع instances: 10 في الـ ecosystem.
  • PM2 هيعمل 10 processes، وكل واحدة فيهم master، وكل master هيعمل workers بعدد الـ cores.
  • الـ workers بتوع أول master بس هيمسكوا port 4000.
  • الباقيين هيضربوا EADDRINUSE ويعملوا process.exit(1)، والـ master بتاعهم مش بيعمل fork تاني، لأن الـ exit handler بيطبع بس.
fork mode: 10 masters على port واحد
  1. PM2 ← master 1
  2. PM2 ← master 2..10
  3. master 1 ← workers: fork
  4. master 2..10 ← workers: fork

حاجات تانية لازم تاخد بالك منها

  • -i 10 رقم ثابت: لو السيرفر عنده 4 cores مثلًا، الـ 10 workers هيتخانقوا على الـ CPU ويزودوا الـ memory من غير فايدة. الأحسن -i max، أو رقم أقل من أو يساوي عدد الـ cores.
  • الـ DB connections: كل worker عنده Sequelize pool لوحده. الـ default في Sequelize هو max: 5، يعني 10 workers × 5 = لحد 50 connection على الداتابيز. اتأكد إن ده مناسب لـ max_connections عندك.
  • NODE_TLS_REJECT_UNAUTHORIZED = 0: ده بيقفل التحقق من الـ certificates لكل الـ requests الخارجة من السيرفر (axios وfetch وغيرهم)، وده خطر أمني (MITM). لو محتاجه لـ service معينة، اعمله على الـ agent بتاعها بس.
  • فرع الـ HTTPS مفيهوش on('error'): فلو الـ port مستخدم، هيرمي error من غير الرسالة الواضحة اللي في onError.
  • addContext بنفس الـ options ملوش لازمة، لأنها نفس الشهادة الأساسية.
  • cluster.isMaster بقت deprecated، والبديل cluster.isPrimary.
  • أي state في الـ memory، زي cron jobs أو socket.io أو cache أو rate limiter جوه app.js، هيتكرر 10 مرات، زي ما اتكلمنا في الفصل اللي فات.
لو service واحدة بس شهادتها self-signed
const https = require("https");const axios = require("axios");// التحقق مقفول للـ client ده بس، وباقي السيرفر سليمconst internalApi = axios.create({  baseURL: process.env.INTERNAL_API_URL,  httpsAgent: new https.Agent({ rejectUnauthorized: false }),});

والأحسن من كده إنك تدي الـ agent الـ CA بتاعة الـ service نفسها في ca، بدل ما تقفل التحقق.

التعديل المقترح

شيل كود الـ cluster من bin/www خالص، وخلي PM2 هو المسؤول عن الـ clustering. وابدأ الملف على طول من الـ else:

bin/www (بعد)
#!/usr/bin/env nodeconst app = require("../app");const http = require("http");const https = require("https");const fs = require("fs");const port = normalizePort(process.env.PORT || "4000");app.set("port", port);const server =  process.env.SERVERHTTPS === "2"    ? http.createServer(app)    : https.createServer(        {          key: fs.readFileSync(process.env.SSL_PUBLIC_KEY),          cert: fs.readFileSync(process.env.SSL_CERT_KEY),        },        app      );server.listen(port);server.on("error", onError);server.on("listening", onListening);// normalizePort و onError و onListening زي ما هما

كده الـ on('error') بقى على الفرعين، والـ addContext والـ TLS اتشالوا، ومبقاش فيه isMaster خالص.

وبعدها ecosystem file واضح بدل الأمر الطويل:

ecosystem.config.js
module.exports = {  apps: [    {      name: "LMS::backend",      script: "./bin/www",      exec_mode: "cluster",      instances: "max",      merge_logs: true,      time: true,      env: { PORT: 4000 },    },  ],};
terminal
pm2 start ecosystem.config.jspm2 reload "LMS::backend"   # للـ deploy من غير downtime

خلّصت الفصل لما

🧠 اختبر نفسك
١) pm2 start ./bin/www -i 10 على سيرفر 8 cores، والملف فيه cluster.fork لكل core. كام worker بيخدم؟
10 بالظبط. فرع الـ master مش بيتنفذ، لأن كل process هي worker عند PM2.
٢) نفس الملف بـ -i 10 -x. إيه اللي بيحصل؟
10 masters، كل واحد بيعمل 8 workers. workers بتوع أول master بس بيمسكوا 4000، والباقي بيقع بـ EADDRINUSE ومحدش بيرجّعه.
٣) 10 workers وSequelize pool max 5. أقصى connections كام؟
50 connection، فراجعها مع max_connections في الداتابيز.
٤) ليه NODE_TLS_REJECT_UNAUTHORIZED=0 خطر حتى لو محتاجه لـ API واحدة؟
لأنه بيقفل التحقق على كل الـ HTTPS الخارجة من الـ process، فأي request ممكن يتعمله MITM.