عدد المشاهدات هو أول رقم يثق به الزائر. فهو يخبرهم ما إذا كان الفيديو يستحق ثلاثين ثانية أو ثلاثين دقيقة من وقتهم. في TopVideoHub، يتحرك هذا الرقم بسرعة. يمكن لمقطع رائج أن يحصد 40,000 مشاهدة في عشر دقائق. إذا تجمد العداد على الصفحة، يشعر المستخدمون بالفراغ، فيغادرون الموقع.
قد يبدو إيصال هذا الرقم إلى المتصفح أمراً تافهاً، لكنه ليس كذلك. الحل الأول الذي تلجأ إليه معظم الفرق هو الـ polling. من السهل ربطه ويعمل بشكل جيد في بيئة الاختبار (staging)، لكن بيئة الاختبار تخدع.
عندما يتحول الـ Polling إلى هجوم DDoS ضد نفسك
قام فريق TopVideoHub ببناء poller بسيط بلغة JavaScript. كان يقوم بجلب أحدث عدد للمشاهدات كل خمس ثوانٍ. في بيئة اختبار مع ثلاثة متصفحات مفتوحة، بدا الأمر رائعاً. أما في بيئة الإنتاج (production)، فقد تسبب في انهيار المنصة.
ثمانية آلاف مشاهد متزامن يقومون بالتحديث كل خمس ثوانٍ ولّدوا 1,600 طلب في الثانية. كل طلب كان يضغط على قاعدة البيانات. ارتفع تأخر النسخ المتماثل (replication lag)، وعانت النسخ الاحتياطية للقراءة (read replicas)، وتم تجاوز طبقات التخزين المؤقت (cache layers). لم يكن الفريق يقدم فيديوهات، بل كان يقدم حملاً ناتجاً عن فعلهم.
الـ polling بريء حتى يثبت العكس. بالنسبة للوحات التحكم ذات الحركة المنخفضة أو لوحات الإدارة، فهو أمر جيد. أما بالنسبة لصفحة فيديو رائج، فهو قنبلة موقوتة. احتاج الفريق إلى قناة مستمرة من الخادم إلى المتصفح، لكنهم لم يحتاجوا إلى تعقيد بروتوكول full-duplex.
لماذا يناسب SSE القنوات أحادية الاتجاه
تم تصميم Server-Sent Events (SSE) خصيصاً لهذا النوع من المشكلات: الخادم لديه البيانات، والمتصفح يحتاج فقط إلى الاستماع.
على عكس WebSockets، يعتمد SSE على HTTP العادي. وهذا الأمر أهم مما يبدو. لن تحتاج إلى قواعد proxy جديدة، أو ترقيات للرؤوس (upgrade headers)، أو تعقيدات في موازن التحميل (load-balancer). إذا كان خادمك يتحدث HTTP/1.1 أو HTTP/2، فإن SSE سيعمل. تصحيح الأخطاء (debugging) سهل لأن التدفق عبارة عن نص فقط. يمكنك توجيه curl إلى نقطة النهاية (endpoint) ومشاهدة الأرقام وهي تتحرك في الوقت الفعلي، وهو ما يتفوق على التخمين حول سبب تعطل إطار socket ثنائي.
يتولى المتصفح الأجزاء المملة مجاناً. إذا انقطع الاتصال، يقوم SSE بإعادة الاتصال تلقائياً باستخدام رأس Last-Event-ID حتى يعرف الخادم من أين يستأنف. واجهة برمجة التطبيقات (API) في JavaScript صغيرة جداً: أنشئ EventSource ، وأضف معالج onmessage ، وقد انتهيت.
التخزين المؤقت (Caching) هو البنية التحتية الحقيقية
أكبر خطأ معماري في العدادات المباشرة هو التعامل مع كل اتصال متصفح كسبب للاستعلام من قاعدة البيانات. إذا كان 8,000 شخص يشاهدون نفس الفيديو، فإن تشغيل 8,000 استعلام كل ثانيتين هو جنون. لن تصمد قاعدة بياناتك أمام حركة المرور الهائلة (viral traffic).
حل TopVideoHub هذه المشكلة باستخدام APCu، وهو نظام التخزين المؤقت لـ opcode والمستخدم في ذاكرة PHP. التدفق بسيط: تقوم عملية خلفية (background process) — أو نقطة نهاية خفيفة يتم استدعاؤها عبر مؤقت — بكتابة عدد المشاهدات الحالي في APCu مرة كل ثانيتين. أما نقطة نهاية SSE، التي قد يظل آلاف المتصفحات فاتحين لها، فتقرأ حصرياً من APCu.
النتيجة: تتلقى قاعدة البيانات طلباً واحداً كل ثانيتين، بغض النظر عن عدد المشاهدين. يصبح التخزين المؤقت هو ممتص الصدمات. APCu ليس شيئاً غريباً؛ فهو يأتي مع PHP، ويعيش في الذاكرة المشتركة، ويقرأ أسرع من أي رحلة ذهاب وإياب عبر الشبكة. بالنسبة لرقم واحد يتغير بشكل متكرر ولكن ليس لحظياً، فهو الأداة المناسبة.
إذا لم تكن تستخدم APCu، فإن Redis أو Memcached سيعملان أيضاً. المبدأ يظل كما هو: افصل مسار القراءة السريع عن قاعدة البيانات.
إقناع PHP و LiteSpeed و Cloudflare بالبث (Streaming)
يريد PHP أن ينتهي ويذهب إلى منزله. وتريد خوادم الويب تخزين المخرجات مؤقتاً (buffering) وتقديم استجابة مرتبة. يحتاج SSE إلى العكس تماماً: اتصال يظل مفتوحاً، ويقوم بتفريغ البايتات فور وصولها. بدون عناية، ستصل "البث" الخاص بك ككتلة واحدة بعد ثلاثين ثانية، مما يفقد الأمر غرضه.
إليك كيف حافظ TopVideoHub على تدفق القناة دون انسداد.
أوقف تخزين المخرجات مؤقتاً (Output buffering). في بداية سكربت SSE، قم بتعطيل كل طبقة من طبقات التخزين المؤقت التي قد يكون PHP قد قام بتفعيلها. استدعِ ob_end_flush() إذا كان هناك مخزن مؤقت نشط، وأوقف التفريغ الضمني باستخدام ob_implicit_flush(true) بعد إرسال الرؤوس (headers).
أخبر البروكسيات بالتراجع. أرسل رأس X-Accel-Buffering: no. يستجيب Nginx لهذا الأمر، وكذلك LiteSpeed. إنه يشير إلى أنه لا ينبغي تخزين الاستجابة مؤقتاً أو ضغطها في كتلة قابلة للتخزين.
حدد عمراً قصيراً للاتصال. كل اتصال SSE يشغل عامل PHP (PHP worker). يضع TopVideoHub حداً أقصى للبث عند 55 ثانية. عندما ينتهي المؤقت، يرسل الخادم تعليقاً نهائياً، ويغلق البث، ويعيد المتصفح الاتصال تلقائياً. هذا الاتصال الجديد يقع على عامل جديد، مما يمنع أي عملية واحدة من الاستحواذ على الموارد للأبد.
أرسل Ping للبقاء متصلاً. أرسل سطر تعليق — مثل : ping — كل عشرين ثانية. يتجاهل معالج الرسائل في المتصفح التعليقات في SSE، لكنها تحافظ على دفء اتصال TCP. غالبًا ما تقوم موازنات الحمل (Load balancers) وشبكات توصيل المحتوى (CDNs) بقطع الاتصالات الصامتة بعد ثلاثين أو ستين ثانية. سطر جديد بسيط قد ينقذك من هذا المصير.
احترم علامة تبويب المستخدم. عندما يقوم الزائر بتصغير علامة التبويب أو إخفائها، انسحب. استمع لحدث visibilitychange في المتصفح واستدعِ eventSource.close(). يجب على الخادم أيضًا اكتشاف انقطاع اتصال العميل وإنهاء الحلقة (loop). يمكن لـ PHP التحقق من connection_aborted() داخل الحلقة. لا تسمح للاتصالات الشبحية باستهلاك العمال (workers) لأشخاص غادروا منذ عشر دقائق.
الحد الأقصى الصارم: عمال PHP
تتميز SSE في PHP بالوضوح بشأن حدودها. كل اتصال SSE مفتوح يستهلك عامل PHP واحدًا. إذا كان لديك مئة عامل في المجموعة، فلديك مئة تدفق (stream). نقطة انتهى. لا يوجد حل بديل غير متزامن (async) طالما أنك تعمل ضمن نموذج عمليات Apache أو PHP-FPM. يمكنك ضبط pm.max_children ، ولكن الذاكرة والمعالج (CPU) هما من يحددان الحدود الحقيقية.
هذا الحد يظهر أثره بسرعة إذا كنت تستخدم العمال أيضًا لتحميل الصفحات العادية، واستدعاءات API، وتوليد الأصول (assets). راقب تشبع العمال بعناية. إذا بدأ نقطة نهاية SSE في وضع الطلبات في قائمة الانتظار لأن جميع العمال محاصرون في تدفقات تستمر لعشرين دقيقة، فسيتباطأ موقعك بالكامل.
عندما لا تعد الأرقام مناسبة، انتقل. لغة Go هي الخطوة التالية المعتادة، رغم أن Rust أو Node.js أو Erlang يمكن أن تؤدي الدور نفسه. تكمن القوة في الـ goroutines الخاصة بـ Go؛ حيث لا تكلف الـ goroutine سوى بضعة كيلوبايتات. يمكنك الحفاظ على عشرات الآلاف من التدفقات على أجهزة متواضعة دون عناء. يظل المنطق الأساسي متطابقًا — القراءة من التخزين المؤقت (cache)، والكتابة إلى المقبس (socket) — ولكن وقت التشغيل (runtime) ينتقل من العمليات الثقيلة إلى الخيوط (threads) خفيفة الوزن.
لكن لا تبدأ من هناك. لغة PHP ستوصلك إلى مدى بعيد بشكل مفاجئ. تحقق من المنتج أولاً. عندما تظهر صفحة المقاييس استنفاد العمال بدلاً من التحميل الزائد على قاعدة البيانات، فهذا يعني أنك تجاوزت قدرات هذه المجموعة التقنية (stack). وهذه مشكلة جيدة.
الخلاصة
العدادات المباشرة لا تتعلق بالتكنولوجيا المجردة، بل تتعلق بحماية قاعدة بياناتك من مستخدميك أنفسهم. ابدأ بـ SSE لأنها أبسط مما تبدو عليه. استخدم التخزين المؤقت (cache) بقوة بين التدفق وقاعدة البيانات حتى لا يتحول عدد الاتصالات إلى عدد استعلامات. راقب حدود العمال لديك كالصقر. وابدأ ببساطة. PHP كافية حتى تصبح غير كافية، وعندها ستعرف بالضبط سبب إعادة الكتابة.
لمشروعك القادم:
- استخدم SSE عندما يتدفق البيانات في اتجاه واحد، من الخادم إلى المتصفح.
- ضع طبقة تخزين مؤقت (cache) أمام قاعدة البيانات. استعلام واحد كل بضع ثوانٍ أفضل من آلاف الاستعلامات.
- ضع حدًا لاتصالات SSE لأقل من دقيقة واترك المتصفح يعيد الاتصال.
- أغلق التدفقات عند إخفاء علامة التبويب. لا تستهلك العمال في اتصالات خاملة.
- راقب استخدام عمال PHP. عندما تصل إلى الحد الأقصى، انقل طبقة البث إلى Go.
