متى الـ polling أفضل من الـ websockets
سنة من الـ trade-offs بمحاسب. ليش الجواب الواضح طلع غلط على ظروف الشبكة عندنا.
جاري تحميل المقال…
سنة من الـ trade-offs بمحاسب. ليش الجواب الواضح طلع غلط على ظروف الشبكة عندنا.
جاري تحميل المقال…
طوال عام ٢٠٢٤ كنّا متأكّدين أنّنا بحاجة إلى websockets. كان المنتج منصّة محاسبة متعدّدة المستأجرين، مع تطبيق Flutter على العميل وخلفيّة Laravel على الخادم؛ والبيانات يجب أن تبدو حيّة عبر الأجهزة، والإجابة المعتادة في الكتب هي «افتح websocket وادفع البيانات». هذا ما فعلناه. بعد ستّة أشهر، أزلناه واستبدلناه بـ polling منضبط، وتحسّن زمن الاستجابة من منظور المستخدم. هنا ما تعلّمناه، ومتى يفوز polling فعلاً.
الحجّة المؤيّدة لـ websockets معروفة، وليست خاطئة:
إن كنت تبني تطبيق دردشة، أو cursor متعدّد اللاعبين، أو شاشة تداول، فهذه الخصائص ليست قابلة للتفاوض. تحتاجها. توقّف عن القراءة.
تطبيق المحاسبة يُستخدم في محلات ومستودعات ومطاعم ومكاتب في الريف السوري والسعودي والخليج. ظروف الشبكة هناك ليست ظروف لابتوبك في مقهى. تحديداً:
نقطة الانهيار كانت يوم ثلاثاء صباحاً. كان التيّار مقطوعاً طوال الليل في مدينة لدينا فيها عدّة مئات من المستخدمين. حين عاد التيّار، عادت كلّ هذه الهواتف إلى الإنترنت في نفس الدقيقة. حاولت كلّها إعادة فتح websockets في نفس الدقيقة. خادم Reverb سقط. حصل المستخدمون على شاشة فارغة. وحصلنا نحن على مكالمة.
يمكنك هندسة الحلّ ضدّ العاصفة. backoff عشوائي، حدود معدّل على الخادم، connection pool، gateway منفصل. فعلنا كلّ ذلك. ساعد. لكنّه لم يبدُ موثوقاً.
لـ polling خاصيّة يسهل تجاهلها لأنّنا تعلّمنا أن نراه بدائيّاً: كلّ طلب مستقلّ. لا حالة مشتركة على السلك. لا سؤال «هل الاتّصال لا يزال سليماً؟» لتجيب عنه. العميل يسأل؛ الخادم يجيب؛ ننتقل.
هذا الشكل متسامح بطرق ليست في websockets:
If-Modified-Since
كلّها تصبح متاحة من جديد. websockets تتنازل عن كلّ هذه البنية.لم نغيّر الرابط فقط. طبقة polling التي حلّت محلّ websockets لها خصائص استلزمت تكرارات لضبطها:
interface PollConfig {
intervalActiveMs: 8_000; // المستخدم يتفاعل
intervalIdleMs: 45_000; // التبويب ظاهر، لا إدخال حديثاً
intervalHiddenMs: 0; // التبويب مخفيّ، لا تفعل شيئاً
jitterRatio: 0.2; // ±٢٠٪ كي لا يتزامن أسطول الأجهزة
maxConcurrent: 1; // لا تتداخل عمليّات polling
cursor: 'updated_at';
}أمور تستحقّ إخراجها:
حين يكون التبويب مخفيّاً، لا نقوم بالـ polling. حين يصبح ظاهراً، نقوم بـ poll واحد فوراً، ثمّ نستأنف الفاصل. هذا التغيير الواحد قلّص حركة الباك-إند لدينا بنسبة ٦٠٪، وحسّن زمن الاستجابة المُدرَك، لأنّ التبويب الأمامي يقوم بـ poll خلال ميلّي ثوانٍ من الظهور.
كلّ poll يرسل أعلى updated_at رآه، والخادم يعيد فقط الصفوف الأحدث
منه. الردّ شبه دائماً صغير. معظم عمليّات polling تعيد صفر صفّ.
نطوي عمليّات polling المتداخلة في واحدة. إن كان هناك poll قيد التنفيذ حين يحلّ الفاصل التالي، نتجاوز الفاصل وندَع الجاري ينتهي. هذا يبدو بديهيّاً؛ وهو مصدر نصف الأخطاء التي ستراها في حلقات polling مكتوبة على عجل.
بعد أسبوعين من polling المنضبط، صار متوسّط زمن الاستجابة المُدرَك لـ«المعاملة تظهر على الجهاز الثاني» ٤٫٦ ثانية. مع websockets كان ١١ ثانية، حين كان يعمل. البروتوكول الأسوأ على الورق فاز في المقياس الذي يهمّ.
polling ليس الجواب لكلّ شيء. الحالات التي أبقينا فيها push:
خارج هذه الحالات، الافتراض هو polling.
إن كان مستخدموك على نفس شبكة خادمك، والجلسة طويلة، وأنت تدفع لزمن استجابة منخفض على كلّ تغيير، websockets صحيح.
إن انكسر أيّ من هذه الشروط، فإنّ polling، حين يُنفَّذ بانضباط، هو الإجابة التي تنجو من الاحتكاك بالعالم الحقيقي.
هذا ليس الاستنتاج المعماري الذي يريده الكتاب. إنّه الاستنتاج الذي يصمد في الثامنة صباح ثلاثاء بعد انقطاع التيّار.
المقال التالي في هذا الخطّ يدور حول كيفيّة هيكلة الـ queue الصادر على العميل ليبقى polling-based sync صحيحاً تحت تذبذب الشبكة. إن كانت محرّكات المزامنة تهمّك، صفحة السلسلة تستكمل من حيث ينتهي هذا المقال.