الملف اللي بنتكلم عنه
ده الشكل المختصر لـ 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 بيطبع بس.
- PM2 ← master 1
- PM2 ← master 2..10
- master 1 ← workers: fork
- 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.