إدارة العمليات والتجميع
يعمل Node.js على خيط واحد بشكل افتراضي، مما يعني أنه يمكنه استخدام نواة CPU واحدة فقط. للاستفادة الكاملة من الأنظمة متعددة النوى وضمان التوفر العالي، نحتاج إلى فهم إدارة العمليات والتجميع.
فهم قيود الخيط الواحد
يستخدم Node.js نموذج I/O غير محظور يعمل بالأحداث يعمل على خيط واحد. بينما هذا فعال لعمليات I/O، فإن المهام التي تتطلب استخدامًا مكثفًا للمعالج يمكن أن تحجب التطبيق بأكمله:
const express = require('express');
const app = express();
app.get('/fast', (req, res) => {
res.json({ message: 'This is fast!' });
});
app.get('/slow', (req, res) => {
// مهمة مكثفة للمعالج تحجب الخادم بأكمله
let result = 0;
for (let i = 0; i < 5000000000; i++) {
result += i;
}
res.json({ result });
});
app.listen(3000);
تحذير: أثناء معالجة نقطة النهاية /slow، سيتم حجب جميع الطلبات الأخرى (بما في ذلك /fast) لأن Node.js يعمل على خيط واحد.
وحدة Cluster
يتضمن Node.js وحدة تجميع مدمجة تسمح لك بإنشاء عمليات فرعية (عمال) تشارك نفس منفذ الخادم:
const cluster = require('cluster');
const http = require('http');
const os = require('os');
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
// إنشاء عمال لكل نواة CPU
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
// استبدال العامل الميت
cluster.fork();
});
} else {
// العمال يشاركون اتصال TCP
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello from worker ' + process.pid);
});
server.listen(3000);
console.log(`Worker ${process.pid} started`);
}
التجميع مع Express
إليك كيفية تنفيذ التجميع مع تطبيق Express:
// server.js
const cluster = require('cluster');
const os = require('os');
const app = require('./app'); // تطبيق Express الخاص بك
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
console.log(`Master process ${process.pid} starting ${numCPUs} workers`);
for (let i = 0; i < numCPUs; i++) {
const worker = cluster.fork();
console.log(`Worker ${worker.process.pid} spawned`);
}
cluster.on('online', (worker) => {
console.log(`Worker ${worker.process.pid} is online`);
});
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died with code ${code}`);
if (!worker.exitedAfterDisconnect) {
console.log('Worker crashed. Starting a new worker...');
cluster.fork();
}
});
// إيقاف سلس
process.on('SIGTERM', () => {
console.log('SIGTERM received. Shutting down gracefully...');
for (const id in cluster.workers) {
cluster.workers[id].disconnect();
}
});
} else {
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Worker ${process.pid} listening on port ${PORT}`);
});
}
ملاحظة: لا تنفذ العملية الرئيسية كود التطبيق. إنها تدير فقط عمليات العمال. كل منطق التطبيق يعمل في عمليات العمال.
التواصل بين العمال
يمكن للعمال التواصل مع العملية الرئيسية ومع بعضهم البعض:
if (cluster.isMaster) {
const worker = cluster.fork();
// إرسال رسالة إلى العامل
worker.send({ cmd: 'notifyRequest' });
// استقبال رسالة من العامل
worker.on('message', (msg) => {
console.log('Master received:', msg);
});
} else {
// العامل يستقبل رسالة من الرئيسي
process.on('message', (msg) => {
if (msg.cmd === 'notifyRequest') {
console.log('Worker notified');
}
});
// العامل يرسل رسالة إلى الرئيسي
process.send({ status: 'ready' });
}
مدير العمليات PM2
PM2 هو مدير عمليات من درجة الإنتاج لتطبيقات Node.js مع موازنة تحميل مدمجة:
# تثبيت PM2 عالميًا
npm install -g pm2
# بدء تطبيقك مع التجميع
pm2 start app.js -i max # max = عدد نوى CPU
# أو تحديد عدد النسخ
pm2 start app.js -i 4
# البدء باسم مخصص
pm2 start app.js -i max --name "my-app"
ملف تكوين PM2
أنشئ ملف ecosystem.config.js لتكوين PM2 المتقدم:
module.exports = {
apps: [{
name: 'api-server',
script: './server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 8080
},
max_memory_restart: '500M',
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
autorestart: true,
watch: false,
max_restarts: 10,
min_uptime: '10s'
}]
};
# استخدام ملف التكوين
pm2 start ecosystem.config.js
# البدء في وضع الإنتاج
pm2 start ecosystem.config.js --env production
أوامر PM2
# مراقبة جميع العمليات
pm2 monit
# قائمة جميع العمليات
pm2 list
# إظهار تفاصيل العملية
pm2 show api-server
# عرض السجلات
pm2 logs
pm2 logs api-server
# إعادة تشغيل التطبيق
pm2 restart api-server
# إعادة تحميل التطبيق (إعادة تشغيل بدون توقف)
pm2 reload api-server
# إيقاف التطبيق
pm2 stop api-server
# حذف التطبيق من PM2
pm2 delete api-server
# حفظ قائمة العمليات
pm2 save
# إحياء العمليات المحفوظة
pm2 resurrect
# سكريبت بدء التشغيل (يعمل عند بدء النظام)
pm2 startup
نصيحة: استخدم pm2 reload بدلاً من pm2 restart للنشر بدون توقف في وضع التجميع.
إعادة التشغيل بدون توقف
ميزة إعادة التحميل في PM2 تمكن النشر بدون توقف:
// app.js - معالجة الإيقاف السلس
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World');
});
const server = app.listen(3000);
// إيقاف سلس
process.on('SIGINT', () => {
console.log('SIGINT received. Closing server gracefully...');
server.close(() => {
console.log('Server closed. Exiting process.');
process.exit(0);
});
// إغلاق قسري بعد 10 ثواني
setTimeout(() => {
console.error('Forcing shutdown after timeout');
process.exit(1);
}, 10000);
});
استراتيجية موازنة التحميل
يستخدم PM2 خوارزمية موازنة تحميل round-robin بشكل افتراضي، ولكن يمكنك تخصيصها:
module.exports = {
apps: [{
name: 'api',
script: './server.js',
instances: 4,
exec_mode: 'cluster',
instance_var: 'INSTANCE_ID',
// استراتيجيات موازنة التحميل
listen_timeout: 3000,
kill_timeout: 5000
}]
};
المراقبة والمقاييس
# تمكين مراقبة الويب PM2
pm2 web
# مراقبة استخدام CPU والذاكرة
pm2 monit
# إنشاء سكريبت بدء التشغيل لبدء النظام
pm2 startup
# حفظ قائمة العمليات الحالية
pm2 save
# إعادة تعيين عداد إعادة التشغيل
pm2 reset api-server
# إظهار المقاييس
pm2 describe api-server
معالجة الحالة المشتركة
نظرًا لأن كل عامل هو عملية منفصلة، فإنهم لا يشاركون الذاكرة. استخدم تخزين خارجي للحالة المشتركة:
const express = require('express');
const redis = require('redis');
const app = express();
// استخدام Redis للحالة المشتركة
const client = redis.createClient();
app.get('/counter', async (req, res) => {
// زيادة العداد في Redis (مشترك عبر جميع العمال)
const count = await client.incr('page:counter');
res.json({
count,
worker: process.pid
});
});
تحذير: لا تستخدم أبدًا متغيرات في الذاكرة للحالة المشتركة في التطبيقات المجمعة. استخدم مخازن خارجية مثل Redis أو MongoDB أو PostgreSQL.
أفضل الممارسات
- عدد العمال: استخدم
os.cpus().length أو أقل قليلاً لترك موارد لنظام التشغيل
- حدود الذاكرة: اضبط
max_memory_restart في PM2 لمنع تسرب الذاكرة
- الإيقاف السلس: قم دائمًا بتنفيذ التنظيف المناسب على إشارات SIGINT/SIGTERM
- فحوصات الصحة: قم بتنفيذ نقاط نهاية /health لموازنات التحميل
- التسجيل: استخدم التسجيل المركزي (مثل Winston أو Bunyan) لتجميع السجلات من جميع العمال
- معالجة الأخطاء: استخدم cluster.on('exit') لإعادة تشغيل العمال المعطلين تلقائيًا
تمرين تطبيقي:
أنشئ تطبيق Node.js بالمتطلبات التالية:
- تنفيذ التجميع لاستخدام جميع نوى CPU
- إضافة نقطة نهاية /health تعيد PID العامل ووقت التشغيل
- تنفيذ معالجة الإيقاف السلس
- إنشاء ecosystem.config.js لـ PM2 مع إعدادات الإنتاج
- إضافة نقطة نهاية مكثفة للمعالج (/calculate) واختبار توزيع التحميل عبر العمال
- استخدام Redis لتتبع إجمالي عدد الطلبات عبر جميع العمال
الخلاصة
في هذا الدرس، تعلمت:
- فهم قيود الخيط الواحد في Node.js
- استخدام وحدة cluster لاستخدام النوى المتعددة
- تنفيذ PM2 لإدارة عمليات الإنتاج
- النشر بدون توقف مع إعادة تحميل PM2
- التواصل والتنسيق بين العمال
- استراتيجيات موازنة التحميل
- معالجة الحالة المشتركة في البيئات المجمعة
- أفضل الممارسات لإدارة العمليات