
Wir werden versuchen herauszufinden, wie Sie die Belastung der Serverhardware verringern und gleichzeitig die maximale Leistung von Webanwendungen sicherstellen können.
Bei der Entwicklung großer Projekte mit hoher Auslastung und großem Online-Aufwand müssen Sie häufig darüber nachdenken, wie Sie die Belastung des Servers verringern können, insbesondere wenn Sie in webSockets arbeiten und Schnittstellen dynamisch ändern. 100500 Benutzer kommen zu uns und wir haben 100500 offene Socket-Verbindungen. Und wenn jeder von ihnen 2 Registerkarten öffnet, sind dies * 201.000 Verbindungen. Und wenn fünf?
Betrachten Sie ein triviales Beispiel. Wir haben zum Beispiel Twitch.tv , die für jeden Benutzer eine WS-Verbindung herstellt. Ein solches Projekt ist online riesig, daher ist jedes Detail wichtig. Wir können es uns nicht leisten, auf jeder Registerkarte eine neue WS-Verbindung zu öffnen, die die alte unterstützt, da die Drüse dafür nicht gemessen werden muss.
Die Idee ist geboren - was ist, wenn WS-Verbindungen nur in einer Registerkarte ausgelöst werden und immer offen bleiben und in den neuen nicht die Verbindung initialisiert werden, sondern nur über die benachbarte Registerkarte abgehört werden? Es geht um die Umsetzung dieser Idee, die ich erzählen möchte.
Verhalten der Browserlogik
- Öffnen Sie die erste Registerkarte und markieren Sie sie als Primär
- Führen Sie den Test aus. Wenn sich die Registerkarte is_primary befindet, stellen Sie die WS-Verbindung her
- Wir arbeiten ...
- Öffnen Sie die zweite Registerkarte (duplizieren Sie das Fenster, geben Sie die Adresse manuell ein, öffnen Sie eine neue Registerkarte, es spielt keine Rolle)
- Überprüfen Sie auf der neuen Registerkarte, ob sich irgendwo eine Registerkarte "Primär" befindet. Wenn ja, markieren Sie die aktuelle Sekundarstufe und warten Sie, was passieren wird.
- Öffnen Sie 10 weitere Registerkarten. Und alle warten.
- Irgendwann wird die Registerkarte Primär geschlossen. Vor ihrem Tod schreit sie alle nach ihrem Tod. Alles steht unter Schock.
- Und dann versuchen alle Registerkarten sofort, primär zu werden. Die Reaktion aller ist unterschiedlich (zufällig) und wer Zeit hat, das und Hausschuhe. Sobald es einer der Registerkarten gelungen ist, is_primary zu werden, schreit sie allen zu, dass der Platz eingenommen wird. Danach tritt die WS-Verbindung wieder selbst ein. Wir arbeiten. Der Rest wartet.
- Usw. Aasfresser warten darauf, dass der Tod der Registerkarte "Primär" an ihre Stelle fällt.
Die technische Seite des Problems
Für die Kommunikation zwischen den Registerkarten verwenden wir das, was sie innerhalb derselben Domäne verbindet - localStorage. Anrufe sind für die Eisenressourcen des Benutzers nicht teuer und die Antwort von ihnen ist sehr schnell. Um ihn herum wird die ganze Idee aufgebaut.
Es gibt eine Bibliothek , die vom Ersteller schon lange nicht mehr unterstützt wurde, aber Sie können sie wie ich zu einem lokalen Zweig machen. Daraus erhalten wir die Datei:
/intercom.js
Das Wesentliche an der Bibliothek ist, dass sie es ermöglicht, dass emit / on-Ereignisse mithilfe von localStorage zwischen Registerkarten kommunizieren.
Danach benötigen wir ein Tool, mit dem wir einen bestimmten Schlüssel in localStorage sperren ( Änderungen blockieren ) können, ohne dass jemand ihn ohne die erforderlichen Rechte ändern kann. Zu diesem Zweck wurde eine kleine Bibliothek namens " locableStorage " geschrieben, deren Kern in der Funktion trySyncLock () enthalten ist
LocableStorage-Bibliothekscode (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) } }; })();
Jetzt ist es notwendig, alles in einem einzigen Mechanismus zu kombinieren, damit wir unsere Pläne umsetzen können.
Implementierungscode if (Intercom.supported) { let intercom = Intercom.getInstance(),
Jetzt werde ich an den Fingern erklären, was hier passiert.
GitHub-Demo-Projekt
Schritt 1. Öffnen Sie die erste Registerkarte
In diesem Beispiel wird ein Timer implementiert, der in mehreren Beiträgen arbeitet, dessen Berechnung jedoch nur in einem erfolgt. Der Timer-Code kann durch alles ersetzt werden, beispielsweise durch Initialisieren der WS-Verbindung. Beim Start wird sofort webSocketInit () ausgeführt, was auf der ersten Registerkarte dazu führt, dass wir den Zähler starten ( den Socket öffnen ) und den Timer startHeartBitInterval () starten , um den Schlüsselwert " wsLU " in localStorage zu aktualisieren. Dieser Schlüssel ist für die Erstellung und Pflege der Registerkarte Primär verantwortlich. Dies ist ein Schlüsselelement der gesamten Struktur. Gleichzeitig wird der Schlüssel " wsOpen " erstellt, der für den Status des Zählers (oder das Öffnen einer WS-Verbindung) verantwortlich ist, und die Variable " primaryStatus ", die die aktuelle Registerkarte main macht, wird wahr. Der Empfang eines Ereignisses vom Zähler (WS-Verbindung) wird von Intercom mit folgendem Design gesendet:
intercom.emit('incoming', {data: count});
Schritt 2. Öffnen Sie die zweite Registerkarte
Das Öffnen der zweiten, dritten und einer anderen Registerkarte führt zu webSocketInit (). Danach treten die Schlüssel " wsLU " und " forceOpen " in den Kampf. Wenn der Code:
if (wsLU) { let diff = Date.now() - parseInt(wsLU); forceOpen = diff > period_heart_bit * 5 * 1000; }
... bewirkt, dass forceOpen wahr wird , dann stoppt der Zähler und startet erneut, aber dies wird nicht passieren, weil diff ist nicht größer als der angegebene Wert, da der wsLU- Schlüssel von der aktuellen Registerkarte Primary unterstützt wird. Alle sekundären Registerkarten hören die Ereignisse ab, die die Registerkarte Primär über Intercom mit folgendem Design anzeigt:
intercom.on('incoming', data => { document.getElementById('counter').innerHTML = data.data; document.getElementById('socketStatus').innerHTML = primaryStatus.toString(); return false; });
Schritt 3. Schließen Sie die Registerkarte
Das Schließen von Registerkarten löst in modernen Browsern das Ereignis onbeforeunload aus . Wir verarbeiten es wie folgt:
window.onbeforeunload = function () { if (primaryStatus) { localStorage.setItem('wsOpen', false); clearInterval(refreshIntervalId); intercom.emit('TAB_CLOSED', {count: count}); } };
Es ist zu beachten, dass alle Methoden nur auf der Registerkarte Primär aufgerufen werden. Wenn Sie eine sekundäre Registerkarte schließen, passiert dem Zähler nichts. Sie müssen nur das Abhören von Ereignissen entfernen, um Speicher freizugeben. Wenn wir jedoch die Registerkarte Primär schließen, setzen wir wsOpen auf false und lösen das Ereignis TAB_CLOSED aus. Alle geöffneten Registerkarten reagieren sofort darauf:
intercom.on('TAB_CLOSED', function (data) { if (localStorage.wsId !== wsId) { count = data.count; setTimeout(() => { webSocketInit() }, parseInt(getRandomArbitary(1, 1000), 10));
Hier beginnt die Magie. Funktion ...
getRandomArbitary (1, 1000) function getRandomArbitary(min, max) { return Math.random() * (max - min) + min; }
... ermöglicht es Ihnen, die Initialisierung des Sockets (in unserem Fall des Zählers) in verschiedenen Intervallen aufzurufen, wodurch es möglich wird, dass einige der sekundären Registerkarten primär werden und die Informationen dazu in localStorage schreiben. Wenn Sie in Zahlen (1, 1000) schamanisiert haben , können Sie die schnellste Antwort der Registerkarten erzielen. Die verbleibenden sekundären Registerkarten verbleiben, um Ereignisse abzuhören und darauf zu reagieren, und warten darauf, dass die primäre Registerkarte stirbt.
Zusammenfassung
Wir haben ein Design, mit dem Sie nur eine webSocket-Verbindung für die gesamte Anwendung beibehalten können, unabhängig von der Anzahl der Registerkarten. Dies reduziert die Hardware-Belastung unserer Server erheblich und ermöglicht es Ihnen, mehr online zu bleiben.