طلبات لا تنتهي؟ كيف أنقذتنا قوائم انتظار الرسائل (Message Queues) من انهيار النظام

يا جماعة الخير، السلام عليكم ورحمة الله.

بتذكر هذيك الليلة، كأنها امبارح. كنا قاعدين في المكتب، الساعة فاتت الواحدة بعد نص الليل، وريحة القهوة معبية الجو. أطلقنا ميزة جديدة في تطبيقنا، ميزة بتسمح للمستخدم يرفع فيديو قصير، ونظامنا بيعمل عليه شوية معالجة، زي ضغط الفيديو وإضافة علامة مائية. الأمور كانت “تمام التمام” في مرحلة الاختبار، وشغلنا كان نظيف ومُرتب.

بس يا خوي، الواقع إشي ثاني خالص. أول ما أطلقنا الميزة وبدأ الضغط الحقيقي من المستخدمين، بدأت الكوابيس. التلفونات ما سكتت، ورسائل الدعم الفني صارت زي المطر. “التطبيق بعلّق!”، “الفيديو ما برفع!”، “بستنى ساعة ولسا ما خلص!”. فتحت لوحة مراقبة الخوادم (Server Dashboard)، وشفت اللي كنت خايف منه: استهلاك المعالج (CPU) في السما، والذاكرة (RAM) بتصيح “ارحموني!”. الخوادم كانت بتوقع واحد ورا الثاني.

كانت طلبات المستخدمين بتتكدس ورا بعض، زي طابور الخبز يوم الجمعة الصبح، بس الفرق إنه طابورنا كان رقمي، وكل واحد في الطابور كان بيصرخ وبيشكي. المشكلة ما كانت في “كمية” الشغل، المشكلة كانت في “طريقة” الشغل. كنا بنجبر الخادم يستقبل الطلب، ويعالج الفيديو، وبعدين يرجع الجواب للمستخدم، كله في نفس النفس، أو ما يُعرف بالـ Synchronous Operation. وهذا كان خطأ قاتل.

هون، وقفت وشربت كاسة شاي بالمرمية، وقلت للفريق: “يا جماعة، المشكلة مش في كود معالجة الفيديو، المشكلة إنه إحنا بنخلي المستخدم يستنى العامل يخلص خبز الرغيف تبعه بإيده. لازم نلاقي طريقة نفصل فيها بين اللي بيطلب الخبز (المستخدم) والعامل اللي بيخبز (الخادم)”. ومن هنا، بدأت رحلتنا مع منقذنا: قوائم انتظار الرسائل (Message Queues).

ما هي “قوائم انتظار الرسائل” (Message Queues)؟ وليش هي مهمة؟

ببساطة شديدة، تخيلها زي مكتب البريد. لما بدك تبعت رسالة (طلب معالجة)، أنت ما بتروح توقف فوق راس ساعي البريد لحد ما يوصلها. أنت بتحط رسالتك في صندوق البريد وبتروح تكمل يومك. صندوق البريد هذا هو “قائمة الانتظار”. بعدين بيجي ساعي البريد (عامل المعالجة أو الـ Worker)، بياخذ الرسائل من الصندوق حبة حبة، وبيوصلها لوجهتها.

في عالم البرمجة، المنظومة بتتكون من ثلاث أجزاء رئيسية:

  • المنتِج (Producer): هو الجزء من تطبيقك اللي بيخلق المهمة. في قصتنا، هو الكود اللي بيستقبل الفيديو من المستخدم وبيقول “يا جماعة، في فيديو جديد محتاج معالجة”.
  • قائمة الانتظار (The Queue): هي الوسيط، الـ “صندوق بريد” اللي بيحتفظ بالمهام (الرسائل) بشكل آمن ومُرتب لحد ما يجي حدا يعالجها.
  • المستهلِك (Consumer): هو العامل الكادح، برنامج منفصل أو جزء من نظامك، وظيفته الوحيدة إنه يسحب المهام من قائمة الانتظار وينفذها واحدة تلو الأخرى.

الفكرة الجوهرية هنا هي “الفصل” (Decoupling). المنتِج ما بيعرف أي إشي عن المستهلك، والمستهلك ما بيعرف أي إشي عن المنتِج. كل اللي بيعرفوه هو قائمة الانتظار اللي بينهم. هذا الفصل بيعطينا قوة ومرونة خرافية.

الحياة قبل وبعد قوائم الانتظار: مقارنة بسيطة

عشان الصورة توضح أكثر، خلينا نقارن السيناريوهين.

السيناريو الأول: الجحيم المتزامن (Synchronous Hell)

