php開發(fā)典型模塊大全光盤Node.js應(yīng)用的熱更新是不是就完美無(wú)缺了呢?我們接著看典型模塊與項(xiàng)目實(shí)戰(zhàn)大全光盤
2022-07-16
簡(jiǎn)介:記得2015年和16年Node.js剛起步的時(shí)候,在前雇主面試的時(shí)候,也被問到如何實(shí)現(xiàn)Node.js服務(wù)的熱更新。
記得 2015 年和 16 年 Node.js 剛起步的時(shí)候,我在前雇主的面試中也被問到如何實(shí)現(xiàn) Node.js 服務(wù)的熱更新。
其實(shí)早期從 Php-fpm / Fast-cgi 轉(zhuǎn)過來(lái)的人,肯定很喜歡這種不用重啟服務(wù)器就能更新業(yè)務(wù)邏輯代碼的部署方案。它的優(yōu)點(diǎn)也很明顯:
熱更新還有很多副作用,比如常見的內(nèi)存泄漏(資源泄漏)。本文將使用-和這兩個(gè)下載量比較高的熱門熱更新輔助模塊來(lái)討論熱更新會(huì)給我們的應(yīng)用帶來(lái)什么。問題。
熱置換原理
在開始說(shuō)熱更新之前,我們首先要了解一下Node.js的模塊機(jī)制的概況網(wǎng)站建設(shè),這樣才能對(duì)它后面帶來(lái)的問題有更深入的了解和理解。
Node.js自身實(shí)現(xiàn)的模塊加載機(jī)制如下圖所示:
簡(jiǎn)單來(lái)說(shuō),父模塊A引入子模塊B的步驟如下:
其實(shí)到這一步,我們已經(jīng)可以發(fā)現(xiàn),要實(shí)現(xiàn)不泄露內(nèi)存的熱更新,需要斷開待熱更新模塊的以下參考鏈接:
這樣,當(dāng)我們?cè)俅稳プ幽KB時(shí),會(huì)再次從磁盤中讀取模塊B的內(nèi)容,然后編譯導(dǎo)入內(nèi)存,從而實(shí)現(xiàn)熱更新的能力。
其實(shí)第一節(jié)提到的-和兩個(gè)包都是基于這個(gè)思想的模塊。當(dāng)然,它們會(huì)被認(rèn)為更完美,例如清除子模塊 B 本身的依賴關(guān)系,以及用于處理循環(huán)引用的場(chǎng)景。
那么,在這兩個(gè)模塊的幫助下,Node.js 應(yīng)用的熱更新就完美了嗎?讓我們來(lái)看看。
問題一:內(nèi)存泄漏
內(nèi)存泄漏是一個(gè)非常有趣的問題。所有進(jìn)入Node.js全棧開發(fā)深水區(qū)的同學(xué),基本都會(huì)遇到內(nèi)存泄露的問題。以我個(gè)人的排查和定位經(jīng)驗(yàn)來(lái)看,開發(fā)者不需要擔(dān)心內(nèi)存泄漏,因?yàn)橄啾绕渌逎y懂的問題,內(nèi)存泄漏是一種故障類型,只要熟悉代碼,100%可以解決慢慢來(lái)。
在這里,我們來(lái)看看似乎清除所有舊模塊引用的熱更新解決方案,以及會(huì)發(fā)生什么形式的內(nèi)存泄漏。
考慮構(gòu)建以下修補(bǔ)程序示例,首先使用它進(jìn)行測(cè)試:
'利用';
= ('');
讓 mod = ('./.js');
模組();
模組();
(() => {
('./.js');
mod = ('./.js');
模組();
}, 100);
在這個(gè)例子中php開發(fā)典型模塊大全光盤,相當(dāng)于不斷的清空 ./.js 模塊的緩存進(jìn)行熱更新。其內(nèi)容如下:
'利用';
= 新 (10e5).fill('*');
讓 = 0;
. = () => {
.log('', ++, .);
};
為了快速觀察內(nèi)存泄漏現(xiàn)象,這里構(gòu)造了一個(gè)大數(shù)組來(lái)代替常規(guī)的模塊閉包引用。
為了方便觀察,我們可以在.js中添加一個(gè)方法,定時(shí)打印當(dāng)前內(nèi)存狀態(tài):
() {
{ rss, } = .();
.log(`rss: ${(rss / 1024 / 1024).(2)}MB, : ${( / 1024 / 1024).(2)} MB`);
}
();
(, 1000);
最后執(zhí)行node.js文件,可以看到內(nèi)存很快溢出:
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 - 堆出
拍攝堆快照后的分析:
很明顯,大量重復(fù)的熱更新模塊被塞進(jìn)了@的數(shù)組中。js編譯結(jié)果導(dǎo)致內(nèi)存泄漏,進(jìn)一步查看@信息:
您可以看到它是條目.js。
看了實(shí)現(xiàn)源碼,發(fā)現(xiàn)泄露的原因是我們需要把熱更新實(shí)現(xiàn)原理一節(jié)中提到的三個(gè)引用全部去掉,可惜只有最基本的還是斷開了。此參考鏈接:
至此,由于最基本的熱更新內(nèi)存問題還沒有解決,94w的月下載量已經(jīng)被蒙蔽了,我們可以直接排除我們的熱更新解決方案以供參考。
參考:
-
接下來(lái),我們來(lái)看看19w的月下載量——它的表現(xiàn)如何。
由于上一節(jié)的測(cè)試代碼代表了最基本的模塊熱改場(chǎng)景,而且用法基本相同,我們只需替換引用即可進(jìn)行這一輪測(cè)試:
//.js
= ('-');
同樣執(zhí)行node.js文件,可以看到內(nèi)存變化如下:
1
2
RSS: 35.00MB, : 11.58MB
1
RSS:110.69MB,:80.10MB
1
RSS:187.36MB網(wǎng)站開發(fā),: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
這里可以發(fā)現(xiàn)- 趨勢(shì)呈波浪狀,說(shuō)明完美處理了原理部分提到的舊模塊的所有引用,使得熱更新前的舊模塊可以正常GCed。
查看源碼后發(fā)現(xiàn),父模塊對(duì)子模塊的引用也被清除了:
因此,在這個(gè)例子中,熱量不會(huì)導(dǎo)致進(jìn)程內(nèi)存泄漏OOM。
詳細(xì)代碼請(qǐng)參考:#L25-L31
所以你不覺得 - 你可以高枕無(wú)憂而不必?fù)?dān)心記憶?
其實(shí)不然,我們?cè)賹?duì)上面的.js做一些小修改:
'利用';
= ('-');
讓 mod = ('./.js');
模組();
模組();
('./.js');
(() => {
('./.js');
mod = ('./.js');
模組();
}, 100);
和之前加個(gè).js相比,它的邏輯還是挺簡(jiǎn)單的:
'利用';
('./.js')
(() => ('./.js'), 100);
對(duì)應(yīng)的場(chǎng)景其實(shí)是在.js中清理了.js之后,同樣使用的模塊的.js也被重新引入,以保持使用最新的熱更新模塊邏輯。
繼續(xù)執(zhí)行node.js文件,可以看到這次內(nèi)存又快速溢出了:
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 - 堆出
繼續(xù)抓取堆快照進(jìn)行分析:
這次@數(shù)組下有大量重復(fù)的熱更新模塊。js導(dǎo)致內(nèi)存泄漏。我們來(lái)看看@的細(xì)節(jié):
是不是很奇怪——明明父模塊對(duì)熱更新子模塊的引用已經(jīng)清理干凈了(本例中是.js的父模塊),但是.js中還保留了這么多舊的引用?
其實(shí)這是因?yàn)?,?Node.js 的模塊實(shí)現(xiàn)機(jī)制中,子模塊和父模塊其實(shí)是多對(duì)多的關(guān)系,而且因?yàn)槟K緩存機(jī)制,子模塊只有在是第一次加載。構(gòu)造函數(shù)初始化:
這意味著在--中所謂的去除熱更新模塊的舊引用的父模塊只是第一次引入熱更新模塊對(duì)應(yīng)的父模塊,在這種情況下。js,所以.js對(duì)應(yīng)的數(shù)組是干凈的。
當(dāng).js作為父模塊引入熱更新模塊時(shí),讀取熱更新模塊最新版本的緩存,更新引用:
它會(huì)判斷數(shù)組中不存在緩存的對(duì)象并添加進(jìn)去。顯然,熱更新前后兩次編譯.js得到的內(nèi)存對(duì)象是不一樣的,所以.js中存在泄漏。
至此,在稍微復(fù)雜一點(diǎn)的邏輯下,我也被打敗了??紤]到實(shí)際開發(fā)中的邏輯負(fù)載會(huì)比這個(gè)高很多,很明顯在生產(chǎn)中使用熱更新,除非作者對(duì)模塊機(jī)制有徹底的把控。為子孫后代挖坑。
留下一個(gè)有趣的想法: - 這種情況下的泄漏并非無(wú)法解決。有興趣的同學(xué)可以參考原理思考一下如何避免這種場(chǎng)景下的熱更新內(nèi)存泄漏。
參考:
有的同學(xué)可能會(huì)覺得上面的例子不夠典型。我們來(lái)看一個(gè)由于熱更新導(dǎo)致開發(fā)者完全無(wú)法控制的非冪等子依賴模塊重復(fù)加載導(dǎo)致的內(nèi)存泄漏案例。
在這里,我們不會(huì)為了構(gòu)造內(nèi)存泄漏而特意尋找非常部分的包。我們以一個(gè)很常見的工具模塊為例,每周下載量很高,繼續(xù)修改我們的.js:
'利用';
= ('');
讓 = 0;
. = () => {
.log('', ++);
};
然后從 .js 中刪除上面的 .js 并只對(duì) .js 重復(fù)熱更新:
'利用';
= ('-');
讓 mod = ('./.js');
模組();
模組();
(() => {
('./.js');
mod = ('./.js');
模組();
}, 10);
() {
{ rss, } = .();
.log(`rss: ${(rss / 1024 / 1024).(2)}MB, : ${( / 1024 / 1024).(2)} MB`);
}
();
(, 1000);
然后執(zhí)行node.js文件,可以看到這次又泄露了。隨著.js的熱更新,堆內(nèi)存迅速增加,最終OOM。
在這種情況下,非冪等子模塊泄漏的原因稍微復(fù)雜一些,涉及到模塊的反復(fù)編譯和執(zhí)行,會(huì)導(dǎo)致閉包循環(huán)引用。
其實(shí)會(huì)發(fā)現(xiàn)模塊的引入對(duì)于開發(fā)者來(lái)說(shuō)是不可控的。也就是說(shuō),開發(fā)者無(wú)法確認(rèn)自己是否導(dǎo)入了可以冪等執(zhí)行的公共模塊。導(dǎo)致它產(chǎn)生內(nèi)存泄漏。
問題二:資源泄露
講完了內(nèi)存問題場(chǎng)景,更可能是熱量造成的,我們?cè)賮?lái)看看另外一種比較難解決的,熱量更容易造成的資源泄漏問題。
我們還是用一個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明,首先構(gòu)造.js:
'利用';
= ('-');
讓 mod = ('./.js');
(() => {
('./.js');
mod = ('./.js');
.log('---------熱更新結(jié)束---------')
}, 1000);
這次我們直接使用-來(lái)進(jìn)行熱更新操作,引入要熱更新的模塊。js如下:
'利用';
= 新日期()。();
(() => .log(), 1000);
在 .js 中,我們創(chuàng)建了一個(gè)定時(shí)任務(wù),它以 1 秒的間隔輸出模塊首次引入的時(shí)間。
最后執(zhí)行node.jsphp開發(fā)典型模塊大全光盤,可以看到如下結(jié)果:
2022/1/21 上午 9:37:29
-------- 熱更新結(jié)束---------
2022/1/21 上午 9:37:29
2022/1/21 上午 9:37:30
-------- 熱更新結(jié)束---------
2022/1/21 上午 9:37:29
2022/1/21 上午 9:37:30
2022/1/21 上午 9:37:31
-------- 熱更新結(jié)束---------
2022/1/21 上午 9:37:29
2022/1/21 上午 9:37:30
2022/1/21 上午 9:37:31
2022/1/21 上午 9:37:32
-------- 熱更新結(jié)束---------
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
-------- 熱更新結(jié)束---------
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
顯然,雖然熱替換模塊的舊引用被正確清除,但舊模塊內(nèi)部的定時(shí)任務(wù)并沒有一起回收,導(dǎo)致資源泄漏。
其實(shí)這里的定時(shí)任務(wù)只是資源之一。在只清除舊模塊引用的場(chǎng)景下,包括FD、FD在內(nèi)的各種系統(tǒng)資源操作無(wú)法自動(dòng)回收。
問題3:ESM喵喵喵?
不管是還是-,都是基于Node.js實(shí)現(xiàn)的模塊機(jī)制的比較熱的邏輯集成。
但是,整個(gè)前端發(fā)展到今天。原生ECMA規(guī)范定義的模塊機(jī)制是(簡(jiǎn)稱ESM)。因?yàn)槭且?guī)范定義的,所以它的實(shí)現(xiàn)是引擎層面的,Node.js對(duì)應(yīng)的層是V8實(shí)現(xiàn)的。因此,當(dāng)前的熱量更不能作用于 ESM 模塊。
但是在我看來(lái),基于熱度比較多因?yàn)槭窃谏蠈訉?shí)現(xiàn)的,會(huì)隱藏各種坑,所以不建議在生產(chǎn)中使用,但是基于ESM的熱度比較多,如果規(guī)范可以定義一個(gè)完整的模塊加載和卸載機(jī)制,將是真正的熱刷新解決方案的未來(lái)。
Node.js 在這方面也有相應(yīng)的實(shí)驗(yàn)特性可以使用。有關(guān)詳細(xì)信息,請(qǐng)參閱:ESM。(#) 不過目前只處于:1的狀態(tài),需要繼續(xù)觀望。
問題四:模塊版本混淆
Node.js的熱更新其實(shí)并不是很多同學(xué)想象的那種全局舊模塊替換,因?yàn)榫彺鏅C(jī)制可能會(huì)導(dǎo)致多個(gè)不同版本的熱更新模塊同時(shí)存在于內(nèi)存中,導(dǎo)致一些奇怪難以定位的錯(cuò)誤。.
我們繼續(xù)構(gòu)建一個(gè)小例子來(lái)說(shuō)明,首先將模塊寫成hot-.js:
'利用';
='v1';
. = () => {
;
};
然后添加一個(gè) .js 來(lái)正常使用這個(gè)模塊:
'利用';
mod = ('./.js');
(() => .log('', mod()), 1000);
然后編寫啟動(dòng).js進(jìn)行熱更新操作:
'利用';
= ('-');
讓 mod = ('./.js');
('./.js');
(() => {
('./.js');
mod = ('./.js');
.log('', mod())
}, 1000);
此時(shí),當(dāng)我們?cè)诓桓?.js 的情況下執(zhí)行 node.js 時(shí),我們可以看到:
v1
v1
v1
v1
注意內(nèi)存中的 .js 都是 v1 版本。
剛才不需要重啟服務(wù),我們修改.js中的:
//.js
='v2';
然后觀察輸出變?yōu)椋?/p>
v1
v1
v2
v1
v2
v1
.js 是熱更新的,所以它重新到達(dá)的 .js 成為最新的 v2 版本,.js 沒有任何變化。
這樣一個(gè)模塊多個(gè)版本的情況,不僅增加了在線故障定位的難度,也在一定程度上造成了內(nèi)存泄漏。
適用于熱更新場(chǎng)景
拋開現(xiàn)場(chǎng)談問題是流氓。雖然熱更新有這么多問題,但是模塊熱更新確實(shí)有一些使用場(chǎng)景。我們將從線上和線下兩個(gè)維度進(jìn)行討論。
對(duì)于離線場(chǎng)景,較小的內(nèi)存和資源泄漏問題可以讓位于開發(fā)效率,因此熱更新非常適合開發(fā)模式下框架的單模塊加載和卸載。
對(duì)于在線場(chǎng)景,熱更新也不是沒用。例如,很明顯,父母和孩子依賴于一對(duì)一的內(nèi)聚邏輯模塊,并且不創(chuàng)建資源屬性。熱插拔可以通過適當(dāng)?shù)拇a組織來(lái)實(shí)現(xiàn)更新的無(wú)縫發(fā)布。目的。
最后,總的來(lái)說(shuō),由于不熟悉會(huì)導(dǎo)致應(yīng)用中毒的風(fēng)險(xiǎn)和熱更新的好處,我個(gè)人是反對(duì)在在線生產(chǎn)環(huán)境中使用熱更新技術(shù)的;而如果ESM模塊后期加載了一個(gè)可以明顯下沉到規(guī)范并由引擎實(shí)現(xiàn)的卸載機(jī)制,那么可能是熱更新真正被廣泛安全使用的合適時(shí)機(jī)。
一些總結(jié)
在這幾年參與維護(hù)的過程中,處理過很多熱更新導(dǎo)致的內(nèi)存泄漏。正好趁著寫這篇文章的機(jī)會(huì),回顧一下之前的案例。
目前實(shí)現(xiàn)熱更新的模塊,其實(shí)可以歸于“黑魔法”的范疇。與“黑科技”相比,“黑魔法”是一把雙刃劍。在使用它之前,您需要小心不要傷害自己。
原始鏈接;