Kami memecahkan masalah sejuta tab terbuka atau "membantu perangkat keras bertahan"


Kami akan mencoba mencari cara untuk mengurangi beban pada perangkat keras server, sambil memastikan kinerja aplikasi Web maksimum.


Dalam pengembangan proyek-proyek besar, beban tinggi dengan online besar, Anda seringkali harus memikirkan cara mengurangi beban di server, terutama ketika bekerja di webSockets dan antarmuka yang berubah secara dinamis. 100500 pengguna datang kepada kami dan kami memiliki 100500 koneksi soket terbuka. Dan jika masing-masing membuka 2 tab - ini adalah * 201.000 koneksi. Dan jika lima?


Pertimbangkan contoh sepele. Kami punya, misalnya, Twitch.tv , yang untuk setiap pengguna menimbulkan koneksi WS. Proyek semacam itu sangat besar online, jadi setiap detail penting. Kami tidak dapat membuka koneksi-WS baru pada setiap tab, mendukung yang lama, karena kelenjar perlu diukur untuk ini.


Idenya lahir - bagaimana jika koneksi WS dibesarkan hanya dalam satu tab dan selalu tetap terbuka, dan di yang baru, jangan menginisialisasi koneksi, tetapi hanya mendengarkan dari tab tetangga? Ini tentang implementasi ide ini yang ingin saya sampaikan.


Perilaku Logika Peramban


  1. Buka tab pertama, tandai sebagai Utama
  2. Jalankan tes - jika tab is_primary, lalu naikkan koneksi-WS
  3. Kami sedang bekerja ...
  4. Buka tab kedua (duplikat jendela, masukkan alamat secara manual, buka di tab baru, itu tidak masalah)
  5. Dari tab baru, lihat apakah ada suatu tempat tab Utama. Jika ya, maka tandai Secondary saat ini dan tunggu apa yang akan terjadi.
  6. Buka 10 tab lainnya. Dan semua orang menunggu.
  7. Pada titik tertentu, tab Utama ditutup. Sebelum kematiannya, dia berteriak kepada semua orang tentang kematiannya. Semuanya kaget.
  8. Dan kemudian semua tab mencoba untuk secara instan menjadi Pratama. Reaksi setiap orang berbeda (acak) dan siapa pun yang punya waktu, itu, dan sandal. Segera setelah salah satu tab berhasil menjadi is_primary, dia berteriak kepada semua orang bahwa tempat itu diambil. Setelah itu, koneksi WS masuk kembali. Kami sedang bekerja. Sisanya sedang menunggu.
  9. Dll Pemulung sedang menunggu kematian tab Pratama untuk jatuh ke tempatnya.

Sisi teknis dari masalah ini


Untuk berkomunikasi di antara tab, kami akan menggunakan apa yang menghubungkan mereka dalam domain yang sama - localStorage. Panggilan untuk itu tidak mahal untuk sumber daya besi pengguna dan respons dari mereka sangat cepat. Sekitar itu, seluruh ide sedang dibangun.


Ada perpustakaan yang sudah lama tidak didukung oleh pembuatnya, tetapi Anda bisa menjadikannya sebagai garpu lokal, seperti yang saya lakukan. Dari sana kita mendapatkan file:


/intercom.js


Inti dari pustaka adalah memungkinkan emit / on events untuk berkomunikasi antar tab menggunakan localStorage untuk ini.


Setelah itu, kita memerlukan alat yang memungkinkan kita untuk mengunci ( memblokir perubahan ) kunci tertentu di Penyimpanan lokal, tanpa mengizinkan siapa pun untuk mengubahnya tanpa hak yang diperlukan. Untuk ini, perpustakaan kecil yang disebut " locableStorage " ditulis, esensi yang terkandung dalam fungsi trySyncLock ()


Kode perpustakaan 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) } }; })(); 

Sekarang perlu untuk menggabungkan semuanya menjadi satu mekanisme tunggal, yang akan memungkinkan kita untuk mengimplementasikan rencana kita.


Kode implementasi
 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.'); } 

Sekarang dengan jari saya akan menjelaskan apa yang terjadi di sini.


Proyek demo GitHub


Langkah 1. Membuka tab pertama


Contoh ini mengimplementasikan penghitung waktu yang bekerja dalam beberapa kontribusi, tetapi penghitungannya hanya terjadi dalam satu kontribusi. Kode penghitung waktu dapat diganti dengan apa saja, misalnya, dengan menginisialisasi koneksi WS. ketika dimulai, webSocketInit () segera dieksekusi, yang pada tab pertama akan mengarahkan kita untuk memulai penghitung ( buka soket ), serta untuk memulai timer startHeartBitInterval () untuk memperbarui nilai kunci " wsLU " di localStorage. Kunci ini bertanggung jawab untuk pembuatan dan pemeliharaan tab Utama. Ini adalah elemen kunci dari keseluruhan struktur. Pada saat yang sama, kunci wsOpen dibuat, yang bertanggung jawab untuk status penghitung (atau membuka koneksi WS) dan variabel primaryStatus , yang membuat tab saat ini utama, menjadi benar. Kwitansi acara apa pun dari konter (koneksi-WS) akan dikeluarkan oleh Intercom, dengan desain:


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

Langkah 2. Membuka tab kedua


Membuka tab kedua, ketiga dan lainnya akan menyebabkan webSocketInit () , setelah itu kunci " wsLU " dan " forceOpen " masuk ke pertempuran. Jika kodenya:


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

... akan menyebabkan forceOpen menjadi benar , maka penghitung akan berhenti dan mulai lagi, tetapi ini tidak akan terjadi, karena diff tidak akan lebih besar dari nilai yang ditentukan, karena kunci wsLU didukung oleh tab Utama saat ini. Semua tab Sekunder akan mendengarkan acara yang diberikan tab Utama kepada mereka melalui Interkom, dengan desain:


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

Langkah 3. Menutup tab


Tab penutup memicu acara onbeforeunload di browser modern. Kami memprosesnya sebagai berikut:


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

Perlu dicatat bahwa semua metode akan dipanggil hanya di tab Utama. Saat Anda menutup tab Sekunder apa pun, tidak akan terjadi apa pun pada penghitung. Anda hanya perlu menghapus penyadapan acara untuk membebaskan memori. Tetapi jika kita menutup tab Utama, maka kita mengatur wsOpen ke false dan memecat acara TAB_CLOSED. Semua tab yang terbuka akan segera menanggapinya:


 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! } }); 

Di sinilah keajaiban dimulai. Berfungsi ...


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

... memungkinkan Anda untuk memanggil inisialisasi soket (dalam kasus kami, penghitung) pada interval yang berbeda, yang memungkinkan salah satu tab Sekunder untuk menjadi Utama dan menulis info tentang hal itu ke Penyimpanan lokal. Setelah di-perdukakan dalam angka (1, 1000), Anda dapat mencapai respons tab tercepat. Tab Sekunder yang tersisa tetap untuk mendengarkan acara dan meresponsnya, menunggu Pratama mati.


Ringkasan


Kami mendapat desain yang memungkinkan Anda hanya menyimpan satu koneksi webSocket untuk seluruh aplikasi, tidak peduli berapa banyak tab yang dimilikinya, yang akan secara signifikan mengurangi beban pada perangkat keras server kami dan, sebagai hasilnya, akan memungkinkan Anda untuk tetap online.

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


All Articles