لما المستخدم يضغط زر “رفع الفيديو”:

  1. الخادم يستقبل طلب الـ HTTP ومعه ملف الفيديو.
  2. الخادم يبدأ فوراً بمعالجة الفيديو (ضغط، تحويل صيغة، إضافة علامة مائية…). هذه العملية ممكن تاخذ من 30 ثانية إلى عدة دقائق.
  3. طوال هذه الفترة، اتصال المستخدم مع الخادم بضل مفتوح ومعلّق. المستخدم شايف شاشة “جاري التحميل…” لا بتخلص.
  4. لو 100 مستخدم عملوا نفس الإشي بنفس الوقت، الخادم لازم يتعامل مع 100 عملية معالجة فيديو ثقيلة بنفس اللحظة. النتيجة؟ استهلاك كامل للموارد وانهيار شبه حتمي.
  5. لو المستخدم أغلق التطبيق أو النت فصل عنده، العملية كلها بتفشل وممكن تترك وراها ملفات مؤقتة ومشاكل.

هذا الأسلوب يشبه مطعم فيه نادل واحد، وهو نفسه الطباخ، وهو نفسه اللي بيحاسب الزباين. مستحيل يخدم أكثر من زبون واحد بكفاءة.

السيناريو الثاني: النعيم غير المتزامن (Asynchronous Heaven)

الآن، مع استخدام Message Queue:

  1. المستخدم يضغط زر “رفع الفيديو”.
  2. الخادم يستقبل طلب الـ HTTP ومعه ملف الفيديو. بيخزن الفيديو في مكان مؤقت (مثل Amazon S3).
  3. الخادم فوراً بيخلق “رسالة” صغيرة، ممكن تكون بصيغة JSON، فيها معلومات مثل: {"videoId": "xyz123", "userId": "456"}.
  4. الخادم يضع هذه الرسالة في قائمة الانتظار (مثلاً RabbitMQ أو AWS SQS). هذه العملية سريعة جداً، بتاخذ أجزاء من الثانية.
  5. الخادم يرجع جواب فوري للمستخدم: “تم استلام الفيديو بنجاح! سنقوم بمعالجته وإعلامك عند الانتهاء”. المستخدم الآن حر يكمل تصفح التطبيق.
  6. في مكان آخر، عندنا مجموعة “مستهلكين” (Consumers/Workers) بيراقبوا قائمة الانتظار.
  7. أحد المستهلكين بيشوف الرسالة الجديدة، بيسحبها، بيقرأ الـ videoId، بيجيب الفيديو من S3، وبيبدأ عملية المعالجة الثقيلة.
  8. لما يخلص، بيحدّث حالة الفيديو في قاعدة البيانات، وممكن يبعت إشعار للمستخدم.

شايفين الفرق؟ الخادم الرئيسي (Web Server) صار سريع وخفيف، وظيفته بس استقبال الطلبات وتوزيع المهام. الشغل الثقيل كله صار يتم “في الخلفية” (In the background) وبشكل منظم.

مثال عملي: من الكود إلى الواقع مع RabbitMQ و Node.js

خلينا نأخذ مثال بسيط عشان نثبت الفكرة. هنستخدم RabbitMQ كـ Message Broker و Node.js لكتابة الكود.

أولاً: المنتِج (Producer) – اللي بيحط الطلب في الطابور

هذا الكود ممكن يكون جزء من الـ API endpoint اللي بيستقبل الفيديو من المستخدم.


// send.js - The Producer
const amqp = require('amqplib/callback_api');

// معلومات الفيديو اللي بدنا نعالجه
const videoInfo = {
  videoId: 'video_abc_789',
  filePath: '/uploads/video_abc_789.mp4'
};
const msg = JSON.stringify(videoInfo);

amqp.connect('amqp://localhost', function(error0, connection) {
  if (error0) {
    throw error0;
  }
  connection.createChannel(function(error1, channel) {
    if (error1) {
      throw error1;
    }
    const queue = 'video_processing_queue';

    channel.assertQueue(queue, {
      durable: true // نتأكد إن الطابور ما بيضيع لو RabbitMQ عمل ريستارت
    });

    // إرسال الرسالة إلى الطابور
    channel.sendToQueue(queue, Buffer.from(msg), {
      persistent: true // نتأكد إن الرسالة نفسها ما بتضيع
    });

    console.log(" [x] أرسلنا المهمة: %s", msg);
  });

  setTimeout(function() {
    connection.close();
    process.exit(0);
  }, 500);
});

ثانياً: المستهلِك (Consumer) – العامل اللي بينفذ الطلب

