php开发典型模块大全光盘Node.js应用的热更新是不是就完美无缺了呢?我们接着看典型模块与项目实战大全光盘
2022-07-16
简介:记得2015年和16年Node.js刚起步的时候,在前雇主面试的时候,也被问到如何实现Node.js服务的热更新。
记得 2015 年和 16 年 Node.js 刚起步的时候,我在前雇主的面试中也被问到如何实现 Node.js 服务的热更新。
其实早期从 Php-fpm / Fast-cgi 转过来的人,肯定很喜欢这种不用重启服务器就能更新业务逻辑代码的部署方案。它的优点也很明显:
热更新还有很多副作用,比如常见的内存泄漏(资源泄漏)。本文将使用-和这两个下载量比较高的热门热更新辅助模块来讨论热更新会给我们的应用带来什么。问题。
热置换原理
在开始说热更新之前,我们首先要了解一下Node.js的模块机制的概况网站建设,这样才能对它后面带来的问题有更深入的了解和理解。
Node.js自身实现的模块加载机制如下图所示:
简单来说,父模块A引入子模块B的步骤如下:
其实到这一步,我们已经可以发现,要实现不泄露内存的热更新,需要断开待热更新模块的以下参考链接:
这样,当我们再次去子模块B时,会再次从磁盘中读取模块B的内容,然后编译导入内存,从而实现热更新的能力。
其实第一节提到的-和两个包都是基于这个思想的模块。当然,它们会被认为更完美,例如清除子模块 B 本身的依赖关系,以及用于处理循环引用的场景。
那么,在这两个模块的帮助下,Node.js 应用的热更新就完美了吗?让我们来看看。
问题一:内存泄漏
内存泄漏是一个非常有趣的问题。所有进入Node.js全栈开发深水区的同学,基本都会遇到内存泄露的问题。以我个人的排查和定位经验来看,开发者不需要担心内存泄漏,因为相比其他晦涩难懂的问题,内存泄漏是一种故障类型,只要熟悉代码,100%可以解决慢慢来。
在这里,我们来看看似乎清除所有旧模块引用的热更新解决方案,以及会发生什么形式的内存泄漏。
考虑构建以下修补程序示例,首先使用它进行测试:
'利用';
= ('');
让 mod = ('./.js');
模组();
模组();
(() => {
('./.js');
mod = ('./.js');
模组();
}, 100);
在这个例子中php开发典型模块大全光盘,相当于不断的清空 ./.js 模块的缓存进行热更新。其内容如下:
'利用';
= 新 (10e5).fill('*');
让 = 0;
. = () => {
.log('', ++, .);
};
为了快速观察内存泄漏现象,这里构造了一个大数组来代替常规的模块闭包引用。
为了方便观察,我们可以在.js中添加一个方法,定时打印当前内存状态:
() {
{ rss, } = .();
.log(`rss: ${(rss / 1024 / 1024).(2)}MB, : ${( / 1024 / 1024).(2)} MB`);
}
();
(, 1000);
最后执行node.js文件,可以看到内存很快溢出:
1
2
RSS: 34.59MB, : 11.51MB
1
RSS:110.20MB,:80.09MB
1
RSS:921.63MB,:888.99MB
1
RSS:998.09MB,:965.12MB
1
1
[:] 毫秒:1018.3 (1024.6) -> 1018.3 (1028.6) MB, 2.3 / 0.0 毫秒 ( mu = 0.783, mu = 0.576)
[:] 毫秒:马克- () 1026.0 (1036.3) -> 1025.9 (1029.3) MB , 457.8 / 0.0 ms (+ 86.6 ms in 77 of , step 8.7 ms, of 555 ms) ( mu = 0.@ >670,亩 = 0.360
: heap - 堆出
拍摄堆快照后的分析:
很明显,大量重复的热更新模块被塞进了@的数组中。js编译结果导致内存泄漏,进一步查看@信息:
您可以看到它是条目.js。
看了实现源码,发现泄露的原因是我们需要把热更新实现原理一节中提到的三个引用全部去掉,可惜只有最基本的还是断开了。此参考链接:
至此,由于最基本的热更新内存问题还没有解决,94w的月下载量已经被蒙蔽了,我们可以直接排除我们的热更新解决方案以供参考。
参考:
-
接下来,我们来看看19w的月下载量——它的表现如何。
由于上一节的测试代码代表了最基本的模块热改场景,而且用法基本相同,我们只需替换引用即可进行这一轮测试:
//.js
= ('-');
同样执行node.js文件,可以看到内存变化如下:
1
2
RSS: 35.00MB, : 11.58MB
1
RSS:110.69MB,:80.10MB
1
RSS:187.36MB网站开发,:156.52MB
1
RSS:256.28MB,:225.26MB
1
RSS: 332.78MB, : 301.71MB
1
RSS:401.61MB,:370.38MB
1
RSS:42.67MB,:11.17MB
1
RSS:65.63MB,:34.15MB
1
这里可以发现- 趋势呈波浪状,说明完美处理了原理部分提到的旧模块的所有引用,使得热更新前的旧模块可以正常GCed。
查看源码后发现,父模块对子模块的引用也被清除了:
因此,在这个例子中,热量不会导致进程内存泄漏OOM。
详细代码请参考:#L25-L31
所以你不觉得 - 你可以高枕无忧而不必担心记忆?
其实不然,我们再对上面的.js做一些小修改:
'利用';
= ('-');
让 mod = ('./.js');
模组();
模组();
('./.js');
(() => {
('./.js');
mod = ('./.js');
模组();
}, 100);
和之前加个.js相比,它的逻辑还是挺简单的:
'利用';
('./.js')
(() => ('./.js'), 100);
对应的场景其实是在.js中清理了.js之后,同样使用的模块的.js也被重新引入,以保持使用最新的热更新模块逻辑。
继续执行node.js文件,可以看到这次内存又快速溢出了:
1
2
RSS: 34.59MB, : 11.51MB
1
RSS:110.20MB,:80.09MB
1
RSS:921.63MB,:888.99MB
1
RSS:998.09MB,:965.12MB
1
1
[:] 毫秒:1018.5 (1025.1) -> 1018.5 (1029.1) MB, 2.2 / 0.0 毫秒 (mu = 0.785, mu = 0.635)
[:] 毫秒:马克- () 1026.1 (1036.8) -> 1025.9 (1029.3) MB , 462.2 / 0.0 ms (+ 87.7 ms in 89, step 7.5 ms, of 559 ms) ( mu = 0.@ >667,亩 = 0.296
: heap - 堆出
继续抓取堆快照进行分析:
这次@数组下有大量重复的热更新模块。js导致内存泄漏。我们来看看@的细节:
是不是很奇怪——明明父模块对热更新子模块的引用已经清理干净了(本例中是.js的父模块),但是.js中还保留了这么多旧的引用?
其实这是因为,在 Node.js 的模块实现机制中,子模块和父模块其实是多对多的关系,而且因为模块缓存机制,子模块只有在是第一次加载。构造函数初始化:
这意味着在--中所谓的去除热更新模块的旧引用的父模块只是第一次引入热更新模块对应的父模块,在这种情况下。js,所以.js对应的数组是干净的。
当.js作为父模块引入热更新模块时,读取热更新模块最新版本的缓存,更新引用:
它会判断数组中不存在缓存的对象并添加进去。显然,热更新前后两次编译.js得到的内存对象是不一样的,所以.js中存在泄漏。
至此,在稍微复杂一点的逻辑下,我也被打败了。考虑到实际开发中的逻辑负载会比这个高很多,很明显在生产中使用热更新,除非作者对模块机制有彻底的把控。为子孙后代挖坑。
留下一个有趣的想法: - 这种情况下的泄漏并非无法解决。有兴趣的同学可以参考原理思考一下如何避免这种场景下的热更新内存泄漏。
参考:
有的同学可能会觉得上面的例子不够典型。我们来看一个由于热更新导致开发者完全无法控制的非幂等子依赖模块重复加载导致的内存泄漏案例。
在这里,我们不会为了构造内存泄漏而特意寻找非常部分的包。我们以一个很常见的工具模块为例,每周下载量很高,继续修改我们的.js:
'利用';
= ('');
让 = 0;
. = () => {
.log('', ++);
};
然后从 .js 中删除上面的 .js 并只对 .js 重复热更新:
'利用';
= ('-');
让 mod = ('./.js');
模组();
模组();
(() => {
('./.js');
mod = ('./.js');
模组();
}, 10);
() {
{ rss, } = .();
.log(`rss: ${(rss / 1024 / 1024).(2)}MB, : ${( / 1024 / 1024).(2)} MB`);
}
();
(, 1000);
然后执行node.js文件,可以看到这次又泄露了。随着.js的热更新,堆内存迅速增加,最终OOM。
在这种情况下,非幂等子模块泄漏的原因稍微复杂一些,涉及到模块的反复编译和执行,会导致闭包循环引用。
其实会发现模块的引入对于开发者来说是不可控的。也就是说,开发者无法确认自己是否导入了可以幂等执行的公共模块。导致它产生内存泄漏。
问题二:资源泄露
讲完了内存问题场景,更可能是热量造成的,我们再来看看另外一种比较难解决的,热量更容易造成的资源泄漏问题。
我们还是用一个简单的例子来说明,首先构造.js:
'利用';
= ('-');
让 mod = ('./.js');
(() => {
('./.js');
mod = ('./.js');
.log('---------热更新结束---------')
}, 1000);
这次我们直接使用-来进行热更新操作,引入要热更新的模块。js如下:
'利用';
= 新日期()。();
(() => .log(), 1000);
在 .js 中,我们创建了一个定时任务,它以 1 秒的间隔输出模块首次引入的时间。
最后执行node.jsphp开发典型模块大全光盘,可以看到如下结果:
2022/1/21 上午 9:37:29
-------- 热更新结束---------
2022/1/21 上午 9:37:29
2022/1/21 上午 9:37:30
-------- 热更新结束---------
2022/1/21 上午 9:37:29
2022/1/21 上午 9:37:30
2022/1/21 上午 9:37:31
-------- 热更新结束---------
2022/1/21 上午 9:37:29
2022/1/21 上午 9:37:30
2022/1/21 上午 9:37:31
2022/1/21 上午 9:37:32
-------- 热更新结束---------
2022/1/21 上午 9:37:29
2022/1/21 上午 9:37:30
2022/1/21 上午 9:37:31
2022/1/21 上午 9:37:32
2022/1/21 上午 9:37:33
-------- 热更新结束---------
2022/1/21 上午 9:37:29
2022/1/21 上午 9:37:30
2022/1/21 上午 9:37:31
2022/1/21 上午 9:37:32
2022/1/21 上午 9:37:33
2022/1/21 上午 9:37:34
显然,虽然热替换模块的旧引用被正确清除,但旧模块内部的定时任务并没有一起回收,导致资源泄漏。
其实这里的定时任务只是资源之一。在只清除旧模块引用的场景下,包括FD、FD在内的各种系统资源操作无法自动回收。
问题3:ESM喵喵喵?
不管是还是-,都是基于Node.js实现的模块机制的比较热的逻辑集成。
但是,整个前端发展到今天。原生ECMA规范定义的模块机制是(简称ESM)。因为是规范定义的,所以它的实现是引擎层面的,Node.js对应的层是V8实现的。因此,当前的热量更不能作用于 ESM 模块。
但是在我看来,基于热度比较多因为是在上层实现的,会隐藏各种坑,所以不建议在生产中使用,但是基于ESM的热度比较多,如果规范可以定义一个完整的模块加载和卸载机制,将是真正的热刷新解决方案的未来。
Node.js 在这方面也有相应的实验特性可以使用。有关详细信息,请参阅:ESM。(#) 不过目前只处于:1的状态,需要继续观望。
问题四:模块版本混淆
Node.js的热更新其实并不是很多同学想象的那种全局旧模块替换,因为缓存机制可能会导致多个不同版本的热更新模块同时存在于内存中,导致一些奇怪难以定位的错误。.
我们继续构建一个小例子来说明,首先将模块写成hot-.js:
'利用';
='v1';
. = () => {
;
};
然后添加一个 .js 来正常使用这个模块:
'利用';
mod = ('./.js');
(() => .log('', mod()), 1000);
然后编写启动.js进行热更新操作:
'利用';
= ('-');
让 mod = ('./.js');
('./.js');
(() => {
('./.js');
mod = ('./.js');
.log('', mod())
}, 1000);
此时,当我们在不更改 .js 的情况下执行 node.js 时,我们可以看到:
v1
v1
v1
v1
注意内存中的 .js 都是 v1 版本。
刚才不需要重启服务,我们修改.js中的:
//.js
='v2';
然后观察输出变为:
v1
v1
v2
v1
v2
v1
.js 是热更新的,所以它重新到达的 .js 成为最新的 v2 版本,.js 没有任何变化。
这样一个模块多个版本的情况,不仅增加了在线故障定位的难度,也在一定程度上造成了内存泄漏。
适用于热更新场景
抛开现场谈问题是流氓。虽然热更新有这么多问题,但是模块热更新确实有一些使用场景。我们将从线上和线下两个维度进行讨论。
对于离线场景,较小的内存和资源泄漏问题可以让位于开发效率,因此热更新非常适合开发模式下框架的单模块加载和卸载。
对于在线场景,热更新也不是没用。例如,很明显,父母和孩子依赖于一对一的内聚逻辑模块,并且不创建资源属性。热插拔可以通过适当的代码组织来实现更新的无缝发布。目的。
最后,总的来说,由于不熟悉会导致应用中毒的风险和热更新的好处,我个人是反对在在线生产环境中使用热更新技术的;而如果ESM模块后期加载了一个可以明显下沉到规范并由引擎实现的卸载机制,那么可能是热更新真正被广泛安全使用的合适时机。
一些总结
在这几年参与维护的过程中,处理过很多热更新导致的内存泄漏。正好趁着写这篇文章的机会,回顾一下之前的案例。
目前实现热更新的模块,其实可以归于“黑魔法”的范畴。与“黑科技”相比,“黑魔法”是一把双刃剑。在使用它之前,您需要小心不要伤害自己。
原始链接;