
Nous allons essayer de comprendre comment réduire la charge sur le matériel du serveur, tout en garantissant les performances maximales des applications Web.
Lors du développement de projets volumineux et à forte charge avec un énorme volume en ligne, vous devez souvent réfléchir à la façon de réduire la charge sur le serveur, en particulier lorsque vous travaillez dans des webSockets et que vous modifiez dynamiquement les interfaces. 100500 utilisateurs viennent à nous et nous avons 100500 connexions socket ouvertes. Et si chacun d'eux ouvre 2 onglets - c'est * 201 000 connexions. Et si cinq?
Prenons un exemple trivial. Nous avons, par exemple, Twitch.tv , qui, pour chaque utilisateur, établit une connexion WS. Un tel projet est énorme en ligne, donc chaque détail est important. Nous ne pouvons pas nous permettre d'ouvrir une nouvelle connexion WS sur chaque onglet, prenant en charge l'ancien, car le presse-étoupe doit être non mesuré pour cela.
L'idée est née - que se passe-t-il si les connexions WS sont établies dans un seul onglet et qu'elles restent toujours ouvertes, et dans les nouvelles, n'initialisez pas la connexion, mais écoutez simplement à partir de l'onglet voisin? C'est à propos de la mise en œuvre de cette idée que je veux dire.
Comportement logique du navigateur
- Ouvrez le premier onglet, marquez-le comme principal
- Exécutez le test - si l'onglet is_primary, puis augmentez la connexion WS
- Nous travaillons ...
- Ouvrez le deuxième onglet (dupliquez la fenêtre, entrez l'adresse manuellement, ouvrez dans un nouvel onglet, cela n'a pas d'importance)
- Dans le nouvel onglet, voyez s'il y a quelque part un onglet principal. Si oui, marquez le secondaire actuel et attendez ce qui se passera.
- Ouvrez 10 autres onglets. Et tout le monde attend.
- À un moment donné, l'onglet Principal se ferme. Avant sa mort, elle crie à tout le monde sa mort. Tout est sous le choc.
- Et puis tous les onglets essaient de devenir instantanément primaire. La réaction de chacun est différente (aléatoire) et celui qui a le temps, ça et les pantoufles. Dès que l'un des onglets a réussi à devenir is_primary, elle crie à tout le monde que la place est prise. Après cela, la connexion WS se reconnecte. Nous travaillons. Les autres attendent.
- Etc. Les charognards attendent que la mort de l'onglet Primaire tombe à sa place.
Le côté technique du problème
Pour communiquer entre les onglets, nous utiliserons ce qui les relie au sein du même domaine - localStorage. Les appels ne sont pas chers pour les ressources en fer de l'utilisateur et leur réponse est très rapide. Autour d'elle, toute l'idée se construit.
Il existe une bibliothèque qui n'a pas été prise en charge par le créateur depuis longtemps, mais vous pouvez en faire un fork local, comme je l'ai fait. De là, nous obtenons le fichier:
/intercom.js
L'essence de la bibliothèque est qu'elle permet aux événements emit / on de communiquer entre les onglets en utilisant localStorage pour cela.
Après cela, nous avons besoin d'un outil qui nous permet de verrouiller ( bloquer les modifications ) une certaine clé dans localStorage, sans permettre à personne de la modifier sans les droits nécessaires. Pour cela, une petite bibliothèque appelée " locableStorage " a été écrite, dont l'essentiel est contenu dans la fonction trySyncLock ()
Code de bibliothèque 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) } }; })();
Maintenant, il est nécessaire de tout combiner en un seul mécanisme, ce qui nous permettra de mettre en œuvre nos plans.
Code d'implémentation if (Intercom.supported) { let intercom = Intercom.getInstance(),
Maintenant, sur les doigts, je vais expliquer ce qui se passe ici.
Projet de démonstration GitHub
Étape 1. Ouverture du premier onglet
Cet exemple implémente un temporisateur fonctionnant dans plusieurs contributions, mais dont le calcul se produit dans une seule. Le code du temporisateur peut être remplacé par n'importe quoi, par exemple, en initialisant la connexion WS. au démarrage, webSocketInit () est immédiatement exécuté, ce qui dans le premier onglet nous amènera à démarrer le compteur ( ouvrir le socket ), ainsi qu'à démarrer le timer startHeartBitInterval () pour mettre à jour la valeur de clé " wsLU " dans localStorage. Cette clé est responsable de la création et de la maintenance de l'onglet Principal. Il s'agit d'un élément clé de toute la structure. Dans le même temps, la clé wsOpen est créée, qui est responsable de l'état du compteur (ou de l'ouverture d'une connexion WS) et la variable primaryStatus , qui rend l'onglet actuel principal, devient vraie. La réception de tout événement du compteur (connexion WS) sera émise par Intercom, avec la conception:
intercom.emit('incoming', {data: count});
Étape 2. Ouverture du deuxième onglet
L'ouverture du deuxième, du troisième et de tout autre onglet entraînera webSocketInit () , après quoi les clés " wsLU " et " forceOpen " entreront en bataille. Si le code:
if (wsLU) { let diff = Date.now() - parseInt(wsLU); forceOpen = diff > period_heart_bit * 5 * 1000; }
... fera que forceOpen devienne vrai , puis le compteur s'arrêtera et redémarrera, mais cela ne se produira pas, car diff ne sera pas supérieur à la valeur spécifiée, car la clé wsLU est prise en charge par l'onglet principal actuel. Tous les onglets secondaires écouteront les événements que l'onglet principal leur donne via Intercom, avec la conception:
intercom.on('incoming', data => { document.getElementById('counter').innerHTML = data.data; document.getElementById('socketStatus').innerHTML = primaryStatus.toString(); return false; });
Étape 3. Fermeture de l'onglet
La fermeture des onglets déclenche l'événement onbeforeunload dans les navigateurs modernes. Nous le traitons comme suit:
window.onbeforeunload = function () { if (primaryStatus) { localStorage.setItem('wsOpen', false); clearInterval(refreshIntervalId); intercom.emit('TAB_CLOSED', {count: count}); } };
Il convient de noter que toutes les méthodes seront appelées uniquement dans l'onglet Principal. Lorsque vous fermez un onglet secondaire, rien n’arrivera au compteur. Il vous suffit de supprimer l'écoute électronique des événements pour libérer de la mémoire. Mais si nous fermons l'onglet Primaire, nous définissons wsOpen sur false et déclenchons l'événement TAB_CLOSED. Tous les onglets ouverts y répondront immédiatement:
intercom.on('TAB_CLOSED', function (data) { if (localStorage.wsId !== wsId) { count = data.count; setTimeout(() => { webSocketInit() }, parseInt(getRandomArbitary(1, 1000), 10));
C'est là que la magie commence. Fonction ...
getRandomArbitary (1, 1000) function getRandomArbitary(min, max) { return Math.random() * (max - min) + min; }
... vous permet d'appeler l'initialisation du socket (dans notre cas, le compteur) à différents intervalles, ce qui permet à certains des onglets secondaires de devenir primaires et d'écrire les informations à ce sujet dans localStorage. Après avoir chamanisé en nombre (1, 1000), vous pouvez obtenir la réponse la plus rapide des onglets. Les onglets secondaires restants restent pour écouter les événements et y répondre, en attendant que le primaire meure.
Résumé
Nous avons une conception qui vous permet de ne conserver qu'une seule connexion webSocket pour l'ensemble de l'application, quel que soit le nombre d'onglets dont elle dispose, ce qui réduira considérablement la charge sur le matériel de nos serveurs et, par conséquent, vous permettra de garder plus en ligne.