Node.js巧妙实现Web应用代码热更新

Node.js巧妙实现Web应用代码热更新
背景

学生们相信,使用Node.js开发Web应用程序必须通过新修改的代码必须在Node.js进程重启重启的问题。谁是用PHP开发的学生将不能申请,但可以肯定的是,我的大的PHP是世界上最好的编程语言。手动重新启动的过程,不仅是一个非常烦人的重复劳动,但当应用规模稍大,启动时间逐渐开始不可忽视。

当然,作为一个程序猿,无论使用哪种语言,它不会允许这样的事情来折磨自己。解决这些问题的最直接和最普遍的方法是监控文件的修改和重新启动的过程。这种方法也提供了很多成熟的解决方案,如节点主管已被抛弃,而较为流行的PM2,或轻node-dev.

本文提供了另一种思维方式。只有一个小的转变是需要实现真正的0启动的热点更新代码,解决恼人的代码更新的问题时,Node.js开发的Web应用程序。

总体思路

说到代码更新,今天最著名的部分是Erlang语言热更新的功能,其特点是高并发和分布式编程,和主要应用场景是类似于证券交易、游戏服务器等领域。这些场景都需要更多的服务操作维护或少。代码更新是一个非常重要的部分,所以我们首先可以了解Erlang的方式。

因为我没有使用Erlang,下面的内容是道听途说,如果您想深入和准确地了解Erlang代码的更新执行,更好地访问官方文档

代码加载二郎由模块命名code_server管理。大部分的代码加载的code_server除了在启动一些必要的代码。

当code_server发现模块代码更新,它会重新加载模块。之后,将用新模块执行新请求,而旧的执行请求将继续由旧模块执行。

在加载新模块后,旧模块将被标记为旧的,新模块是当前的标记。在当前热更新时,Erlang将扫描旧模块的执行并将其杀死,然后根据此逻辑继续更新模块。

不是所有的代码在Erlang允许热更新,如核,但是,编译器,并不得更新等基本模块

我们还可以发现有一个模块类似于code_server在Node.js,即要求系统,所以Erlang的做法也应该试试node.js.by了解Erlang的实践,我们可以总结Node.js解决代码热更新要点

如何更新模块代码

如何使用新模块处理请求

如何释放旧模块的资源

然后我们逐一分析这些要点。

如何更新模块代码

