हम एक लाख खुले टैब की समस्या को हल करते हैं या "हार्डवेयर को जीवित रहने में मदद करते हैं"


हम अधिकतम वेब अनुप्रयोग प्रदर्शन सुनिश्चित करते हुए सर्वर हार्डवेयर पर लोड को कम करने का तरीका जानने की कोशिश करेंगे।


विशाल ऑनलाइन के साथ बड़े, उच्च-लोड प्रोजेक्ट्स के विकास में, आपको अक्सर यह सोचना होगा कि सर्वर पर लोड को कैसे कम किया जाए, खासकर जब वेबस्केट्स में काम करना और गतिशील रूप से बदलते इंटरफेस। 100500 उपयोगकर्ता हमारे पास आते हैं और हमारे पास 100500 खुले सॉकेट कनेक्शन हैं। और अगर उनमें से प्रत्येक 2 टैब खोलता है - यह * 201,000 कनेक्शन है। और अगर पाँच?


एक तुच्छ उदाहरण पर विचार करें। हमारे पास, उदाहरण के लिए, Twitch.tv , जो प्रत्येक उपयोगकर्ता के लिए एक WS कनेक्शन बढ़ाता है। इस तरह की परियोजना ऑनलाइन बड़ी है, इसलिए हर विवरण महत्वपूर्ण है। हम पुराने टैब का समर्थन करते हुए प्रत्येक टैब पर एक नया डब्ल्यूएस-कनेक्शन खोलने का जोखिम नहीं उठा सकते हैं, क्योंकि ग्रंथि को इसके लिए असहनीय होने की आवश्यकता है।


विचार पैदा होता है - क्या होगा यदि WS कनेक्शन केवल एक टैब में उठाए जाते हैं और इसे हमेशा खुला रखते हैं, और नए में कनेक्शन को प्रारंभ नहीं करते हैं, लेकिन सिर्फ पड़ोसी टैब से सुनते हैं? यह इस विचार के कार्यान्वयन के बारे में है जिसे मैं बताना चाहता हूं।


ब्राउज़र लॉजिक बिहेवियर


  1. पहला टैब खोलें, इसे प्राथमिक के रूप में चिह्नित करें
  2. परीक्षण चलाएँ - यदि is_primary टैब है, तो WS-कनेक्शन बढ़ाएँ
  3. हम काम कर रहे हैं ...
  4. दूसरा टैब खोलें (विंडो को डुप्लिकेट करें, मैन्युअल रूप से पता दर्ज करें, एक नए टैब में खोलें, इससे कोई फर्क नहीं पड़ता)
  5. नए टैब से, देखें कि कहीं प्राथमिक टैब है या नहीं। यदि हाँ, तो वर्तमान माध्यमिक को चिह्नित करें और क्या होगा इसके लिए प्रतीक्षा करें।
  6. 10 और टैब खोलें। और सभी को इंतजार है।
  7. कुछ बिंदु पर, प्राथमिक टैब बंद हो जाता है। अपनी मृत्यु से पहले, वह अपनी मौत के बारे में सभी को चिल्लाती है। सब कुछ सदमे में है।
  8. और फिर सभी टैब तुरंत प्राथमिक बनने की कोशिश कर रहे हैं। सभी की प्रतिक्रिया अलग-अलग (यादृच्छिक) है और जिसके पास समय है, वह और चप्पल। जैसे ही टैब में से एक is_primary बनने में कामयाब रहा, वह सभी को चिल्लाती है कि जगह ले ली गई है। उसके बाद, WS कनेक्शन पुन: प्रवेश करता है। हम काम कर रहे हैं। बाकी लोग इंतजार कर रहे हैं।
  9. आदि स्केवियर्स प्राथमिक टैब की मृत्यु की प्रतीक्षा कर रहे हैं ताकि इसकी जगह गिर जाए।

मुद्दे का तकनीकी पक्ष


