我们解决了一百万个打开标签或“帮助硬件生存”的问题


我们将尝试找出如何减轻服务器硬件的负担,同时确保最佳的Web应用程序性能。


在开发具有大量在线资源的大型高负载项目时,您通常必须考虑如何减少服务器上的负载,尤其是在使用WebSocket和动态更改接口时。 有100500个用户来找我们,我们有100500个开放式套接字连接。 如果它们每个都打开2个选项卡-这是* 201,000个连接。 如果五个?


考虑一个简单的例子。 例如,我们有 Twitch.tv ,这将为每个用户建立一个WS连接。 这样的项目在线上非常庞大,因此每个细节都很重要。 我们不能在每个选项卡上打开新的WS-connection,以支持旧的WS-connection,因为为此需要对腺体进行测量。


这个想法诞生了-如果仅在一个选项卡中取消WS连接并始终保持其打开状态,而在新连接中不初始化连接,而只是从相邻的选项卡中监听,该怎么办? 我要讲的是关于这个想法的实现。


浏览器逻辑行为


  1. 打开第一个标签,将其标记为“主要”
  2. 运行测试-如果使用is_primary选项卡,则提高WS连接
  3. 我们正在努力...
  4. 打开第二个标签(复制窗口,手动输入地址,在新标签中打开,没关系)
  5. 在新标签上,查看是否有主标签。 如果是,则标记当前的辅助节点并等待将发生的情况。
  6. 再打开10个标签。 每个人都在等待。
  7. 有时,“主要”选项卡关闭。 在她去世之前,她向所有人大声疾呼自己的死亡。 一切都震惊。
  8. 然后所有选项卡都试图立即成为“主要”选项。 每个人的反应都不同(随机),有时间的人,那个人和拖鞋。 一旦其中一个选项卡成功成为is_primary,她便向所有人大喊该地点被占用。 之后,WS连接重新进入自身。 我们正在工作。 其余的都在等待。
  9. 等等 清道夫正在等待“主要”选项卡的消失掉入其位置。

问题的技术方面


为了在选项卡之间进行通信,我们将使用在相同域-localStorage中将它们连接起来的东西。 调用它对于用户的铁资源并不昂贵,并且来自它们的响应非常快。 围绕它,整个想法正在建立。


创建者很长一段时间都没有支持一个 ,但是您可以像我一样将其设置为本地派生库。 从中我们得到文件:


/intercom.js


该库的本质在于,它允许使用此类的localStorage在各选项卡之间进行发射/打开事件的通信。


之后,我们需要一个工具,该工具可以锁定阻止更改 )localStorage中的某个键,而无需任何人在没有必要权限的情况下对其进行更改。 为此,编写了一个名为locableStorage ”的小型库,其实质包含在trySyncLock()函数中


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

现在有必要将所有内容组合成一个单一的机制,这将使我们能够执行计划。


实施代码
 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.'); } 

现在,我将用手指解释这里发生的事情。


GitHub示范项目


步骤1.打开第一个标签


本示例实现了一个计时器,该计时器以多种方式工作,但其计算仅发生一次。 可以将计时器代码替换为任何内容,例如,通过初始化WS连接。 启动时,将立即执行webSocketInit() ,这将在第一个选项卡中引导我们启动计数器( 打开套接字 ),并启动计时器startHeartBitInterval()更新本地存储中的键值“ wsLU ”。 此项负责创建和维护“主要”选项卡。 这是整个结构的关键要素。 同时,创建键“ wsOpen ”,该键负责计数器的状态(或打开WS连接),并且使当前选项卡为主的变量“ primaryStatus ”变为true。 对讲机收到的任何事件(WS连接)的接收将由对讲机发出,其设计如下:


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

步骤2.打开第二个选项卡


打开第二,第三和任何其他选项卡将导致webSocketInit() ,此后,键“ wsLU ”和“ forceOpen ”进入战斗。 如果代码:


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

...将使forceOpen变为true ,然后计数器将停止并再次启动,但这不会发生,因为 diff不会大于指定的值,因为当前的“主要”选项卡支持wsLU键。 所有“次要”选项卡将通过以下方式侦听“主要”选项卡通过对讲机提供给他们的事件:


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

步骤3.关闭标签


关闭选项卡在现代浏览器中触发onbeforeunload事件。 我们对其进行如下处理:


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

应该注意的是,所有方法将仅在“主要”选项卡中被调用。 关闭任何“辅助”选项卡时,计数器将不会发生任何事情。 您只需要删除事件的监听即可释放内存。 但是,如果关闭“主要”选项卡,则将wsOpen设置false并触发TAB_CLOSED事件。 所有打开的标签页都会立即对其做出响应:


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

这就是魔术开始的地方。 功能...


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

...允许您以不同的时间间隔调用套接字的初始化(在我们的示例中为计数器),这使某些“辅助”选项卡成为“主要”并将其信息写入localStorage成为可能。 用数字 1,1000)进行萨满化处理可以使选项卡的响应速度最快。 其余的“次要”选项卡将保留以侦听事件并对其进行响应,等待主要事件消失。


总结


我们的设计允许您为整个应用程序保留一个webSocket连接,无论它具有多少个选项卡,这都将大大减少服务器硬件的负载,从而使您保持更多的联机状态。

Source: https://habr.com/ru/post/zh-CN415401/


All Articles