“يا أبو عمر، المستخدمين بشتكوا… الموقع بطيء!”
أذكر تلك الأيام جيداً، كنا في خضم إطلاق منصة جديدة لعميل مهم. الحماس كان في أوجه، والليالي الطويلة من البرمجة والتصميم بدأت تؤتي ثمارها. أطلقنا المشروع، وفي الساعات الأولى، كانت الاحتفالات تعمّ المكتب. لكن، سرعان ما تحولت هذه الاحتفالات إلى مكالمات قلقة ورسائل بريد إلكتروني تحمل في طياتها عبارة واحدة متكررة: “الموقع بطيء جداً!”.
كان السيناريو كالتالي: عندما يقوم مستخدم جديد بالتسجيل، كان نظامنا يقوم بسلسلة من العمليات: حفظ بياناته في قاعدة البيانات، ثم إرسال بريد إلكتروني ترحيبي، ثم إنشاء ملف شخصي مبدئي له، ثم تحديث لوحة التحكم الخاصة بالإحصائيات. كل هذه العمليات كانت تحدث واحدة تلو الأخرى بينما المستخدم يحدّق في شاشة تحميل تدور وتدور… “الرجاء الانتظار”.
شعرت بالإحباط الشديد. يا زلمة، كل هذا التعب والجهد، وآخرتها المستخدم يهرب من أول تجربة؟ عقدنا اجتماعاً طارئاً. كان الجو متوتراً، والجميع يلقي باللوم على جزء مختلف من النظام. في خضم النقاش، صرخت قائلاً: “يا جماعة، ليش لازم المستخدم يستنى كل هاد؟ إحنا بس محتاجين نتأكد إنه سجّل بنجاح، والباقي ممكن نعمله بالخلفية!”. كانت تلك هي اللحظة التي أضاءت فيها لمبة فوق رؤوسنا جميعاً. كانت تلك هي اللحظة التي قررنا فيها أن ننتقل من عالم “الاتصال المتزامن” الخانق إلى رحابة “طوابير الرسائل” اللامتزامنة.
ما هي طوابير الرسائل (Message Queues)؟ وليش بنحتاجها أصلاً؟
قبل أن نغوص في التفاصيل التقنية، دعني أشرح لك الفكرة ببساطة من خلال مثال من حياتنا اليومية. تخيل أنك تريد إرسال طرد بريدي (رسالة) إلى صديق لك. لديك خياران:
- الطريقة المتزامنة (Synchronous): أن تأخذ الطرد بنفسك، تسافر إلى مدينة صديقك، تطرق بابه، وتسلمه الطرد يداً بيد، ثم تعود إلى منزلك. خلال كل هذا الوقت، أنت “محجوز” ولا يمكنك فعل أي شيء آخر. هذا بالضبط ما كان يفعله نظامنا.
- الطريقة اللامتزامنة (Asynchronous): أن تذهب إلى أقرب مكتب بريد (وهو الوسيط أو الـ Broker)، تضع الطرد في الصندوق المخصص (وهو الطابور أو الـ Queue)، وتعود مباشرة لإكمال أعمالك. لاحقاً، سيأتي عامل البريد (المستهلِك أو الـ Consumer) ليأخذ الطرد ويوصله إلى صديقك. أنت لم تنتظر، والعملية تمت بنجاح.
طوابير الرسائل هي ببساطة “مكتب البريد الرقمي” لتطبيقاتنا. هي برامج وسيطة (مثل RabbitMQ, Kafka, SQS) تسمح لأجزاء مختلفة من نظامك (خدمات مصغرة) بالتواصل مع بعضها البعض بشكل غير مباشر وغير متزامن. الخدمة الأولى (المنتِج) تضع “رسالة” في الطابور وتكمل عملها، والخدمة الثانية (المستهلِك) تلتقط هذه الرسالة من الطابور في وقت لاحق لتنفيذ المهمة المطلوبة.
كيف أنقذتنا طوابير الرسائل: من النظرية للتطبيق
لنعد إلى مشكلتنا الأصلية ونرى كيف تغير الوضع بعد تطبيق هذا المفهوم.
السيناريو الكابوسي: الاتصال المتزامن (Synchronous Communication)
كانت بنيتنا القديمة تبدو هكذا:
طلب تسجيل المستخدم ➔ خدمة المستخدمين (حفظ في DB) ➔ [انتظار...] ➔ خدمة الإشعارات (إرسال إيميل) ➔ [انتظار...] ➔ خدمة الملفات الشخصية (إنشاء ملف) ➔ [انتظار...] ➔ إرجاع استجابة للمستخدم
المشاكل كانت واضحة:
- الاقتران الشديد (Tight Coupling): كل خدمة تعتمد بشكل مباشر على الأخرى. لو تعطلت خدمة الإشعارات، عملية التسجيل بأكملها تفشل.
- تجربة مستخدم سيئة: المستخدم ينتظر اكتمال كل هذه العمليات التي لا تعنيه بشكل مباشر في تلك اللحظة.
- صعوبة التوسع (Scalability): إذا زاد الضغط على إرسال الإيميلات، سيبطئ ذلك كل عمليات التسجيل في النظام.
السيناريو المنقذ: الاتصال اللامتزامن (Asynchronous Communication)
بعد إدخال طابور الرسائل (اخترنا RabbitMQ وقتها)، أصبحت البنية الجديدة كالتالي:
طلب تسجيل المستخدم ➔ خدمة المستخدمين (حفظ في DB + إرسال رسالة "UserRegistered" إلى الطابور) ➔ إرجاع استجابة فورية للمستخدم!
في الخلفية، وبشكل مستقل تماماً، كانت هناك خدمات أخرى “تستمع” إلى هذا الطابور:
- خدمة الإشعارات (Consumer): ترى رسالة “UserRegistered”، تلتقطها، وترسل الإيميل الترحيبي.
- خدمة الملفات الشخصية (Consumer): ترى نفس الرسالة، تلتقطها، وتنشئ الملف الشخصي.
- خدمة الإحصائيات (Consumer): ترى الرسالة أيضاً وتحدّث بياناتها.
النتائج كانت مذهلة: زمن استجابة عملية التسجيل انخفض من 5-10 ثوانٍ إلى أقل من 200 ميلي ثانية! اختفت شكاوى البطء، وتحسنت موثوقية النظام بشكل كبير.
نصيحة من أبو عمر: مبدأ “افصل اهتماماتك” (Separation of Concerns) لا ينطبق فقط على الكود داخل الملف الواحد، بل هو حجر الأساس في تصميم الأنظمة الكبيرة. طوابير الرسائل هي أداة ممتازة لتحقيق هذا الفصل على مستوى البنية التحتية.
خلينا نشوف الكود: مثال عملي مع RabbitMQ و Python
الكلام النظري جميل، لكن دعنا نرى كيف يبدو هذا على أرض الواقع. سنستخدم مكتبة pika في بايثون للتواصل مع RabbitMQ.
المكونات المطلوبة
ستحتاج إلى تثبيت RabbitMQ (أسهل طريقة هي عبر Docker) ومكتبة pika في بايثون:
pip install pika
المنتِج (Producer): الذي يرسل رسالة التسجيل
هذا هو الكود الذي يمكن أن يكون في “خدمة المستخدمين” بعد نجاح عملية الحفظ في قاعدة البيانات.
# producer.py
import pika
import json
# الاتصال بـ RabbitMQ
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
# نتأكد من وجود الطابور، إذا لم يكن موجوداً سيتم إنشاؤه
channel.queue_declare(queue='user_registration_queue', durable=True)
# الرسالة التي نريد إرسالها (عادة تكون بصيغة JSON)
message_body = {
'user_id': 123,
'email': 'user@example.com',
'name': 'أحمد'
}
# إرسال الرسالة إلى الطابور
channel.basic_publish(
exchange='',
routing_key='user_registration_queue',
body=json.dumps(message_body),
properties=pika.BasicProperties(
delivery_mode=2, # اجعل الرسالة دائمة (persistent)
))
print(" [x] تم إرسال رسالة التسجيل!")
connection.close()
المستهلِك (Consumer): خدمة إرسال الإيميلات
هذا الكود يمثل خدمة منفصلة تعمل في الخلفية، تنتظر الرسائل وتنفذ المهام.
# consumer_email_service.py
import pika
import time
import json
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='user_registration_queue', durable=True)
print(' [*] في انتظار الرسائل. للخروج اضغط CTRL+C')
def callback(ch, method, properties, body):
message = json.loads(body)
user_email = message.get('email')
user_name = message.get('name')
print(f" [.] استقبلت رسالة للمستخدم: {user_name}")
# هنا تضع كود إرسال الإيميل الحقيقي
print(f" [.] جاري إرسال إيميل ترحيبي إلى {user_email}...")
time.sleep(5) # محاكاة لعملية إرسال إيميل قد تستغرق وقتاً
print(" [x] تم إرسال الإيميل بنجاح!")
# إعلام الطابور بأن الرسالة تمت معالجتها بنجاح
ch.basic_ack(delivery_tag=method.delivery_tag)
# تحديد أن المستهلك لا يستقبل أكثر من رسالة واحدة في كل مرة قبل أن ينتهي من الحالية
channel.basic_qos(prefetch_count=1)
channel.basic_consume(queue='user_registration_queue', on_message_callback=callback)
channel.start_consuming()
لاحظ أن الـ producer.py ينتهي فوراً، بينما الـ consumer_email_service.py يأخذ وقته (5 ثوانٍ) لمعالجة الرسالة. هذا هو جوهر اللامزامنة! يمكنك تشغيل عدة نسخ من الـ consumer لتوزيع الحمل ومعالجة الرسائل بشكل أسرع.
متى يجب أن أستخدم طوابير الرسائل؟ (ومتى لا يجب)
رغم قوتها، طوابير الرسائل ليست الحل لكل المشاكل. من المهم أن تعرف متى تستخدمها.
حالات الاستخدام المثالية
- المهام طويلة الأمد: معالجة الفيديوهات، توليد التقارير الضخمة، تحليل البيانات. أي شيء يأخذ أكثر من ثانية أو ثانيتين هو مرشح جيد.
- التواصل بين الخدمات المصغرة (Microservices): هي الطريقة الأمثل لجعل خدماتك مستقلة وقادرة على التطور والتوسع بشكل منفصل.
- توزيع الأحمال (Load Leveling): إذا كان تطبيقك يتعرض لقفزات مفاجئة في الاستخدام (مثلاً، عند إرسال حملة إعلانية)، يمكن للطابور أن “يمتص” هذه القفزة ويسمح لخدماتك بمعالجة الطلبات بهدوء وبوتيرة ثابتة.
- زيادة الموثوقية: إذا تعطلت خدمة المستهلِك، تبقى الرسائل محفوظة بأمان في الطابور (إذا تم ضبطها كـ durable) حتى تعود الخدمة للعمل مرة أخرى.
متى يجب أن تفكر مرتين؟
ببساطة، عندما تحتاج إلى استجابة فورية تعتمد على نتيجة العملية. مثلاً، عند التحقق من توفر اسم مستخدم أثناء التسجيل، لا يمكنك وضع هذه المهمة في طابور وإخبار المستخدم “سوف نعلمك لاحقاً إذا كان الاسم متاحاً!”. هذه العملية بطبيعتها متزامنة وتتطلب اتصالاً مباشراً.
الخلاصة: لا تترك خدماتك تنتظر في الطابور! 🚀
العودة إلى قصتنا، كان قرار استخدام طوابير الرسائل نقطة تحول حقيقية في مشروعنا وفي طريقة تفكيرنا كفريق. تعلمنا درساً قيّماً: سرعة الاستجابة للمستخدم هي الأولوية القصوى، وأي عملية يمكن تأجيلها أو تنفيذها في الخلفية، يجب أن يتم ذلك.
طوابير الرسائل ليست مجرد أداة تقنية، بل هي عقلية تصميمية كاملة ترتكز على فصل المهام، زيادة الموثوقية، وبناء أنظمة مرنة وقابلة للتوسع قادرة على تحمل ضغط العمل اليوم وفي المستقبل.
نصيحة أخيرة من أبو عمر: في المرة القادمة التي تبرمج فيها ميزة جديدة، اسأل نفسك هذا السؤال البسيط: “هل المستخدم بحاجة ماسة لانتظار نتيجة هذه العملية الآن؟”. إذا كان الجواب “لا”، فأنت تعرف الآن أين تضعها… في طابور الرسائل. 😉