هذا الكود بيكون شغال على خادم أو عدة خوادم منفصلة، ومستعد يستقبل المهام.


// receive.js - The Consumer
const amqp = require('amqplib/callback_api');

amqp.connect('amqp://localhost', function(error0, connection) {
  if (error0) {
    throw error0;
  }
  connection.createChannel(function(error1, channel) {
    if (error1) {
      throw error1;
    }
    const queue = 'video_processing_queue';

    channel.assertQueue(queue, {
      durable: true
    });
    
    // prefetch(1) بتخبر RabbitMQ ما يعطينا أكثر من مهمة واحدة بنفس الوقت
    // هذا بيضمن إنه لو الـ worker مات، المهمة اللي كان شغال عليها بترجع للطابور
    channel.prefetch(1);

    console.log(" [*] في انتظار مهام جديدة في طابور %s. للخروج اضغط CTRL+C", queue);

    channel.consume(queue, function(msg) {
      const videoInfo = JSON.parse(msg.content.toString());
      console.log(" [.] استلمنا مهمة جديدة: ", videoInfo);

      // هنا يتم استدعاء دالة معالجة الفيديو الحقيقية
      // سنقوم بمحاكاة عملية طويلة
      console.log(" [!] جاري معالجة الفيديو: %s", videoInfo.videoId);
      setTimeout(function() {
        console.log(" [✔] تم الانتهاء من معالجة الفيديو: %s", videoInfo.videoId);
        
        // هذه أهم خطوة: نخبر الطابور أننا انتهينا من معالجة الرسالة بنجاح
        // لو نسينا هاي الخطوة، الطابور رح يعتبر إنه المهمة لسا ما خلصت
        channel.ack(msg);
      }, 10 * 1000); // محاكاة 10 ثواني من العمل
    }, {
      noAck: false // مهم جداً: نطلب تأكيد يدوي
    });
  });
});

بهذه الطريقة، لو عندك 1000 فيديو لازم يتعالجوا، رح يصطفوا في الطابور بشكل منظم، والـ workers رح يسحبوهم ويعالجوهم واحد ورا الثاني. بدك تسرّع العملية؟ بسيطة، شغل كمان workers. النظام صار مرن وقابل للتوسع (Scalable).

نصائح من العبد لله (أبو عمر)

بعد سنين من الشغل مع هاي الأنظمة، تعلمت كم شغلة بحب أشارككم فيها:

  • ابدأ بسيط: مش ضروري تبني منظومة RabbitMQ كاملة من أول يوم. لو بتشتغل على بيئة سحابية زي AWS أو Google Cloud، استخدم خدماتهم المدارة مثل SQS أو Pub/Sub. سهلة جداً وبتوفر عليك وجع الراس في البداية.
  • فكر في “المراسلة المتينة” (Idempotency): شو بيصير لو العامل عالج نفس الرسالة مرتين بالخطأ (مثلاً، عالجها بس مات قبل ما يبعت تأكيد)؟ لازم تصميم الكود تبعك بحيث لو نفس العملية تنفذت مرتين، النتيجة النهائية تكون صحيحة. مثلاً، قبل ما تبدأ معالجة الفيديو، اعمل تشييك “هل هذا الفيديو تمت معالجته من قبل؟”.
  • المراقبة والإنذار (Monitoring and Alerting): لازم تعرف دايماً كم رسالة في الطابور. لو عدد الرسائل بيزيد بشكل مستمر، هذا معناه إنه المنتجين أسرع من المستهلكين، ولازم تضيف كمان workers. حط تنبيه يوصلك لو عدد الرسائل تعدى حد معين.
  • تعامل مع الفشل (Handling Failures): شو بيصير لو العامل حاول يعالج رسالة وفشل (مثلاً، الفيديو كان تالف)؟ لازم يكون عندك سياسة إعادة محاولة (Retry Policy)، ممكن تحاول 3 مرات مع فترة انتظار بينهم. بعد 3 محاولات فاشلة، لازم الرسالة تروح على طابور خاص اسمه “طابور الرسائل الميتة” (Dead-Letter Queue – DLQ) عشان تقدر تحللها بعدين يدوياً، وما تضل عالقة في الطابور الرئيسي وتعيق باقي الشغل.

الخلاصة: من طابور الطلبات إلى خط أنابيب فعال 😉

الانتقال من المعالجة المتزامنة إلى المعالجة غير المتزامنة باستخدام قوائم انتظار الرسائل كان نقلة نوعية في طريقة تفكيرنا وتصميمنا للأنظمة. هو مش مجرد حل لمشكلة ضغط، هو تغيير في العقلية نحو بناء أنظمة مرنة، قوية، وقابلة للتوسع.

