أنظمة بحجم المنتجات
صمّم نظام محادثة
تطبيق المحادثة يبدو تافهاً حتى تحاول بناءه: يجب أن تعبر الرسالة الإنترنت في جزء من الثانية، وأن تصل إلى كل أجهزة المستخدم، وبالترتيب الصحيح — ملايين المرات يومياً. ضيّق النطاق: محادثات فردية ومجموعات صغيرة، نصوص فقط، بلا مكالمات ولا قصص. حتى هذا النطاق الضيق يجبرك على حل الاتصالات والحضور (presence) والترتيب والمزامنة دون اتصال. أربع مشكلات حقيقية، درس واحد.
التصميم الساذج: كل عميل يسأل «هل من جديد لي؟» كل بضع ثوانٍ. احسب الأرقام: مليون مستخدم متصل يسألون كل 5 ثوانٍ يعني 200,000 طلب في الثانية — والغالبية الساحقة تعود فارغة، لأن الناس يستقبلون رسالة كل بضع دقائق، لا كل بضع ثوانٍ. أنت تحرق معظم قدرة خوادمك للإجابة بـ«لا». والأسوأ أن زمن الاستجابة مدمج في التصميم: الرسالة تنتظر حتى نصف فترة الاستطلاع قبل أن يراها أحد. الـpolling نموذج أولي مقبول ومنتج فظيع.
الـlong polling هو حجر العبور: العميل يسأل، والخادم يبقي الطلب مفتوحاً حتى تصل رسالة أو ينتهي المهلة، ثم يعيد العميل السؤال فوراً. الردود الفارغة تختفي، لكنك ما زلت تدفع تكلفة إعداد HTTP كاملة لكل رسالة، وتحت الحمل تحمل الخوادم آلاف الطلبات المعلّقة. أما WebSocket فهو الجواب الحقيقي: ترقية اتصال HTTP مرة واحدة، ثم الإبقاء على قناة دائمة ثنائية الاتجاه لكل مستخدم. يستطيع الخادم الآن دفع الرسالة لحظة وجودها — بلا سؤال ولا انتظار. اتصال واحد لكل مستخدم، يبقى مفتوحاً لساعات.
WebSocket يحل التوصيل ويخلق مشكلة جديدة: خوادم المحادثة ذات حالة (stateful). الجهاز الواحد يستطيع إبقاء نحو 50–100 ألف اتصال مفتوح، لذا فالخدمة الكبيرة أسطول من خوادم المحادثة، كل منها يملك مجموعة ثابتة من الاتصالات. تحتاج خريطة اتصالات — أي مستخدم يقيم على أي خادم — وتُحفظ عادة في مخزن سريع مثل Redis. إعادة التوازن وانهيار الخوادم والنشر تعني الآن إعادة توصيل آلاف العملاء دون تدافع مدمر. إدارة الاتصالات ليست تفصيلاً؛ إنها نظام فرعي كامل.
خدمة الحضور تجيب سؤالاً واحداً: من متصل؟ الآلية هي heartbeat — كل عميل يرسل نبضة لخدمة الحضور كل بضع ثوانٍ، ومن تنقطع نبضاته نحو 30 ثانية يُعلَّم غير متصل. تغيّرات الحالة تُبث (fanout) لأصدقاء المستخدم أو أعضاء مجموعته فقط، وليس للجميع أبداً. والآن الجزء الصادق: الحضور متسق في النهاية (eventually consistent) بالتصميم. إن ضاعت نبضة، يبدو الصديق غير متصل لنصف دقيقة وهو متصل فعلاً. لا أحد يتضرر، فتقبل الخطأ وتتجنب التنسيق المكلف الذي كان سيجعل الحضور دقيقاً تماماً.
- عميل المُرسِل يدفع الرسالة عبر WebSocket إلى خادم المحادثة، الذي يمنحها معرّفاً ورقم تسلسل خاصاً بالقناة.
- خادم المحادثة يضع الرسالة في طابور رسائل — أصبحت الكتابة دائمة ويستطيع الخادم تأكيد الاستلام للمُرسِل دون انتظار التسليم.
- عمال يستهلكون الطابور، يخزّنون الرسالة في مخزن key-value مفهرس بمعرّف القناة، ويبحثون عن خادم المحادثة لكل مستلِم لدفعها عبر اتصالاته — أو عبر إشعار push إن كان غير متصل.
لماذا رقم تسلسل لكل قناة وليس ترتيباً عاماً واحداً؟ المُسلسِل العام (global sequencer) عملية واحدة يجب أن ترى كل رسالة في النظام — عنق زجاجة ونقطة فشل مفردة — ولا يشتري لك شيئاً، لأن رسائل القنوات المختلفة لا معنى لترتيبها فيما بينها أصلاً. لا أحد يهتم إن كانت رسالة في المجموعة أ «سبقت» رسالة في المجموعة ب. لكن داخل القناة الواحدة، الترتيب هو المنتج: الردود يجب أن تتبع الرسائل التي تجيب عنها. لذا تحصل كل قناة على عدّادها المتزايد الخاص، وهو رخيص التوليد ويتوسع بسهولة مع التقسيم (sharding).
رقم التسلسل نفسه يحل المزامنة بين الأجهزة. كل جهاز يتذكر مؤشراً (cursor) — أعلى رقم تسلسل رآه في كل قناة. عند إعادة الاتصال، يرسل الجهاز مؤشراته فيعيد الخادم تشغيل كل ما بعدها: يلتقط هاتفك من حيث توقف حاسوبك المحمول تماماً، بلا تكرار ولا فجوات. وإن كان المستخدم غير متصل كلياً، فلا اتصال لدفع الرسالة إليه، فيسلّم مسار الإرسال الرسالة إلى خدمة إشعارات push بدلاً من ذلك. وفي محادثات المجموعات يكون الـfanout لكل عضو: تُكتب الرسالة مرة واحدة في القناة، ثم تُدفع إلى اتصال كل عضو — ومع المجموعات الصغيرة، هذا الضرب رخيص.
سؤالان. أولاً: لماذا يُفضَّل WebSocket على الـpolling في المحادثة؟ (استطلاع بمعدل طلب كل 5 ثوانٍ لكل مستخدم يعني ~200 ألف طلب/ثانية لكل مليون مستخدم، معظمها فارغة، مع زمن استجابة مدمج؛ بينما WebSocket دائم واحد لكل مستخدم يتيح للخادم الدفع الفوري بلا هدر.) ثانياً: لماذا يُرتَّب الرسائل لكل قناة بدلاً من ترتيب عام؟ (المُسلسِل العام عنق زجاجة ونقطة فشل مفردة، والترتيب بين القنوات بلا معنى — الترتيب يحمل دلالة داخل القناة فقط، لذا تحتفظ كل قناة بعدّادها الخاص.)
نظام المحادثة أربعة قرارات متسلسلة: اقتل الـpolling باتصالات WebSocket دائمة واقبل الحالة التي تجلبها؛ اجعل الحضور رخيصاً وخاطئاً قليلاً عن قصد؛ اكتب عبر طابور إلى مخزن key-value مرتَّب بأرقام تسلسل لكل قناة؛ ودع المؤشرات نفسها تقود المزامنة بين الأجهزة، وإشعارات push لغير المتصلين، والـfanout لكل عضو في المجموعات. لا شيء من هذا غريب — الحرفة في أن تقول لماذا.
افتح تبويب الشبكة في منتج محادثة تستخدمه (WhatsApp Web أو Slack أو Messenger) وراقب ما يتدفق فعلاً. اعثر على اتصال WebSocket، واقرأ بعض الإطارات، ولاحظ رسائل الـheartbeat. ثم قدّر لفريقك: كم اتصالاً متزامناً تحتاج قاعدة مستخدميك، وكم خادم محادثة بمعدل 50 ألف اتصال لكل خادم؟ دوّن الرقمين — هذه بذرة خطة سعة حقيقية.
أنظمة بحجم المنتجات