व्यू काउंट वह पहला नंबर है जिस पर एक विज़िटर भरोसा करता है। यह उन्हें बताता है कि क्या कोई वीडियो उनके तीस सेकंड या तीस मिनट के समय के लायक है। TopVideoHub पर, वह नंबर तेज़ी से बदलता है। एक ट्रेंडिंग क्लिप दस मिनट में 40,000 व्यूज़ हासिल कर सकती है। अगर पेज पर काउंटर फ्रीज़ हो जाए, तो ऐसा लगता है जैसे कमरा खाली हो। यूज़र्स साइट छोड़ देते हैं।
उस नंबर को ब्राउज़र तक पहुँचाना मामूली लग सकता है। लेकिन ऐसा नहीं है। ज़्यादातर टीमें सबसे पहले 'पोलिंग' (polling) का सहारा लेती हैं। इसे सेटअप करना आसान है और यह स्टेजिंग (staging) में ठीक काम करता है। लेकिन स्टेजिंग धोखा दे सकती है।
जब पोलिंग खुद के खिलाफ DDoS बन जाए
TopVideoHub की टीम ने एक साधारण JavaScript poller बनाया। यह हर पांच सेकंड में लेटेस्ट व्यू काउंट फेच (fetch) करता था। तीन ब्राउज़र खुले होने वाले टेस्ट एनवायरनमेंट में यह बहुत अच्छा लग रहा था। लेकिन प्रोडक्शन (production) में, इसने पूरे प्लेटफॉर्म को तहस-नहस कर दिया।
हर पांच सेकंड में रिफ्रेश करने वाले आठ हज़ार कॉन्करेंट (concurrent) व्यूअर्स ने प्रति सेकंड 1,600 रिक्वेस्ट जेनरेट कीं। हर रिक्वेस्ट सीधे डेटाबेस में जा रही थी। रेप्लिकेशन लैग (replication lag) बढ़ गया। रीड रेप्लिकास (read replicas) पर दबाव बढ़ गया। कैश लेयर्स (cache layers) बायपास हो गईं। टीम वीडियो सर्व नहीं कर रही थी, बल्कि वे खुद ही अपने सिस्टम पर लोड डाल रहे थे।
पोलिंग तब तक ठीक है जब तक कि वह समस्या न बन जाए। लो-ट्रैफ़िक डैशबोर्ड या एडमिन पैनल के लिए यह ठीक है। लेकिन एक वायरल वीडियो पेज के लिए, यह एक टिक-टिक करते बम की तरह है। टीम को सर्वर से ब्राउज़र तक एक परसिस्टेंट पाइप (persistent pipe) की ज़रूरत थी, लेकिन उन्हें फुल-डुप्लेक्स प्रोटोकॉल (full-duplex protocol) की जटिलता नहीं चाहिए थी।
SSE वन-वे पाइप्स के लिए क्यों सही है
Server-Sent Events (SSE) ठीक इसी तरह की समस्या के लिए बनाया गया है: सर्वर के पास डेटा है, और ब्राउज़र को बस उसे सुनना है।
WebSockets के विपरीत, SSE सादे HTTP पर चलता है। यह सुनने में जितना साधारण लगता है, उससे कहीं ज़्यादा महत्वपूर्ण है। आपको नए प्रॉक्सी नियमों, अपग्रेड हेडर या लोड-बैलेंसर की जटिलताओं की ज़रूरत नहीं है। यदि आपका सर्वर HTTP/1.1 या HTTP/2 का उपयोग करता है, तो SSE काम करेगा। डिबगिंग (debugging) बहुत आसान है क्योंकि स्ट्रीम सिर्फ टेक्स्ट होती है। आप एंडपॉइंट पर curl चलाकर रियल टाइम में नंबरों को स्क्रॉल होते देख सकते हैं, जो कि यह अंदाज़ा लगाने से कहीं बेहतर है कि कोई बाइनरी सॉकेट फ्रेम क्यों गलत हो गया।
ब्राउज़र कठिन काम मुफ्त में संभाल लेता है। यदि कनेक्शन टूट जाता है, तो SSE Last-Event-ID हेडर के साथ स्वचालित रूप से फिर से कनेक्ट हो जाता है ताकि सर्वर को पता चल सके कि कहाँ से शुरू करना है। JavaScript में API सरफेस बहुत छोटी है: बस एक EventSource बनाएँ, एक onmessage हैंडलर जोड़ें, और आपका काम हो गया।
कैशिंग ही असली आर्किटेक्चर है
लाइव काउंटर्स में सबसे बड़ी आर्किटेक्चरल गलती हर ब्राउज़र कनेक्शन को डेटाबेस क्वेरी करने का कारण मानना है। यदि 8,000 लोग एक ही वीडियो देख रहे हैं, तो हर दो सेकंड में 8,000 क्वेरी चलाना पागलपन है। आपका डेटाबेस वायरल ट्रैफ़िक को झेल नहीं पाएगा।
TopVideoHub ने इसे APCu (PHP का इन-मेमोरी ऑपकोड और यूजर कैश) के साथ हल किया। इसका फ्लो (flow) सरल है। एक बैकग्राउंड प्रोसेस—या टाइमर पर आधारित एक लाइटवेट एंडपॉइंट—हर दो सेकंड में एक बार वर्तमान व्यू काउंट को APCu में लिखता है। SSE एंडपॉइंट, जिसे हज़ारों ब्राउज़र खुला रख सकते हैं, विशेष रूप से केवल APCu से ही डेटा पढ़ता है।
परिणाम: चाहे कितने भी व्यूअर्स जुड़े हों, डेटाबेस पर हर दो सेकंड में केवल एक ही हिट पड़ता है। कैश एक शॉक एब्जॉर्बर (shock absorber) की तरह काम करता है। APCu कोई बहुत अजीब चीज़ नहीं है। यह PHP के साथ आता है, शेयर्ड मेमोरी (shared memory) में रहता है, और किसी भी नेटवर्क राउंड-ट्रिप से तेज़ पढ़ता है। एक ऐसे नंबर के लिए जो बार-बार बदलता है लेकिन तुरंत नहीं, यह सही टूल है।
यदि आप APCu का उपयोग नहीं कर रहे हैं, तो Redis या Memcached भी काम करेंगे। सिद्धांत वही रहता है: हॉट रीड पाथ (hot read path) को डेटाबेस से अलग रखें।
PHP, LiteSpeed और Cloudflare को स्ट्रीमिंग के लिए राजी करना
PHP अपना काम खत्म करके तुरंत बंद होना चाहता है। वेब सर्वर आउटपुट को बफर (buffer) करना और एक व्यवस्थित रिस्पॉन्स देना चाहते हैं। SSE को इसके विपरीत चीज़ चाहिए: एक ऐसा कनेक्शन जो खुला रहे और जैसे ही बाइट्स आएं, उन्हें फ्लश (flush) करता रहे। सावधानी न बरतने पर, आपकी "स्ट्रीम" तीस सेकंड बाद एक ही बड़े टुकड़े (blob) के रूप में आती है, जिससे पूरा उद्देश्य ही खत्म हो जाता है।
यहाँ बताया गया है कि TopVideoHub ने पाइप को बिना रुकावट के कैसे चालू रखा।
आउटपुट बफरिंग को खत्म करें। SSE स्क्रिप्ट की शुरुआत में, PHP द्वारा इनेबल की गई बफरिंग की हर लेयर को डिसेबल कर दें। यदि कोई बफर एक्टिव है, तो ob_end_flush() को कॉल करें, और हेडर भेजने के बाद ob_implicit_flush(true) के साथ इम्प्लिसिट फ्लशिंग (implicit flushing) को बंद कर दें।
प्रॉक्सियों को पीछे हटने के लिए कहें। X-Accel-Buffering: no हेडर भेजें। Nginx इसे मानता है। LiteSpeed भी इसे मानता है। यह संकेत देता है कि रिस्पॉन्स को बफर या कैश करने योग्य टुकड़े में कंप्रेस नहीं किया जाना चाहिए।
लाइफटाइम कम रखें। प्रत्येक SSE कनेक्शन एक PHP वर्कर (worker) को व्यस्त रखता है। TopVideoHub स्ट्रीम को 55 सेकंड तक सीमित रखता है। जब टाइमर पूरा होता है, तो सर्वर एक अंतिम कमेंट भेजता है, स्ट्रीम को बंद कर देता है, और ब्राउज़र स्वचालित रूप से फिर से कनेक्ट हो जाता है। वह रीकनेक्शन एक नए वर्कर पर जाता है, जिससे किसी भी सिंगल प्रोसेस के हमेशा के लिए अटके रहने की समस्या नहीं होती।
जीवित रहने के लिए पिंग (Ping) करें। हर बीस सेकंड में एक कमेंट लाइन भेजें—जैसे कि : ping। SSE में कमेंट्स को ब्राउज़र का मैसेज हैंडलर अनदेखा कर देता है, लेकिन वे TCP कनेक्शन को सक्रिय (warm) रखते हैं। लोड बैलेंसर और CDNs अक्सर तीस या साठ सेकंड के बाद शांत कनेक्शनों को काट देते हैं। एक साधारण न्यूलाइन (newline) आपको इस समस्या से बचा सकती है।
यूज़र के टैब का सम्मान करें। जब कोई विज़िटर टैब को मिनिमाइज़ या हाइड कर दे, तो कनेक्शन बंद कर दें। ब्राउज़र में visibilitychange को सुनें और eventSource.close() को कॉल करें। सर्वर को भी क्लाइंट डिस्कनेक्ट होने का पता लगाना चाहिए और लूप को समाप्त कर देना चाहिए। PHP एक लूप के अंदर connection_aborted() को चेक कर सकता है। उन लोगों के लिए वर्कर्स को बर्बाद न होने दें जो दस मिनट पहले ही चले गए हैं (ghost connections)।
एक सख्त सीमा: PHP वर्कर्स
PHP में SSE अपनी सीमाओं के प्रति ईमानदार है। प्रत्येक खुला SSE कनेक्शन एक PHP वर्कर का उपयोग करता है। यदि आपके पूल में सौ वर्कर्स हैं, तो आपके पास सौ स्ट्रीम्स हैं। बस इतना ही। जब तक आप Apache या PHP-FPM प्रोसेस मॉडल के भीतर हैं, तब तक कोई async वर्कअराउंड (workaround) नहीं है। आप pm.max_children को ट्यून कर सकते हैं, लेकिन मेमोरी और CPU ही वास्तविक सीमा तय करते हैं।
यह सीमा तब जल्दी असर दिखाती है जब आप वर्कर्स का उपयोग नियमित पेज लोड, API कॉल और एसेट जनरेशन के लिए भी कर रहे हों। अपने वर्कर सैचुरेशन (worker saturation) की सावधानीपूर्वक निगरानी करें। यदि आपका SSE एंडपॉइंट कतार (queuing) में लगने लगता है क्योंकि सभी वर्कर्स बीस मिनट की स्ट्रीम्स में व्यस्त हैं, तो आपकी पूरी साइट धीमी हो जाएगी।
जब संख्याएँ अब पर्याप्त न रहें, तो आगे बढ़ें। Go आमतौर पर अगला कदम होता है, हालांकि Rust, Node.js, या Erlang भी वही भूमिका निभा सकते हैं। Go के goroutines ही इसकी कुंजी हैं। एक goroutine की लागत कुछ किलोबाइट होती है। आप बिना किसी परेशानी के मामूली हार्डवेयर पर भी हज़ारों स्ट्रीम्स को संभाल सकते हैं। मुख्य लॉजिक वही रहता है—कैश से पढ़ें, सॉकेट पर लिखें—लेकिन रनटाइम भारी प्रोसेस से बदलकर हल्केवेट थ्रेड्स (lightweight threads) में बदल जाता है।
हालांकि, वहीं से शुरुआत न करें। PHP आपको आश्चर्यजनक रूप से काफी आगे तक ले जा सकता है। पहले प्रोडक्ट को वैलिडेट करें। जब मेट्रिक्स पेज डेटाबेस ओवरलोड के बजाय वर्कर की कमी (worker exhaustion) दिखाए, तब समझें कि आप इस स्टैक से आगे निकल चुके हैं। यह एक अच्छी समस्या है।
मुख्य बातें (Takeaway)
लाइव काउंटर केवल तकनीक के बारे में नहीं हैं। वे आपके डेटाबेस को आपके अपने यूज़र्स से बचाने के बारे में हैं। SSE से शुरुआत करें क्योंकि यह दिखने में जितना सरल है, उससे कहीं अधिक है। स्ट्रीम और डेटाबेस के बीच आक्रामक रूप से कैश (cache) का उपयोग करें ताकि कनेक्शन की संख्या क्वेरी की संख्या न बन जाए। अपने वर्कर लिमिट्स पर पैनी नज़र रखें। और शुरुआत सरल तरीके से करें। PHP तब तक पर्याप्त है जब तक कि वह पर्याप्त न रहे, और तब तक आप ठीक से जान जाएंगे कि आप इसे दोबारा क्यों लिख रहे हैं।
आपके अगले प्रोजेक्ट के लिए:
- SSE का उपयोग तब करें जब डेटा एक तरफ से बहता हो, सर्वर से ब्राउज़र की ओर।
- डेटाबेस के आगे एक कैश लेयर (cache layer) रखें। हर कुछ सेकंड में एक क्वेरी हज़ारों क्वेरीज़ से बेहतर है।
- SSE कनेक्शन को एक मिनट से कम समय के लिए सीमित रखें और ब्राउज़र को फिर से कनेक्ट करने दें।
- टैब हाइड होने पर स्ट्रीम्स को बंद कर दें। खाली पड़े कनेक्शनों के लिए लाइव वर्कर्स का उपयोग न करें।
- PHP वर्कर के उपयोग की निगरानी करें। जब आप अपनी सीमा तक पहुँच जाएँ, तो स्ट्रीमिंग लेयर को Go में पोर्ट कर लें।
