DachangTechnologyAdvancedフロントエンドノードAdvanced
トッププログラマーの成長ガイドをクリックし、公開番号に注意してください
返信1、高度なノード交換グループに参加する
Node.jsが2015年と16年に始まったばかりのとき、以前の雇用主の就職の面接でNode.jsサービスのホットアップデートを実装する方法も尋ねられたことを覚えています。
実際、初期にPhp-fpm / Fast-cgiから転送されたNoderは、サーバーを再起動せずにビジネスロジックコードを更新するこの展開スキームを間違いなく気に入っています。その利点も非常に明白です。
サービスを再起動する必要がないということは、特に多数の長いリンクホールドがあるアプリケーションの場合、ユーザー接続が中断されないことを意味します
ファイル更新の読み込みキャッシュは、ミリ秒単位でアプリケーションの更新を完了することができる非常に高速なプロセスです
一般的なメモリリーク(リソースリーク)など、ホットアップデートには多くの副作用もあります。この記事では、ダウンロード数が比較的多い2つの人気のあるホットアップデート補助モジュールであるclear-moduleとdecacheを使用して、ホットアップデートがアプリケーションに与える影響について説明します。 。どのような問題が発生しますか。
熱交換の原理
ホットアップデートについて説明する前に、まずNode.jsのモジュールメカニズムの概要を理解する必要があります。これにより、後で発生する問題をより深く理解できるようになります。
Node.js自体によって実装されたモジュール読み込みメカニズムを次の図に示します。
簡単に言うと、親モジュールAがサブモジュールBを導入する手順は次のとおりです。
サブモジュールBキャッシュが存在するかどうかを確認します
存在しない場合は、Bをコンパイルして解析します
Bモジュールキャッシュをに追加します
require.cache
(ここで、キーはモジュールBのフルパスです)Bモジュール参照を親モジュールAの
children
配列に追加します
存在する場合は、親モジュールAの配列にBが存在するかどうかを判断し、存在し
children
ない場合は、Bモジュールの参照を追加します。
実際、この時点で、メモリリークなしでホットアップデートを実現するには、ホットアップデートするモジュールの次の参照リンクを切断する必要があることがすでにわかります。
このように、require
サブモジュールBに再度移動すると、モジュールBの内容がディスクから再度読み取られ、コンパイルされてメモリにインポートされるため、ホットアップデートの機能が実現されます。
実際、最初のセクションで説明したモジュールclear-module
とdecache
2つのパッケージは、このアイデアに従って実装されています。もちろん、サブモジュールB自体の依存関係をクリアしたり、forループを作成したりするなど、より完璧であると見なされます。シーン。
では、これら2つのモジュールの助けを借りて、Node.jsアプリケーションのホットアップデートは完璧ですか?どれどれ。
問題1:メモリリーク
メモリリークは非常に興味深い問題です.Node.jsフルスタック開発の深海域に入るすべての学生は、基本的にメモリリークの問題に遭遇します。トラブルシューティングとポジショニングの私の個人的な経験から、開発者はする必要はありません他のあいまいな問題と比較して、メモリリークは、コードに精通して時間がかかる限り、100%解決可能なタイプの障害であるため、メモリリークを恐れてください。
ここでは、すべての古いモジュール参照をクリアしているように見えるホットアップデートソリューションと、どのような形式のメモリリークが発生するかを見てみましょう。
デキャッシュ
decache
次の修正プログラムの例を作成し、それを使用して最初にテストすることを検討してください。
'use strict';
const cleanCache = require('decache');
let mod = require('./update_mod.js');
mod();
mod();
setInterval(() => {
cleanCache('./update_mod.js');
mod = require('./update_mod.js');
mod();
}, 100);
この例では./update_mod.js
、ホットアップデートのためにこのモジュールのキャッシュを継続的にクリアすることと同じです。内容は次のとおりです。
'use strict';
const array = new Array(10e5).fill('*');
let count = 0;
module.exports = () => {
console.log('update_mod', ++count, array.length);
};
メモリリーク現象をすばやく観察するために、通常のモジュールクロージャリファレンスを置き換えるために、ここに大きな配列が構築されています。
観察の便宜のためにindex.js
、現在のメモリステータスを定期的に出力するメソッドを追加できます。
function printMemory() {
const { rss, heapUsed } = process.memoryUsage();
console.log(`rss: ${(rss / 1024 / 1024).toFixed(2)}MB, heapUsed: ${(heapUsed / 1024 / 1024).toFixed(2)}MB`);
}
printMemory();
setInterval(printMemory, 1000);
最後に、node index.js
ファイルを実行すると、メモリがすぐにオーバーフローすることがわかります。
update_mod 1 1000000
update_mod 2 1000000
rss: 34.59MB, heapUsed: 11.51MB
update_mod 1 1000000
rss: 110.20MB, heapUsed: 80.09MB
update_mod 1 1000000
...
rss: 921.63MB, heapUsed: 888.99MB
update_mod 1 1000000
rss: 998.09MB, heapUsed: 965.12MB
update_mod 1 1000000
update_mod 1 1000000
<--- Last few GCs --->
[50524:0x158008000] 13860 ms: Scavenge 1018.3 (1024.6) -> 1018.3 (1028.6) MB, 2.3 / 0.0 ms (average mu = 0.783, current mu = 0.576) allocation failure
[50524:0x158008000] 14416 ms: Mark-sweep (reduce) 1026.0 (1036.3) -> 1025.9 (1029.3) MB, 457.8 / 0.0 ms (+ 86.6 ms in 77 steps since start of marking, biggest step 8.7 ms, walltime since start of marking 555 ms) (average mu = 0.670, current mu = 0.360
<--- JS stacktrace --->
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
ヒープスナップショットを取得した後の分析:アレイ内で繰り返される多数のホットアップデートモジュールのコンパイル結果がメモリリークにつながる
ことは明らかであり、情報をさらに詳しく調べると、それがエントリであることがわかります。Module@39215
children
update_mod.js
Module@39215
index.js
実装のソースコードを読んだ後decache
、リークの理由は、ホットアップデートの実装の原則に関するセクションで説明した3つの参照をすべて削除する必要があるためですが、残念ながら、最も基本的な参照リンクのみdecache
が切断されています。require.cache
これまでのところ、decache
最も基本的なホットアップデートメモリの問題が解決されていないため、94wの月間ダウンロードボリュームはブラインドされており、参照用にホットアップデートソリューションを直接除外できます。
参照する:
デキャッシュ問題のソースコードの実際の場所:https://github.com/dwyl/decache/blob/main/decache.js#L35
クリアモジュール
次に、月間ダウンロード量が19wであるか見てみましょうclear-module
。
前のセクションのテストコードは最も基本的なモジュールのホットチェンジシナリオを表しており、clear-module
APIの使用法は基本的に同じであるためdecache
、参照を置き換えるだけcleanCache
でこのラウンドのテストを実行できます。
// index.js
const cleanCache = require('clear-module');
また、node index.js
ファイルを実行すると、次のようにメモリの変化を確認できます。
update_mod 1 1000000
update_mod 2 1000000
rss: 35.00MB, heapUsed: 11.58MB
update_mod 1 1000000
rss: 110.69MB, heapUsed: 80.10MB
update_mod 1 1000000
rss: 187.36MB, heapUsed: 156.52MB
update_mod 1 1000000
rss: 256.28MB, heapUsed: 225.26MB
update_mod 1 1000000
rss: 332.78MB, heapUsed: 301.71MB
update_mod 1 1000000
rss: 401.61MB, heapUsed: 370.38MB
update_mod 1 1000000
rss: 42.67MB, heapUsed: 11.17MB
update_mod 1 1000000
rss: 65.63MB, heapUsed: 34.15MB
update_mod 1 1000000
ここで、clear-module
メモリの傾向が波打っていることを確認できます。これは、原則のセクションで説明した古いモジュールのすべての参照を完全に処理するため、ホットアップデート前の古いモジュールを正常にGCできることを示しています。
ソースコードを確認した後clear-module
、サブモジュールへの親モジュールの参照もクリアされていることがわかります。
したがって、この例では、熱によってプロセスメモリリークOOMが発生することはありません。
詳細なコードは次の場所にあります:https://github.com/sindresorhus/clear-module/blob/main/index.js#L25-L31
clear-module
それで、あなたはあなたが記憶を心配することなく座ってリラックスすることができると思いますか?
実際、いいえ、index.js
上記にいくつかの小さな変更を加えます。
'use strict';
const cleanCache = require('clear-module');
let mod = require('./update_mod.js');
mod();
mod();
require('./utils.js');
setInterval(() => {
cleanCache('./update_mod.js');
mod = require('./update_mod.js');
mod();
}, 100);
前に追加したものと比較するとutils.js
、そのロジックは非常に単純です。
'use strict';
require('./update_mod.js')
setInterval(() => require('./update_mod.js'), 100);
対応するシナリオは、実際には、ミドルウェアがindex.js
クリーンアップされた後、最新のホットアップデートモジュールロジックを引き続き使用するために、使用されてupdate_mod.js
いるモジュールutils.js
も再導入されるというものです。require
ファイルの実行を続行するnode index.js
と、今度はメモリが再び急速にオーバーフローすることがわかります。
update_mod 1 1000000
update_mod 2 1000000
rss: 34.59MB, heapUsed: 11.51MB
update_mod 1 1000000
rss: 110.20MB, heapUsed: 80.09MB
update_mod 1 1000000
...
rss: 921.63MB, heapUsed: 888.99MB
update_mod 1 1000000
rss: 998.09MB, heapUsed: 965.12MB
update_mod 1 1000000
update_mod 1 1000000
<--- Last few GCs --->
[53359:0x140008000] 13785 ms: Scavenge 1018.5 (1025.1) -> 1018.5 (1029.1) MB, 2.2 / 0.0 ms (average mu = 0.785, current mu = 0.635) allocation failure
[53359:0x140008000] 14344 ms: Mark-sweep (reduce) 1026.1 (1036.8) -> 1025.9 (1029.3) MB, 462.2 / 0.0 ms (+ 87.7 ms in 89 steps since start of marking, biggest step 7.5 ms, walltime since start of marking 559 ms) (average mu = 0.667, current mu = 0.296
<--- JS stacktrace --->
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
分析のためにヒープスナップショットを取得し続けます。
今回はModule@37543
、children
アレイの下に多数のホットモジュールが繰り返され、メモリリークが発生します。詳細upload_mod.js
を見てみましょう。Module@37543
clear-module
ホットアップデートされたサブモジュールへの親モジュールの参照がクリーンアップされた(この例ではindex.js
、親モジュールです)が、utils.js
非常に多くの古い参照がまだ保持されているのは不思議ではありませんか?
実際、これは、Node.jsのモジュール実装メカニズムでは、サブモジュールと親モジュールが実際には多対多の関係にあり、モジュールキャッシングメカニズムのため、サブモジュールは次の場合にのみ実行されるためです。が初めてロードされます。コンストラクターの初期化:
これはclear-module
、ホットアップデートモジュールへの親モジュールの古い参照のいわゆる削除は、ホットアップデートモジュールに対応する親モジュールが初めて導入されたときのみであり、この場合index.js
、index.js
対応するchildren
アレイがクリーンであることを意味します。
ホットアップデートモジュールがutils.js
親モジュールとして導入されると、最新バージョンのホットアップデートモジュールのキャッシュが読み取られ、children
参照が更新されます。
キャッシュされたオブジェクトchildren
が配列に存在しない場合は追加されると判断しますが、明らかupdate_mod.js
に、ホットアップデート前後の2回のコンパイルで取得したメモリオブジェクトは同じではないためutils.js
、キャッシュにリークがあります。
これまでのところ、少し複雑なロジックの下で、clear-module
それも打ち負かされています。実際の開発でのロジック負荷がこれよりもはるかに高くなることを考えると、作成者が完全に制御しない限り、ホットアップデートが本番環境で使用されることは明らかです。モジュールメカニズム、そうでなければ、それはまだあなた自身と将来の世代のための穴を掘ります。
興味深い考えを残してください:clear-module
このシナリオでのリークは解決できません。興味のある学生は、原則を参照して、このシナリオでのホットアップデートメモリリークを回避する方法について考えることができます。
参照する:
親モジュールを設定します:https://github.com/nodejs/node/blob/v16.13.2/lib/internal/modules/cjs/loader.js#L176
更新されたリファレンス:https://github.com/nodejs/node/blob/v16.13.2/lib/internal/modules/cjs/loader.js#L167
lodash
上記の例は一般的ではないと考える学生もいるかもしれません。ホットアップデートのために開発者が完全に制御できない非べき等のサブ依存モジュールの繰り返しロードによって引き起こされるメモリリークのケースを見てみましょう。
ここでは、メモリリークを構築するために、非常に部分的なパッケージを探すことはしません。例として、週あたりのダウンロード量が最大3900wの非常に一般的なツールモジュールを取り上げて、変更を続けましょ うlodash
。 uploda_mod.js
'use strict';
const lodash = require('lodash');
let count = 0;
module.exports = () => {
console.log('update_mod', ++count);
};
次に index.js
、上記を削除し 、ホットアップデートutils.js
のみ update_mod.js
を繰り返します。
'use strict';
const cleanCache = require('clear-module');
let mod = require('./update_mod.js');
mod();
mod();
setInterval(() => {
cleanCache('./update_mod.js');
mod = require('./update_mod.js');
mod();
}, 10);
function printMemory() {
const { rss, heapUsed } = process.memoryUsage();
console.log(`rss: ${(rss / 1024 / 1024).toFixed(2)}MB, heapUsed: ${(heapUsed / 1024 / 1024).toFixed(2)}MB`);
}
printMemory();
setInterval(printMemory, 1000);
次に node index.js
ファイルを実行すると、今回はダブルリークが再びリークされていることがわかります。 update_mod.js
ホットアップデートを使用すると、ヒープメモリが急速に増加し、最終的にOOMになります。
この場合、べき等でないサブモジュールのリークの理由はもう少し複雑で lodash
、モジュールのコンパイルと実行が繰り返されるため、クロージャー循環参照が発生します。
実際、モジュールの導入は開発者にとって制御不能であることがわかります。つまり、開発者は、独立して lodash
実行できるパブリックモジュールをインポートしたかどうかを確認できず、メモリリークが発生します。
問題2:リソースリーク
熱によって引き起こされる可能性が高いメモリの問題のシナリオについて説明した後、熱によって引き起こされる可能性が高い別の種類の比較的解決できないリソースリークの問題を見てみましょう。
簡単な例を使用して説明します。まず、次のように構成しindex.js
ます。
'use strict';
const cleanCache = require('clear-module');
let mod = require('./update_mod.js');
setInterval(() => {
cleanCache('./update_mod.js');
mod = require('./update_mod.js');
console.log('-------- 热更新结束 --------')
}, 1000);
今回はclear-module
、ホットアップデート操作を直接使用し、ホットアップデートするモジュールをupdate_mod.js
次のように紹介します。
'use strict';
const start = new Date().toLocaleString();
setInterval(() => console.log(start), 1000);
ではupdate_mod.js
、モジュールが最初に導入された時刻を1秒間隔で出力する時間指定タスクを作成しました。
実行の最後にnode index.js
、次の結果が表示されます。
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
明らかにclear-module
、ホットスワップ可能なモジュールの古い参照は正しくクリアされますが、古いモジュール内の時間指定されたタスクは一緒にリサイクルされないため、リソースリークが発生します。
実際、ここでスケジュールされたタスクはリソースの1つにすぎず、、、、などのさまざまなシステムリソース操作はsocket
、fd
古いモジュール参照のみをクリアするシナリオでは自動的にリサイクルできません。
質問3:ESM Meow Meow Meow?
decache
それがそうであるかどうかに関係なくclear-module
、Node.jsによって実装されたCommonJSモジュールメカニズムに基づくホットでより論理的な統合です。
しかし、フロントエンド全体が今日まで開発されています。ネイティブECMA仕様で定義されているモジュールメカニズムはESModule(略してESM)です。仕様で定義されているため、その実装はエンジンレベルで行われ、レイヤーはに対応します。 Node.jsはV8によって実装されています。したがって、現在の熱はESMモジュールに作用できません。
ただし、私の意見では、CommonJSベースのホットスポットは、より高いレベルで実装され、さまざまなピットを隠すため、本番環境での使用はお勧めしません。ただし、ESMベースのホットスポットは、仕様で可能であれば、完全なモジュールのロードおよびアンロードメカニズムを定義できます。完全なモジュールのロードおよびアンロードメカニズムを定義します。これは、真のホットリフレッシュソリューションの未来です。
Node.jsには、この領域で使用できる対応する実験的な機能もあります。詳細については、ESMフックを参照してください。(https://nodejs.org/dist/latest/docs/api/esm.html#esm_hooks)ただし、現在は安定性:1の状態にあるだけであり、引き続き待機して確認する必要があります。
問題4:モジュールバージョンの混乱
Node.jsのホットアップデートは、実際には多くの学生が想像したようなグローバルな古いモジュールの置き換えではありません。キャッシュメカニズムにより、ホットアップデートされたモジュールの複数の異なるバージョンが同時にメモリに存在し、奇妙な結果になる可能性があるためです。見つけるのが難しいバグ。
説明のために小さな例を作成し続けましょう。最初に、ホットアップデートするモジュールを作成しますupdate_mod.js
。
'use strict';
const version = 'v1';
module.exports = () => {
return version;
};
utils.js
次に、このモジュールを通常どおりに使用するために1つ追加します。
'use strict';
const mod = require('./update_mod.js');
setInterval(() => console.log('utils', mod()), 1000);
index.js
次に、ホットアップデート操作のスタートアップエントリを記述します。
'use strict';
const cleanCache = require('clear-module');
let mod = require('./update_mod.js');
require('./utils.js');
setInterval(() => {
cleanCache('./update_mod.js');
mod = require('./update_mod.js');
console.log('index', mod())
}, 1000);
この時点で、実行してnode index.js
変更しないと、次のupdate_mod.js
ことがわかります。
utils v1
index v1
utils v1
index v1
メモリはupdate_mod.js
すべてのv1
バージョンであることに注意してください。
今すぐサービスを再起動する必要はありません。update_mod.js
変更していversion
ます:
// update_mod.js
const version = 'v2';
次に、出力が次のようになることを確認します。
index v1
utils v1
index v2
utils v1
index v2
utils v1
index.js
でホットアップデート操作が実行されたため、最新バージョンに再require
到着し、に変更はありません。update_mod.js
v2
utils.js
モジュールの複数のバージョンのこのような状況は、オンライン障害の特定を困難にするだけでなく、ある程度のメモリリークを引き起こします。
ホットアップデートシナリオに適しています
シーン以外の問題について話すのはフーリガンです。ホットアップデートには非常に多くの問題がありますが、モジュールのホットアップデートには確かにいくつかの使用シナリオがあります。オンラインとオフラインの2つの側面から説明します。
オフラインのシナリオでは、マイナーなメモリとリソースリークの問題が開発効率に影響を与える可能性があるため、ホットアップデートは開発モードでのフレームワークの単一モジュールのロードとアンロードに非常に適しています。
オンラインシナリオの場合、ホットアップデートは役に立たないわけではありません。たとえば、親と子が1対1のまとまりのあるロジックモジュールに依存し、リソース属性を作成しないことは明らかです。適切なコード編成を通じてホットプラグを実行して、シームレスを実現できます。アップデートのリリース。目的。
結局、一般的に、アプリケーションを汚染するリスクと不慣れによるホットアップデートの利点のために、私は個人的にオンライン実稼働環境でホットアップデートテクノロジーを使用することに反対しています;そしてESMモジュールが後でロードされる場合仕様に明確に適合し、エンジンによって実装できるオフロードメカニズムでは、ホットアップデートが実際に広く安全に使用されるのに適切な時期である可能性があります。
いくつかの要約
過去数年間にAliNodeを保守する過程で、ホットアップデートによって引き起こされる多くのメモリリークに対処してきました。この記事を書く機会を利用して、以前のケースを確認しました。
現在、ホットアップデートを実現しているモジュールは、実際には「黒魔術」の範疇にあると考えられます。「黒魔術」と比較すると、「黒魔術」は両刃の剣です。使用する前に注意が必要です。自分を傷つけるために。
- 終わり -
Node 社群
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:
1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客 www.inode.club 让我们一起成长
点赞和在看就是最大的支持