
Intentaremos descubrir cómo reducir la carga en el hardware del servidor, mientras aseguramos el máximo rendimiento de la aplicación web.
En el desarrollo de proyectos grandes y de alta carga con gran en línea, a menudo tiene que pensar en cómo reducir la carga en el servidor, especialmente cuando trabaja en webSockets y cambia dinámicamente las interfaces. 100500 usuarios vienen a nosotros y tenemos 100500 conexiones de socket abierto. Y si cada uno de ellos abre 2 pestañas, esto es * 201,000 conexiones. ¿Y si son cinco?
Considere un ejemplo trivial. Tenemos, por ejemplo, Twitch.tv , que para cada usuario genera una conexión WS. Tal proyecto es enorme en línea, por lo que cada detalle es importante. No podemos permitirnos abrir una nueva conexión WS en cada pestaña, admitiendo la anterior, porque la glándula no debe estar medida para esto.
La idea nace: ¿qué pasa si las conexiones WS se levantan en una sola pestaña y siempre se mantienen abiertas, y en las nuevas no se inicializa la conexión, sino que se escucha desde la pestaña vecina? Se trata de la implementación de esta idea que quiero contar.
Comportamiento lógico del navegador
- Abra la primera pestaña, márquela como Primaria
- Ejecute la prueba: si la pestaña es_primaria, levante la conexión WS
- Estamos trabajando ...
- Abra la segunda pestaña (duplique la ventana, ingrese la dirección manualmente, ábrala en una nueva pestaña, no importa)
- Desde la nueva pestaña, vea si hay en algún lugar una pestaña Principal. En caso afirmativo, marque el Secundario actual y espere lo que sucederá.
- Abre 10 pestañas más. Y todos están esperando.
- En algún momento, la pestaña Principal se cierra. Antes de su muerte, ella grita a todos acerca de su muerte. Todo está en shock.
- Y luego todas las pestañas están tratando de convertirse instantáneamente en Primarias. La reacción de todos es diferente (al azar) y quien tenga tiempo, eso y zapatillas. Tan pronto como una de las pestañas logró convertirse en primaria, grita a todos que el lugar está ocupado. Después de eso, la conexión WS vuelve a entrar. Estamos trabajando El resto está esperando.
- Etc. Los carroñeros esperan que la muerte de la pestaña Primaria caiga en su lugar.
El aspecto técnico del problema.
Para comunicarnos entre las pestañas, utilizaremos lo que las conecta dentro del mismo dominio: localStorage. Las llamadas no son caras para los recursos de hierro del usuario y la respuesta de ellos es muy rápida. A su alrededor, se está construyendo toda la idea.
Hay una biblioteca que no ha sido compatible con el creador durante mucho tiempo, pero puede convertirla en una bifurcación local, como lo hice yo. De allí obtenemos el archivo:
/intercom.js
La esencia de la biblioteca es que permite que los eventos de emisión / encendido se comuniquen entre pestañas usando localStorage para esto.
Después de eso, necesitamos una herramienta que nos permita bloquear ( bloquear cambios ) una determinada clave en localStorage, sin permitir que nadie la cambie sin los derechos necesarios. Para esto, se escribió una pequeña biblioteca llamada " locableStorage ", cuya esencia está contenida en la función trySyncLock ()
Código de biblioteca 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) } }; })();
Ahora es necesario combinar todo en un solo mecanismo, lo que nos permitirá implementar nuestros planes.
Código de implementación if (Intercom.supported) { let intercom = Intercom.getInstance(),
Ahora con los dedos explicaré lo que está sucediendo aquí.
Proyecto de demostración de GitHub
Paso 1. Abrir la primera pestaña
Este ejemplo implementa un temporizador que funciona en varias contribuciones, pero cuyo cálculo se produce solo en una. El código del temporizador se puede reemplazar con cualquier cosa, por ejemplo, inicializando la conexión WS. cuando se inicia, webSocketInit () se ejecuta inmediatamente, lo que en la primera pestaña nos llevará a iniciar el contador ( abrir el socket ), así como a iniciar el temporizador startHeartBitInterval () para actualizar el valor clave " wsLU " en localStorage. Esta clave es responsable de la creación y el mantenimiento de la pestaña Principal. Este es un elemento clave de toda la estructura. Al mismo tiempo, se crea la clave wsOpen , que es responsable del estado del contador (o de abrir una conexión WS) y la variable primaryStatus , que hace que la pestaña actual sea principal, se convierte en verdadera. Intercom emitirá la recepción de cualquier evento del contador (conexión WS) con el diseño:
intercom.emit('incoming', {data: count});
Paso 2. Abrir la segunda pestaña
Abrir la segunda, tercera y cualquier otra pestaña hará que webSocketInit () , después de lo cual la tecla " wsLU " y " forceOpen " entren en batalla. Si el código:
if (wsLU) { let diff = Date.now() - parseInt(wsLU); forceOpen = diff > period_heart_bit * 5 * 1000; }
... hará que forceOpen se vuelva verdadero , luego el contador se detendrá y comenzará de nuevo, pero esto no sucederá, porque diff no será mayor que el valor especificado, porque la clave wsLU es compatible con la pestaña principal actual. Todas las pestañas secundarias escucharán los eventos que la pestaña Primaria les brinda a través de Intercom, con el diseño:
intercom.on('incoming', data => { document.getElementById('counter').innerHTML = data.data; document.getElementById('socketStatus').innerHTML = primaryStatus.toString(); return false; });
Paso 3. Cerrar la pestaña
Cerrar pestañas activa el evento onbeforeunload en los navegadores modernos. Lo procesamos de la siguiente manera:
window.onbeforeunload = function () { if (primaryStatus) { localStorage.setItem('wsOpen', false); clearInterval(refreshIntervalId); intercom.emit('TAB_CLOSED', {count: count}); } };
Cabe señalar que todos los métodos se invocarán solo en la pestaña Principal. Cuando cierre cualquier pestaña secundaria, no pasará nada al contador. Solo necesita eliminar las escuchas telefónicas de los eventos para liberar memoria. Pero si cerramos la pestaña Principal, establecemos wsOpen en falso y activamos el evento TAB_CLOSED. Todas las pestañas abiertas responderán de inmediato:
intercom.on('TAB_CLOSED', function (data) { if (localStorage.wsId !== wsId) { count = data.count; setTimeout(() => { webSocketInit() }, parseInt(getRandomArbitary(1, 1000), 10));
Aquí es donde comienza la magia. Función ...
getRandomArbitary (1, 1000) function getRandomArbitary(min, max) { return Math.random() * (max - min) + min; }
... le permite llamar a la inicialización del socket (en nuestro caso, el contador) a diferentes intervalos, lo que hace posible que una de las pestañas secundarias se convierta en primaria y escriba la información al respecto en localStorage. Habiendo chamanizado en números (1, 1000), puede lograr la respuesta más rápida de las pestañas. Las pestañas secundarias restantes permanecen para escuchar los eventos y responder a ellos, esperando que la primaria muera.
Resumen
Tenemos un diseño que le permite mantener solo una conexión webSocket para toda la aplicación, sin importar cuántas pestañas tenga, lo que reducirá significativamente la carga en el hardware de nuestros servidores y, como resultado, le permitirá mantener más en línea.