टैब के बीच संवाद करने के लिए, हम उसी डोमेन के भीतर उन्हें जोड़ने वाले का उपयोग करेंगे - लोकलस्टोरेज। इसके लिए कॉल उपयोगकर्ता के लौह संसाधनों के लिए महंगी नहीं हैं और उनसे प्रतिक्रिया बहुत तेज है। इसके आसपास, पूरे विचार का निर्माण किया जा रहा है।


एक पुस्तकालय है जिसे लंबे समय से निर्माता द्वारा समर्थित नहीं किया गया है, लेकिन आप इसे एक स्थानीय कांटा बना सकते हैं, जैसा कि मैंने किया था। इससे हमें फाइल मिलती है:


/intercom.js


पुस्तकालय का सार यह है कि यह इसके लिए स्थानीयस्टोरेज का उपयोग करके टैब के बीच संवाद करने के लिए ईमित्र / घटनाओं की अनुमति देता है।


उसके बाद, हमें एक उपकरण की आवश्यकता होती है जो हमें लोकलस्टोरेज में एक निश्चित कुंजी को लॉक करने ( ब्लॉक चेंज) करने की अनुमति देता है, बिना किसी को आवश्यक अधिकारों के बिना इसे बदलने की अनुमति नहीं देता है। इसके लिए, " locableStorage " नामक एक छोटी सी लाइब्रेरी लिखी गई थी, जिसका सार trySyncLock () फ़ंक्शन में निहित है


LocableStorage पुस्तकालय कोड
 (function () { function now() { return new Date().getTime(); } function someNumber() { return Math.random() * 1000000000 | 0; } let myId = now() + ":" + someNumber(); function getter(lskey) { return function () { let value = localStorage[lskey]; if (!value) return null; let splitted = value.split(/\|/); if (parseInt(splitted[1]) < now()) { return null; } return splitted[0]; } } function _mutexTransaction(key, callback, synchronous) { let xKey = key + "__MUTEX_x", yKey = key + "__MUTEX_y", getY = getter(yKey); function criticalSection() { try { callback(); } finally { localStorage.removeItem(yKey); } } localStorage[xKey] = myId; if (getY()) { if (!synchronous) setTimeout(function () { _mutexTransaction(key, callback); }, 0); return false; } localStorage[yKey] = myId + "|" + (now() + 40); if (localStorage[xKey] !== myId) { if (!synchronous) { setTimeout(function () { if (getY() !== myId) { setTimeout(function () { _mutexTransaction(key, callback); }, 0); } else { criticalSection(); } }, 50) } return false; } else { criticalSection(); return true; } } function lockImpl(key, callback, maxDuration, synchronous) { maxDuration = maxDuration || 5000; let mutexKey = key + "__MUTEX", getMutex = getter(mutexKey), mutexValue = myId + ":" + someNumber() + "|" + (now() + maxDuration); function restart() { setTimeout(function () { lockImpl(key, callback, maxDuration); }, 10); } if (getMutex()) { if (!synchronous) restart(); return false; } let aquiredSynchronously = _mutexTransaction(key, function () { if (getMutex()) { if (!synchronous) restart(); return false; } localStorage[mutexKey] = mutexValue; if (!synchronous) setTimeout(mutexAquired, 0) }, synchronous); if (synchronous && aquiredSynchronously) { mutexAquired(); return true; } return false; function mutexAquired() { try { callback(); } finally { _mutexTransaction(key, function () { if (localStorage[mutexKey] !== mutexValue) throw key + " was locked by a different process while I held the lock" localStorage.removeItem(mutexKey); }); } } } window.LockableStorage = { lock: function (key, callback, maxDuration) { lockImpl(key, callback, maxDuration, false) }, trySyncLock: function (key, callback, maxDuration) { return lockImpl(key, callback, maxDuration, true) } }; })(); 

अब सब कुछ एक ही तंत्र में संयोजित करना आवश्यक है, जो हमें अपनी योजनाओं को लागू करने की अनुमति देगा।