الفوائد اللي حصلنا عليها كانت هائلة:

  • تجربة مستخدم أفضل: التطبيق صار يستجيب فوراً، والمستخدم ما عاد يشوف شاشات تحميل طويلة.
  • موثوقية أعلى: لو أحد عمال المعالجة (Consumers) فشل أو توقف، الرسالة ما بتضيع. بترجع للطابور وبعالجها عامل ثاني.
  • قابلية توسع أفقية: لما زاد الضغط، كل اللي عملناه هو إضافة المزيد من الـ Consumers. ما احتجنا نكبّر الخادم الرئيسي.
  • صيانة أسهل: صار بإمكاننا نوقف الـ Consumers عشان نحدّث الكود تبعهم بدون ما نأثر على استقبال الطلبات الجديدة، لأنها بتتخزن في الطابور.

نصيحتي الأخيرة لك يا صديقي المبرمج: المرة الجاية اللي بتلاقي حالك بتكتب كود لعملية بتاخذ أكثر من نصف ثانية داخل طلب HTTP، وقف وفكر. هل بقدر أحولها لعملية غير متزامنة؟ في أغلب الأحيان، الجواب رح يكون نعم، وقوائم انتظار الرسائل هي صديقك الوفي في هذه الرحلة.

ما تخاف من الأحمال الكبيرة، خطط لها صح، وخلي طوابيرك دايماً منظمة. بالتوفيق!

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

التكنلوجيا المالية Fintech

كانت بيانات بطاقات عملائنا قنبلة موقوتة: كيف أنقذنا ‘الترميز’ (Tokenization) من جحيم تخزين البيانات الحساسة؟

أشارككم قصة حقيقية من قلب المعركة التقنية، عندما كانت بيانات بطاقات عملائنا قنبلة موقوتة تهدد مستقبل شركتنا. اكتشفوا كيف كانت تقنية "الترميز" (Tokenization) طوق النجاة...

16 مايو، 2026 قراءة المزيد
البنية التحتية وإدارة السيرفرات

من الخوادم “الثلجية” إلى الكود: كيف أنقذتنا Terraform من جحيم الانحراف التكويني

أشارككم قصة حقيقية عن معاناة فريقنا مع الخوادم الفريدة التي لا تتكرر، وكيف كانت "البنية التحتية كود" (IaC) باستخدام Terraform هي طوق النجاة الذي أخرجنا...

16 مايو، 2026 قراءة المزيد
ادارة الفرق والتنمية البشرية

المسار المهني المزدوج: كيف أنقذنا أفضل مبرمجينا من “الترقية إلى عدم الكفاءة”؟

كان أفضل مبرمجينا يرحلون أو يتحولون إلى مديرين سيئين. في هذه المقالة، أسرد لكم قصة كيف أنقذنا "المسار المهني المزدوج" من هذا الجحيم، وكيف يمكن...

16 مايو، 2026 قراءة المزيد
اختبارات الاداء والجودة

كانت اختباراتنا 100% ناجحة لكن الكود مليء بالثغرات: كيف أنقذنا “الاختبار الطفري” (Mutation Testing) من جحيم التغطية الزائفة؟

أشارككم قصة حقيقية من ميدان البرمجة، حيث كانت نسبة تغطية الاختبارات (Code Coverage) تخدعنا وتوهمنا بالجودة، إلى أن اكتشفنا "الاختبار الطفري" (Mutation Testing). هذه التقنية...

16 مايو، 2026 قراءة المزيد
أتمتة العمليات

من النشر اليدوي إلى التسليم المستمر: دليلك العملي لبناء أول خط أنابيب CI/CD باستخدام GitHub Actions

مقالة عملية من أبو عمر، مطور فلسطيني، تشرح بالتفصيل كيفية الانتقال من النشر اليدوي المجهد إلى الأتمتة الكاملة باستخدام خطوط أنابيب CI/CD على GitHub Actions....

16 مايو، 2026 قراءة المزيد
نصائح برمجية

كانت بياناتنا مجرد أرقام ونصوص: كيف أنقذتنا الكائنات القيمية (Value Objects) من جحيم الأخطاء الصامتة؟

أشارككم قصة من قلب المعركة البرمجية، كيف كاد خطأ بسيط بسبب البيانات العشوائية أن يكلفنا الكثير. سنتعلم معًا كيف تنقذنا "الكائنات القيمية" (Value Objects) من...

16 مايو، 2026 قراءة المزيد
البودكاست