针对模块的代码更新的问题,我们需要阅读Node.js模块管理器的实现,直接关系到module.js.through简单的阅读,我们可以发现,核心代码模块。_load,一点点代码精简。
检查所请求的文件的缓存。
1。如果缓存中已经存在一个模块,则返回它的导出对象。
2。如果模块是原产: /电话` NativeModule.require(带),该
并返回结果。文件名
创建一个 3。否则,为文件创建新模块并将其保存到缓存中。
然后在返回输出之前加载文件/内容
对象。
模块。_load =功能(要求家长,主要){
var文件名=模块。_resolvefilename(要求家长);

无功cachedmodule =模块。_cache {文件};
如果(cachedmodule){
返回cachedmodule.exports;
}

var模块=新模块(文件名,父级);
模块。_cache { } =模块文件名;
module.load(文件名);

返回module.exports;
};

require.cache =模块。_cache;

可以发现,核心是_cache模块。只要模块缓存被清除,模块管理器将在下一次需要时重新加载最新的代码。

写一个小程序来验证
/ / main.js
函数中(模块){
VaR路径= require.resolve(模块);
需要;
}

setInterval(){()函数(
中('。 /代码。JS的);
var代码=要求(;
console.log(代码);
},5000);
/ / code.js
module.exports =你好世界;

当我们执行main.js改变code.js的内容,我们可以发现,我们的代码已经成功更新为最新的代码,在控制台。
然后,模块管理器更新代码的问题已经解决,然后看看如何使新模块在Web应用程序中真正执行。

如何使用新模块处理请求

为了更好地适应你的习惯,我们将以快递为例来解决这个问题。事实上,大多数Web应用程序都可以应用类似的思想。

首先,如果我们的服务像Express的demo一样,所有代码都在同一个模块中,所以我们不能加热模块。
VaR表示=需要('express);
var(=);

app.get(/功能(REQ,RES){
res.send('Hello World);
});

(3000)app.listen;

为了实现热装,这样是不允许在Erlang基地的图书馆,我们需要一些基本的代码控制更新过程,不能热更新。如果操作app.listen进行查询,它不是从重启Node.js过程非常不同。所以我们需要一些聪明的代码分开,经常更新业务代码不经常更新底层代码。
/ / app.js基础代码
VaR表示=需要('express);
var(=);
var路由器=需要(;

app.use(路由器);

(3000)app.listen;
/ / router.js业务代码
VaR表示=需要('express);
路由器(=路由器);

这个中间件也可以加载/自动更新
router.use(express.static(征收'));

router.get(/功能(REQ,RES){
res.send('Hello World);
});

module.exports =路由器;

不幸的是,在这个过程中,router.js仍无法使热更新,虽然核心代码已经成功分离。第一,因为一个更新的触发机制的缺乏,服务不知道什么时候更新模块。其次,app.use操作将保留旧的router.js模块所有的时间,所以即使模块更新,请求仍然会被旧的模块处理而不是新的模块。

为了进一步提高,我们需要做出细微调整app.js,触发机制的启动文件监控,并解决应用程序。通过关闭数据包使用的缓存问题。
/ / app.js
VaR表示=需要('express);
VaR FS =需要('fs);
var(=);

var路由器=需要(;

app.use(功能(REQ,RES,下){
/ /获取最新的路由器使用的封闭性,避免app.use路由器缓存对象
路由器(REQ,RES,下);
});

(3000)app.listen;

监视/修改/修改文件
Fs.watch(require.resolve('。 /路由器。js),函数(){)
中(require.resolve('。 /路由器。JS的));
{试
路由器=需要(;
} catch(前){
Console.error('module更新失败);
}
});

函数中(modulepath){
需要缓存{ modulepath } = null;
}

试图修改router.js会发现一直在塑造我们的代码热更新,而新的请求将使用最新的router.js代码。除了修改路由器的内容。JS的回报,你也可以尝试修改路由功能和预期更新。

当然,完美实现热更新方案,更应与改善我们自己的解决方案。首先,在中间件的使用,我们可以宣布一些中间件,不需要热更新或更新每次在app.use,但在router.use,我们可以宣布一些中间件,我们希望修改灵活其次,文件监控不仅能监控路由文件,但听所有的文件,需要热更新。除了文件监控,它还可以编辑扩展功能相结合,将信号发送到Node.js过程当储蓄,或访问一个特定的URL触发更新。

如何释放旧模块的资源

如何解释旧的模块资源释放的问题,其实需要了解Node.js的内存回收机制,本文不准备详细描述了Node.js,内存回收机制,解释了许多文章和书籍,有兴趣的同学可以扩大自己的阅读。一个简短的总结是,当一个对象不被任何对象的引用,它将被标记为可回收,这将在下一个GC处理释放内存。

然后我们的话题,是如何让老模块的代码更新以确保没有对象保持模块的参考。首先,让我们来看看如何更新模块中的代码段的代码,看看会发生什么当老模块资源不恢复,使结果更重要,我们修改code.js
/ / code.js
var数组{ };

对于(var i = 0;i < 10000;i + +){
Array.push('mem_leak_when_require_cache_clean_test_item_ +我);
}

module.exports =阵列;
/ / app.js
函数中(模块){
VaR路径= require.resolve(模块);
需要;
}

setInterval(){()函数(
var代码=要求(;
中('。 /代码。JS的);
},10);

好吧,我们用了一个很笨但很有效的方式提高了router.js模块的内存占用。main.js再次启动后,我们会发现记忆有明显的增加,Node.js会提示内存不足后的过程。然而,事实上,从router.js app.js和代码,我们没有找到的旧模块参考得救了。

随着一些轮廓工具如结堆转储的帮助下,我们可以快速定位问题。在module.js,我们发现Node.js会自动添加一个引用的所有模块。
功能模块(ID,父){
this.id = ID;
this.exports = { };
this.parent =母;
如果(父、母、子){
Parent.children.push(本);
}

this.filename = null;
this.loaded = false;
this.children = { };
}

因此,我们可以调整中功能和删除时的参考模块更新。
/ / app.js
函数中(modulepath){
VaR模块=需要缓存{ modulepath };
在module.parent / /参考删除
如果(模块父类){
Module.parent.children.splice(module.parent.children.indexof(模块),1);
}
需要缓存{ modulepath } = null;
}

setInterval(){()函数(
var代码=要求(;
中(require.resolve('。 /代码。JS的));
},10);

同样,这一次更好,内存只会稍微增加,这表明旧模块使用的资源已经正确释放。

新中的使用功能的问题,不经常使用,但它是不容易的担心。在Node.js,除了要求系统将添加引用,通过EventEmitter监控事件也是一种常见的功能。EventEmitter有很大的怀疑,会有模块之间相互引用,所以可以EventEmitter能够释放资源的正确吗答案是肯定的。
/ / code.js
var =需要('events)、EventEmitter();

ModuleA.on('whatever,函数(){(){
});

当code.js模块更新,所有引用都去掉,如果不是由其他模块、未引用,模块将自动释放,包括我们的内部事件监测。

只有一个畸形的EventEmitter的应用场景是无法应对这个系统,那就是当每个事件的执行将code.js听一个全局对象,这将导致全局对象停止安装事件,而Node.js将及时发现迅速在事件绑定,怀疑内存泄漏。

在这一点上,你可以看到只处理Node.js需要系统自动添加参考我们的旧模块资源回收并不是一个大问题,虽然我们不能做同样的一个旧的热更新模块下的Erlang也保留扫描这种细粒度的控制,但我们可以避免通过合理解决资源发布模块的老问题。

在Web应用程序中,还有另一个参考问题。未发行的模块或核心模块是参照要求热更新的模块,如app.use,导致旧的模块资源的衰竭,和新的要求,不能用新的模块正确处理。解决这个问题的办法是控制全局变量或参考曝光进入,并手动更新加入热更新执行过程。例如,如何使用新的模块,在请求路由器的封装过程为例。通过控制这一项,我们可以释放其他模块router.js无论如何,将公布入境的释放。

另一个问题,会造成资源释放类似setInterval运行。它将保持对象无法释放的生命周期。然而,在Web应用程序中,我们很少使用这种技术,因此该计划没有注意到它。
结束

到目前为止,我们已经解决了三个问题Node.js更新代码在高温下的Web应用程序,但由于缺乏对Node.js扫描保留对象的有效机制,因此无法消除100%大模块类似setInterval导致资源的问题不能被释放。这也是由于这样的局限性。目前,我们提供的yog2框架主要应用这一技术和调试阶段的发展,并通过热更新实现了快速发展。在生产环境的代码更新仍然使用重启或PM2热加载功能保证了在线服务的稳定性。

由于热更新实际上是对框架和业务体系结构密切相关,本文并没有给出一个通用的解决方案。作为参考,这是我们如何利用这一技术在yog2框架简介。由于yog2框架本身支持的前端和后端系统的应用程序分割,我们的更新策略是随着应用的粒度更新代码。在时间fs.watch喜欢这种操作同样会有兼容性问题,一些替代品,如fs.watchfile更消耗性能,所以我们结合yog2测试机部署功能,新的代码是通过上传通知框架需要更新应用程序代码和应用程序部署。粒度更新模块的缓存而,路由缓存和模板缓存将被更新,以完成所有代码的更新。

如果您使用的是类似的框架表达或膝关节骨性关节炎,你只需要重建的主要途径,根据方法和业务需求,所以你可以把这个技术很好。
免责声明:本网信息来自于互联网,目的在于传递更多信息,并不代表本网赞同其观点。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,并请自行核实相关内容。本站不承担此类作品侵权行为的直接责任及连带责任。如若本网有任何内容侵犯您的权益,请及时联系我们,本站将会在24小时内处理完毕。
相关文章
返回顶部