يا أهلاً وسهلاً فيكم يا جماعة الخير. معكم أخوكم أبو عمر.
خليني أحكيلكم قصة صارت معي قبل كم سنة، قصة علّمتني درس ما بنساه في عالم البرمجة والأداء العالي. كنا وقتها شغالين على تطبيق جديد، تطبيق “مرتب” زي ما بنحكي، وكان فيه ميزة أساسية: المستخدم بيقدر يطلب تصدير تقرير شامل لكل بياناته على شكل ملف PDF. الشغل كان تمام، والتطبيق سريع، والأمور في السليم.
وجاء يوم الإطلاق. عملنا حملة تسويقية بسيطة، وتفاجأنا بعدد المستخدمين اللي سجلوا وبدأوا يستخدموا التطبيق. الفرحة كانت غامرة، لحد ما بدأت توصلنا رسايل على الدعم الفني: “التطبيق بطيء جداً”، “طلبت تقرير ومن وقتها التطبيق معلّق”، “الصفحة ما بتحمّل!”.
دخلنا على السيرفرات، لقينا المعالجات (CPUs) بتصيّح من الضغط، والذاكرة شبه ممتلئة. قضينا ساعات طويلة بنحلل الأكواد ونبحث عن المشكلة. الكود نفسه كان نظيف وما فيه أخطاء منطقية. طيب وين المشكلة؟ بعد ليلة طويلة من القهوة والبحث والتمحيص، صرخ واحد من الشباب في الفريق: “يا جماعة… الطلبات متزامنة! (Synchronous)”.
كانت هي لحظة “وجدتها!”. المشكلة ما كانت في “ماذا” يفعله الكود، بل في “كيف” يفعله. كان السيرفر يستقبل طلب “تصدير التقرير”، ويظل واقف مستني، ماسك “الخيط” (Thread) ومش راضي يفلته لحد ما يخلّص إنشاء الملف كامل، وهي عملية كانت تاخذ أحياناً دقيقة أو دقيقتين. خلال هالدقيقة، أي مستخدم ثاني يحاول يفتح صفحة بسيطة، كان طلبه ينتظر في طابور طويل خلف طلب التقرير الثقيل. كنا بالزبط زي اللي حاطين موظف واحد على كاونتر بنك، وواحد من الزباين طلب منه يعدّ كل القروش اللي في الخزنة. الكل راح يستنى!
هون كان لازم نلاقي حل جذري، حل ينقذنا من عنق الزجاجة هذا. وكان الحل هو “طوابير الرسائل” أو الـ Message Queues.
ما هي المعالجة المتزامنة (Synchronous) وما مشكلتها؟
قبل ما نغوص في الحل، خلينا نوضح المشكلة الأصلية ببساطة. المعالجة المتزامنة معناها إن المهام بتتنفذ بالترتيب، واحدة تلو الأخرى. لما تطبيقك يستقبل طلب من مستخدم، بوقف كل شي تقريباً عشان يخدم هالطلب، ويرجعله الرد. وبعدها بس، بكون جاهز يستقبل الطلب اللي بعده.
هذا الأسلوب ممتاز للمهام السريعة (أقل من 500 ميلي ثانية). مثلاً، طلب عرض بيانات مستخدم، أو حفظ تعليق. لكن تخيل معي السيناريو الكارثي تبعنا:
- المستخدم يضغط على زر “تصدير التقرير”.
- المتصفح يرسل طلب (HTTP Request) للسيرفر.
- السيرفر يستقبل الطلب ويبدأ عملية طويلة: تجميع البيانات من قاعدة البيانات، معالجتها، تصميم الـ PDF، وحفظ الملف. هذه العملية قد تستغرق 90 ثانية.
- طوال هذه الـ 90 ثانية، السيرفر “مشغول”. الخيط اللي بيعالج الطلب محجوز بالكامل.
- المستخدم نفسه شايف شاشة تحميل بتلف، وممكن يفكر التطبيق “علّق” ويغلق الصفحة.
- والأسوأ، مستخدمون آخرون يحاولون القيام بعمليات بسيطة، طلباتهم تتراكم وتنتظر انتهاء عملية التقرير الطويلة.
النتيجة: تجربة مستخدم سيئة جداً، موارد سيرفر مهدرة في الانتظار، وصعوبة هائلة في التوسع لمواجهة أي زيادة في الأحمال.
المنقذ وصل: مفهوم طوابير الرسائل (Message Queues)
طابور الرسائل هو وسيط، زي ساعي البريد الذكي. بدل ما تطبيقك الرئيسي (الـ Web Server) يقوم بالمهمة الثقيلة بنفسه، هو بس بيكتب “رسالة” فيها تفاصيل المهمة المطلوبة (مثلاً: “اصنع تقرير للمستخدم رقم 123 بهذه المواصفات”)، وبيحطها في “صندوق بريد” خاص، اللي هو الطابور (Queue).
بعد ما يحط الرسالة، بيرجع فوراً للمستخدم وبقله: “تمام، استلمنا طلبك، وبنشتغل عليه. رح نبعتلك إشعار بس يجهز التقرير”. هيك المستخدم حس باستجابة فورية والتطبيق ما “علّق” معه.
في مكان ثاني، على سيرفر آخر أو في عملية منفصلة تماماً، فيه “عامل” متخصص (بنسميه Consumer أو Worker). هالعامل وظيفته الوحيدة إنه يضل يراقب صندوق البريد (الـ Queue). كل ما يلاقي رسالة جديدة، بياخذها، وبيقرأها، وبنفذ المهمة المطلوبة بهدوء وبدون ما يزعج السيرفر الرئيسي. ولما يخلص، ممكن يبعت إشعار للمستخدم أو يحدّث حالة الطلب في قاعدة البيانات.
المكونات الأساسية للنظام:
- المنتج (Producer): هو الجزء من الكود اللي بينشئ الرسالة ويضعها في الطابور. في حالتنا، هو الكود المسؤول عن استقبال طلب “تصدير التقرير” من المستخدم.
- الطابور (Queue): هو الوسيط الذي يخزن الرسائل بشكل مؤقت. مثل RabbitMQ, AWS SQS, Kafka.
- المستهلك (Consumer / Worker): هو عملية أو برنامج منفصل، وظيفته سحب الرسائل من الطابور ومعالجتها.
كيف طبقنا طوابير الرسائل لحل مشكلتنا؟ (مثال عملي)
خلينا نشوف الفرق بالكود. لنفترض أننا نستخدم Node.js مع Express.js.
قبل الطابور: الكود المتزامن الكارثي
كان الكود تبعنا يشبه هذا:
// app.js - The Synchronous Hell
const express = require('express');
const { generateLargePdfReport } = require('./report-generator');
const app = express();
app.post('/export-report', async (req, res) => {
try {
console.log("Request received. Starting report generation...");
// هذه العملية قد تستغرق 90 ثانية أو أكثر!
const reportFile = await generateLargePdfReport(req.user.id);
console.log("Report generated. Sending response.");
// لن يتم إرسال الرد إلا بعد انتهاء العملية الطويلة
res.download(reportFile);
} catch (error) {
res.status(500).send("Failed to generate report.");
}
});
app.listen(3000, () => console.log('Server is busy waiting...'));
المشكلة واضحة في السطر await generateLargePdfReport(req.user.id). الطلب معلّق هنا حتى تكتمل المهمة.
بعد الطابور: الكود غير المتزامن الرشيق
الآن، استخدمنا مكتبة مثل amqplib للتواصل مع طابور رسائل (RabbitMQ كمثال). صار عنا ملفين: واحد للسيرفر الرئيسي، وواحد للعامل (Worker).
1. الكود في السيرفر الرئيسي (Producer)
السيرفر الآن وظيفته فقط إضافة مهمة للطابور والرد فوراً.
// app.js - The Asynchronous Heaven
const express = require('express');
const amqp = require('amqplib');
const app = express();
const RABBITMQ_URL = 'amqp://localhost';
const QUEUE_NAME = 'report_generation_queue';
app.post('/export-report', async (req, res) => {
try {
// 1. الاتصال بـ RabbitMQ
const connection = await amqp.connect(RABBITMQ_URL);
const channel = await connection.createChannel();
// 2. التأكد من وجود الطابور
await channel.assertQueue(QUEUE_NAME, { durable: true });
// 3. تحضير الرسالة
const message = { userId: req.user.id, params: req.body.params };
// 4. إرسال الرسالة إلى الطابور
channel.sendToQueue(QUEUE_NAME, Buffer.from(JSON.stringify(message)), { persistent: true });
console.log(" [x] Sent '%s'", message);
await channel.close();
await connection.close();
// 5. الرد على المستخدم فوراً!
res.status(202).send({
message: "طلبك قيد المعالجة. سنرسل لك إشعاراً عند اكتمال التقرير."
});
} catch (error) {
res.status(500).send("Could not queue your request. Please try again later.");
}
});
app.listen(3000, () => console.log('Server is ready and responsive!'));
لاحظ كيف أن الرد (res.status(202).send(...)) يحدث بشكل فوري تقريباً بعد إرسال الرسالة للطابور.
2. الكود في العامل المنفصل (Consumer/Worker)
هذا برنامج يعمل في الخلفية، كل وظيفته هي الاستماع للطابور وتنفيذ المهام.
// worker.js - The Hard Worker
const amqp = require('amqplib');
const { generateLargePdfReport } = require('./report-generator');
const { notifyUser } = require('./notification-service');
const RABBITMQ_URL = 'amqp://localhost';
const QUEUE_NAME = 'report_generation_queue';
async function startWorker() {
const connection = await amqp.connect(RABBITMQ_URL);
const channel = await connection.createChannel();
await channel.assertQueue(QUEUE_NAME, { durable: true });
// prefetch(1) تضمن أن العامل لا يأخذ أكثر من رسالة واحدة في كل مرة
// هذا يمنع عامل واحد من حجز كل المهام بينما العمال الآخرون عاطلون
channel.prefetch(1);
console.log(" [*] Waiting for messages in %s. To exit press CTRL+C", QUEUE_NAME);
channel.consume(QUEUE_NAME, async (msg) => {
const messageData = JSON.parse(msg.content.toString());
console.log(" [x] Received message for user:", messageData.userId);
try {
// هنا تحدث العملية الطويلة، بعيداً عن السيرفر الرئيسي
const reportFile = await generateLargePdfReport(messageData.userId, messageData.params);
// بعد الانتهاء، نرسل إشعاراً للمستخدم
await notifyUser(messageData.userId, `Your report is ready! Download it here: ${reportFile.url}`);
console.log(" [x] Done processing message for user:", messageData.userId);
// نؤكد للقناة أننا انتهينا من معالجة الرسالة بنجاح لحذفها من الطابور
channel.ack(msg);
} catch (err) {
console.error("Failed to process message:", err);
// في حالة الفشل، يمكننا إعادة الرسالة للطابور أو تسجيلها للمحاولة لاحقاً
// channel.nack(msg);
}
}, {
noAck: false // نضبطها على false لنتمكن من عمل ack يدوياً
});
}
startWorker();
فوائد استخدام طوابير الرسائل التي لمسناها بأنفسنا
بعد تطبيق هذا النظام، النتائج كانت مذهلة وفورية:
- تحسين تجربة المستخدم: التطبيق صار “صاروخ”. الاستجابة فورية، والمستخدم لم يعد يشعر بأي تعليق أو بطء.
- زيادة الموثوقية (Reliability): في النظام القديم، لو انقطع الاتصال أو فشل السيرفر أثناء إنشاء التقرير، يضيع الطلب. الآن، الرسالة تبقى محفوظة في الطابور. لو فشل الـ Worker، يمكن إعادة معالجة الرسالة تلقائياً أو بواسطة عامل آخر.
- قابلية التوسع (Scalability): هذه هي الجوهرة الحقيقية. عندما زاد الضغط على إنشاء التقارير، لم نلمس السيرفر الرئيسي. كل ما فعلناه هو تشغيل المزيد من نسخ الـ
worker.js. كان عنا 5 عمال، ثم 10، ثم 20، كلهم يسحبون المهام من نفس الطابور ويعملون على التوازي. هذا ما يسمى بالتوسع الأفقي (Horizontal Scaling). - فصل الخدمات (Decoupling): أصبح سيرفر الويب لا يعرف شيئاً عن كيفية إنشاء التقارير، والعامل لا يعرف شيئاً عن سيرفر الويب. هذا الفصل يسهل الصيانة والتطوير. يمكننا تحديث منطق إنشاء التقارير بدون إعادة تشغيل التطبيق الرئيسي والعكس صحيح.
نصائح من كيس أبو عمر: متى وكيف تستخدم طوابير الرسائل؟
من خبرتي، هذه الأداة قوية جداً، لكنها ليست الحل لكل شيء. إليك بعض النصائح العملية:
متى تستخدمها؟
- العمليات طويلة الأمد: أي شيء يأخذ أكثر من ثانية أو ثانيتين. مثل معالجة الفيديوهات، ضغط الصور، إرسال الإيميلات دفعة واحدة، أو توليد التقارير المعقدة.
- للتواصل بين الخدمات المصغرة (Microservices): هي الطريقة المثلى لجعل الخدمات تتحدث مع بعضها البعض بشكل غير متزامن وموثوق.
- لمواجهة “الانفجارات” في الطلبات (Bursty Traffic): لو عندك نظام يستقبل 1000 طلب في دقيقة واحدة ثم يهدأ، الطابور يعمل كـ “مخزن مؤقت” (Buffer)، يجمع كل الطلبات ثم يقوم العمال بمعالجتها على مهل.
- لضمان تنفيذ المهام: عندما يكون من الضروري جداً ألا تضيع مهمة ما (مثل معالجة دفعة مالية)، الطوابير تضمن بقاء المهمة حتى يتم تنفيذها بنجاح.
أشهر الأدوات في السوق
نصيحة أبو عمر: لا ترهق نفسك في البداية بالاختيار. ابدأ بشيء بسيط ومشهور. RabbitMQ خيار ممتاز كبداية لأنه “Broker” تقليدي وسهل الفهم. Redis يمكن استخدامه كطابور بسيط جداً للمهام غير الحرجة. عندما تكبر، يمكنك النظر في حلول أكثر تخصصاً مثل Kafka (للبيانات المتدفقة) أو الخدمات السحابية المدارة مثل AWS SQS أو Google Cloud Pub/Sub لتريح رأسك من عناء الإدارة.
احذر من هذه الأفخاخ
- التعقيد الإضافي: أنت تضيف مكوناً جديداً (نظام الطوابير) إلى بنيتك التحتية. هذا يعني أنه يحتاج إلى مراقبة، صيانة، وتحديث.
- المعالجة المكررة (Idempotency): ماذا لو عالج العامل الرسالة بنجاح، ولكن انقطع الاتصال قبل أن يرسل تأكيد (ack) للطابور؟ الطابور سيعتقد أن الرسالة لم تعالج وسيعطيها لعامل آخر. يجب أن تصمم مهامك بحيث لو تم تنفيذها مرتين، تكون النتيجة واحدة (هذا ما يسمى بالـ Idempotency). مثلاً، بدلاً من “أضف 10 إلى الرصيد”، اجعلها “اجعل الرصيد 110”.
- ترتيب الرسائل: لا تفترض أن الرسائل ستتم معالجتها بنفس الترتيب الذي أُرسلت به، خاصة عند وجود عدة عمال. إذا كان الترتيب مهماً جداً، ستحتاج إلى تقنيات متقدمة أو طوابير خاصة تضمن ذلك (مثل Kafka Partitions أو RabbitMQ single-active-consumer).
الخلاصة: من عنق الزجاجة إلى الطريق السريع 🚀
التحول من المعالجة المتزامنة إلى غير المتزامنة باستخدام طوابير الرسائل كان نقلة نوعية في طريقة تفكيرنا وتصميمنا للتطبيقات. لقد حول تطبيقنا من سيارة قديمة عالقة في زحمة سير خانقة، إلى أسطول من الشاحنات على طريق سريع مفتوح، كل شاحنة (Worker) تحمل مهمة وتوصلها لوجهتها بكفاءة واستقلالية.
قد تبدو طوابير الرسائل معقدة في البداية، ولكن الفائدة التي ستحصل عليها في الأداء والموثوقية وقابلية التوسع تستحق هذا الجهد الأولي. ابدأ صغيراً، جربها على مهمة واحدة طويلة في تطبيقك، وشاهد الفرق بنفسك.
وتذكروا نصيحة أخوكم أبو عمر دائماً: “لا تجعل المستخدم ينتظر. إذا كانت المهمة ستستغرق أكثر من بضع ثوانٍ، فكّر فورًا في وضعها في طابور. وقت المستخدم أثمن من أن يضيع في شاشة تحميل.”
بالتوفيق يا جماعة الخير!