
Vamos tentar descobrir como reduzir a carga no hardware do servidor, garantindo o desempenho máximo dos aplicativos da Web.
No desenvolvimento de projetos grandes e de alta carga com grande disponibilidade on-line, você geralmente precisa pensar em como reduzir a carga no servidor, especialmente ao trabalhar no webSockets e mudar dinamicamente as interfaces. 100500 usuários vêm até nós e temos 100500 conexões de soquete aberto. E se cada um deles abrir 2 guias - isso é * 201.000 conexões. E se cinco?
Considere um exemplo trivial. Temos, por exemplo, Twitch.tv , que para cada usuário gera uma conexão WS. Esse projeto é enorme online, portanto, todos os detalhes são importantes. Não podemos nos dar ao luxo de abrir uma nova conexão WS em cada guia, suportando a antiga, porque a glândula precisa ser incomensurável para isso.
Nasce a idéia - e se as conexões WS forem levantadas em apenas uma guia e sempre a mantiverem abertas, e nas novas não inicializarem a conexão, mas apenas ouvir na guia vizinha? É sobre a implementação dessa idéia que eu quero contar.
Comportamento lógico do navegador
- Abra a primeira guia, marque-a como Principal
- Execute o teste - se a guia is_primary, aumente a conexão WS
- Estamos trabalhando ...
- Abra a segunda guia (duplique a janela, digite o endereço manualmente, abra em uma nova guia, não importa)
- Na nova guia, verifique se há em algum lugar uma guia Principal. Se sim, marque o secundário atual e aguarde o que acontecerá.
- Abra mais 10 guias. E todo mundo está esperando.
- Em algum momento, a guia Primário é fechada. Antes de sua morte, ela grita a todos sobre sua morte. Tudo está em choque.
- E então todas as guias estão tentando se tornar instantaneamente Primárias. A reação de todos é diferente (aleatória) e quem tem tempo, isso e chinelos. Assim que uma das abas conseguiu se tornar primária, ela grita para todos que o lugar está ocupado. Depois disso, a conexão do WS entra novamente. Nós estamos trabalhando. O resto está esperando.
- Etc. Os catadores estão esperando a morte da guia Primária cair em seu lugar.
O lado técnico da questão
Para se comunicar entre as guias, usaremos o que as conecta no mesmo domínio - localStorage. As chamadas para ele não são caras para os recursos de ferro do usuário e a resposta é muito rápida. Em torno disso, toda a idéia está sendo construída.
Há uma biblioteca que não é suportada pelo criador há muito tempo, mas você pode torná-la um fork local, como eu fiz. A partir dele, obtemos o arquivo:
/intercom.js
A essência da biblioteca é que ela permite que eventos de emissão / ativação se comuniquem entre guias usando localStorage para isso.
Depois disso, precisamos de uma ferramenta que permita bloquear ( bloquear alterações ) uma determinada chave no localStorage, sem permitir que ninguém a altere sem os direitos necessários. Para isso, uma pequena biblioteca chamada " locableStorage " foi escrita, cuja essência está contida na função trySyncLock ()
Código da 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) } }; })();
Agora é necessário combinar tudo em um único mecanismo, o que nos permitirá implementar nossos planos.
Código de implementação if (Intercom.supported) { let intercom = Intercom.getInstance(),
Agora, nos dedos, explicarei o que está acontecendo aqui.
Projeto de demonstração do GitHub
Etapa 1. Abrindo a primeira guia
Este exemplo implementa um cronômetro trabalhando em várias contribuições, mas o cálculo ocorre em apenas uma. O código do timer pode ser substituído por qualquer coisa, por exemplo, inicializando a conexão WS. Quando iniciado, o webSocketInit () é executado imediatamente, o que na primeira guia nos leva a iniciar o contador ( abrir o soquete ) e a iniciar o timer startHeartBitInterval () para atualizar o valor da chave " wsLU " em localStorage. Essa chave é responsável pela criação e manutenção da guia Primária. Este é um elemento chave de toda a estrutura. Ao mesmo tempo, a chave wsOpen é criada, responsável pelo status do contador (ou abrir uma conexão WS) e a variável primaryStatus , que torna a guia atual principal, se torna verdadeira. O recebimento de qualquer evento do contador (conexão WS) será emitido pelo Intercom, com o design:
intercom.emit('incoming', {data: count});
Etapa 2. Abrindo a segunda guia
Abrir a segunda, terceira e qualquer outra guia fará com que webSocketInit () , após o qual a chave " wsLU " e " forceOpen " entrem em batalha. Se o código:
if (wsLU) { let diff = Date.now() - parseInt(wsLU); forceOpen = diff > period_heart_bit * 5 * 1000; }
... fará com que forceOpen se torne verdadeiro , o contador irá parar e iniciar novamente, mas isso não acontecerá, porque diff não será maior que o valor especificado, porque a chave wsLU é suportada pela guia Primária atual. Todas as guias secundárias ouvirão os eventos que a guia Primária lhes fornecer através do Intercom, com o design:
intercom.on('incoming', data => { document.getElementById('counter').innerHTML = data.data; document.getElementById('socketStatus').innerHTML = primaryStatus.toString(); return false; });
Etapa 3. Fechando a guia
Fechar guias aciona o evento onbeforeunload nos navegadores modernos. Nós processamos da seguinte maneira:
window.onbeforeunload = function () { if (primaryStatus) { localStorage.setItem('wsOpen', false); clearInterval(refreshIntervalId); intercom.emit('TAB_CLOSED', {count: count}); } };
Observe que todos os métodos serão chamados apenas na guia Primário. Quando você fecha qualquer guia Secundário, nada acontece com o contador. Você só precisa remover a escuta de eventos para liberar memória. Mas se fecharmos a guia Primário, definiremos wsOpen como false e acionar o evento TAB_CLOSED. Todas as guias abertas responderão imediatamente a ela:
intercom.on('TAB_CLOSED', function (data) { if (localStorage.wsId !== wsId) { count = data.count; setTimeout(() => { webSocketInit() }, parseInt(getRandomArbitary(1, 1000), 10));
É aqui que a mágica começa. Função ...
getRandomArbitary (1, 1000) function getRandomArbitary(min, max) { return Math.random() * (max - min) + min; }
... permite chamar a inicialização do soquete (no nosso caso, o contador) em diferentes intervalos, o que possibilita que algumas das guias secundárias se tornem Primárias e grave as informações sobre isso no localStorage. Tendo shamanized em números (1, 1000), você pode obter a resposta mais rápida das guias. As guias secundárias restantes permanecem para ouvir os eventos e respondê-los, esperando a Primária morrer.
Sumário
Temos um design que permite manter apenas uma conexão webSocket para todo o aplicativo, independentemente de quantas guias ele tiver, o que reduzirá significativamente a carga no hardware de nossos servidores e, como resultado, permitirá que você fique mais on-line.