الفكرة
في fork، كل instance بتبقى process مستقلة: ليها memory لوحدها، وevent loop لوحده، وفي الغالب port لوحده. مفيش أي حاجة مشتركة بين الـ processes.
الـ processes مش بتتقاسم port، فلازم حد قدامهم يوزّع الطلبات عليهم. والحد ده غالبًا Nginx.
- Client ← Nginx: request
- Nginx ← process 1
- Nginx ← process 2
- Nginx ← process 3
بإيدك: سكريبت بيشغّل 3 processes
fork-launcher.js
const { fork } = require("node:child_process");const ports = [3001, 3002, 3003];for (const port of ports) { // كل واحدة process جديدة تمامًا، وبناخد الـ port من env const child = fork("./server.js", [], { env: { ...process.env, PORT: String(port) }, }); child.on("exit", (code) => console.log(`port ${port} exited with ${code}`));}السكريبت ده بيعمل fork وبس: مش بيرجّع process لو وقعت، ومش بيوزّع طلبات. وعشان العميل يكلم port واحد، محتاج Nginx:
/etc/nginx/conf.d/app.conf
upstream app { server 127.0.0.1:3001; server 127.0.0.1:3002; server 127.0.0.1:3003;}server { listen 80; location / { proxy_pass http://app; }}نفس الكلام بـ PM2
بدل السكريبت، PM2 بيشغّل الـ processes ويرجّع أي واحدة تقع. والمشكلة إن كلهم هيقروا نفس PORT، فنستخدم increment_var:
ecosystem.config.js
module.exports = { apps: [ { name: "api-fork", script: "./server.js", exec_mode: "fork", instances: 3, increment_var: "PORT", // 3001، 3002، 3003 env: { PORT: 3001 }, }, ],};terminal
pm2 start ecosystem.config.jspm2 list # عمود mode = forkcurl localhost:3001; curl localhost:3002; curl localhost:3003- PM2 بيزوّد
PORTبواحد لكل instance جديدة. - لو نسيت
increment_var، كلهم هيحاولوا يفتحوا 3001، والتانية والتالتة هيقعوا بـEADDRINUSE. - Nginx لسه مطلوب قدامهم. PM2 في fork مش بيوزّع ترافيك.
خلّصت الفصل لما
🧠 اختبر نفسك
١) 3 instances fork من غير increment_var على PORT=3001. إيه اللي هيحصل؟
واحدة بس هتفتح 3001، والاتنين التانيين هيقعوا بـ EADDRINUSE وPM2 هيفضل يعيد تشغيلهم.
٢) مين بيوزّع الطلبات في fork؟
حد برّه PM2، زي Nginx أو load balancer.
٣) عندي worker بيسحب jobs من Redis. fork ولا cluster؟
fork، لأنه مش بيسمع على port، وcluster ميزته الأساسية تقسيم الـ port.