कार्यान्वयन कोड
 if (Intercom.supported) { let intercom = Intercom.getInstance(), //Intercom singleton period_heart_bit = 1, //LocalStorage update frequency wsId = someNumber() + Date.now(), //Current tab ID primaryStatus = false, //Primary window tab status refreshIntervalId, count = 0, //Counter. Delete this intFast; //Timer window.webSocketInit = webSocketInit; window.semiCloseTab = semiCloseTab; intercom.on('incoming', data => { document.getElementById('counter').innerHTML = data.data; document.getElementById('socketStatus').innerHTML = primaryStatus.toString(); return false; }); /** * Random number * @returns {number} - number */ function someNumber() { return Math.random() * 1000000000 | 0; } /** * Try do something */ function webSocketInit() { // Check for crash or loss network let forceOpen = false, wsLU = localStorage.wsLU; if (wsLU) { let diff = Date.now() - parseInt(wsLU); forceOpen = diff > period_heart_bit * 5 * 1000; } //Double checked locking if (!localStorage.wsOpen || localStorage.wsOpen !== "true" || forceOpen) { LockableStorage.trySyncLock("wsOpen", function () { if (!localStorage.wsOpen || localStorage.wsOpen !== "true" || forceOpen) { localStorage.wsOpen = true; localStorage.wsId = wsId; localStorage.wsLU = Date.now(); //TODO this app logic that must be SingleTab ---------------------------- primaryStatus = true; intFast = setInterval(() => { intercom.emit('incoming', {data: count}); count++ }, 1000); //TODO ------------------------------------------------------------------ startHeartBitInterval(); } }); } } /** * Show singleTab app status */ setInterval(() => { document.getElementById('wsopen').innerHTML = localStorage.wsOpen; }, 200); /** * Update localStorage info */ function startHeartBitInterval() { refreshIntervalId = setInterval(function () { localStorage.wsLU = Date.now(); }, period_heart_bit * 1000); } /** * Close tab action */ intercom.on('TAB_CLOSED', function (data) { if (localStorage.wsId !== wsId) { count = data.count; setTimeout(() => { webSocketInit() }, parseInt(getRandomArbitary(1, 1000), 10)); //Init after random time. Important! } }); function getRandomArbitary(min, max) { return Math.random() * (max - min) + min; } /** * Action after some tab closed */ window.onbeforeunload = function () { if (primaryStatus) { localStorage.setItem('wsOpen', false); clearInterval(refreshIntervalId); intercom.emit('TAB_CLOSED', {count: count}); } }; /** * Emulate close window */ function semiCloseTab() { if (primaryStatus) { localStorage.setItem('wsOpen', false); clearInterval(refreshIntervalId); clearInterval(intFast); intercom.emit('TAB_CLOSED', {count: count}); } } webSocketInit() //Try do something } else { alert('intercom.js is not supported by your browser.'); } 

अब उंगलियों पर समझाऊंगा कि यहां क्या हो रहा है।


GitHub डेमो प्रोजेक्ट


चरण 1. पहला टैब खोलना


यह उदाहरण कई योगदानों में काम करने वाले टाइमर को लागू करता है, लेकिन इसकी गणना केवल एक में होती है। टाइमर कोड को कुछ भी से बदला जा सकता है, उदाहरण के लिए, डब्ल्यूएस कनेक्शन को इनिशियलाइज़ करके। जब शुरू किया जाता है, तो webSocketInit () को तुरंत निष्पादित किया जाता है, जो पहले टैब में हमें काउंटर शुरू करने के लिए ले जाएगा ( सॉकेट खोलें ), साथ ही साथ स्थानीय स्टार्टर में " wsLU " कुंजी मूल्य को अपडेट करने के लिए टाइमर startHeartBitInterval () को शुरू करना होगा। यह कुंजी प्राथमिक टैब के निर्माण और रखरखाव के लिए जिम्मेदार है। यह पूरी संरचना का एक प्रमुख तत्व है। उसी समय, कुंजी " wsOpen " बनाई जाती है, जो काउंटर की स्थिति के लिए ज़िम्मेदार है (या एक डब्ल्यूएस कनेक्शन खोलना) और चर " प्राइमरीस्टैटस ", जो वर्तमान टैब को मुख्य बनाता है, सच हो जाता है। काउंटर से किसी भी घटना की प्राप्ति (WS- कनेक्शन) डिजाइन के साथ इंटरकॉम द्वारा उत्सर्जित की जाएगी:


 intercom.emit('incoming', {data: count}); 

चरण 2. दूसरा टैब खोलना


दूसरे, तीसरे और किसी भी अन्य टैब को खोलने से WebSocketInit () का कारण बनेगा , जिसके बाद कुंजी " wsLU " और " forceOpen " लड़ाई में प्रवेश करते हैं। यदि कोड:


 if (wsLU) { let diff = Date.now() - parseInt(wsLU); forceOpen = diff > period_heart_bit * 5 * 1000; } 

... बल का कारण बनेगा , सच हो जाएगा, तो काउंटर बंद हो जाएगा और फिर से शुरू होगा, लेकिन ऐसा नहीं होगा, क्योंकि diff निर्दिष्ट मान से अधिक नहीं होगा, क्योंकि wsLU कुंजी वर्तमान प्राथमिक टैब द्वारा समर्थित है। सभी माध्यमिक टैब उन घटनाओं को सुनेंगे जो प्राथमिक टैब इंटरकॉम के माध्यम से उन्हें डिजाइन के साथ देता है:


 intercom.on('incoming', data => { document.getElementById('counter').innerHTML = data.data; document.getElementById('socketStatus').innerHTML = primaryStatus.toString(); return false; }); 

चरण 3. टैब बंद करना


बंद करने वाले टैब आधुनिक ब्राउज़रों में ऑनबेफ्रुनलोड घटना को ट्रिगर करते हैं। हम इसे इस प्रकार संसाधित करते हैं:


 window.onbeforeunload = function () { if (primaryStatus) { localStorage.setItem('wsOpen', false); clearInterval(refreshIntervalId); intercom.emit('TAB_CLOSED', {count: count}); } }; 

यह ध्यान दिया जाना चाहिए कि सभी तरीकों को केवल प्राथमिक टैब में कहा जाएगा। जब आप किसी भी द्वितीयक टैब को बंद करते हैं, तो काउंटर पर कुछ भी नहीं होगा। आपको बस मेमोरी को खाली करने के लिए घटनाओं के वायरटैप को हटाने की आवश्यकता है। लेकिन अगर हम प्राइमरी टैब को बंद कर देते हैं, तो हम TAB_CLOSED इवेंट को झूठा और आग देने के लिए wsOpen सेट करते हैं । सभी खुले टैब तुरंत इसका जवाब देंगे:


 intercom.on('TAB_CLOSED', function (data) { if (localStorage.wsId !== wsId) { count = data.count; setTimeout(() => { webSocketInit() }, parseInt(getRandomArbitary(1, 1000), 10)); //Init after random time. Important! } }); 

यहीं से शुरू होता है जादू। समारोह ...


getRandomArbitary (1, 1000)
 function getRandomArbitary(min, max) { return Math.random() * (max - min) + min; } 

... आपको अलग-अलग अंतराल पर सॉकेट (हमारे मामले में, काउंटर) के आरंभीकरण को कॉल करने की अनुमति देता है, जो कुछ माध्यमिक टैब के लिए प्राथमिक बनना संभव बनाता है और इसके बारे में जानकारी स्थानीयस्टोरेज में लिखता है। संख्या (1, 1000) में shamanized होने के बाद , आप टैब की सबसे तेज़ प्रतिक्रिया प्राप्त कर सकते हैं। शेष माध्यमिक टैब घटनाओं को सुनने और उन पर प्रतिक्रिया करने के लिए बने हुए हैं, प्राथमिक मरने की प्रतीक्षा कर रहे हैं।


परिणाम


हमें एक ऐसा डिज़ाइन मिला है, जिससे आप पूरे एप्लिकेशन के लिए केवल एक वेब-सॉकेट कनेक्शन रख सकते हैं, चाहे उसके पास कितने भी टैब हों, जो हमारे सर्वर के हार्डवेयर पर लोड को काफी कम कर देगा और परिणामस्वरूप, आपको अधिक ऑनलाइन रखने की अनुमति देगा।

Source: https://habr.com/ru/post/hi415401/


All Articles