Node.jsのホットアップデートについて話し、いくつかの一般的なメモリリークを理解する

DachangTechnologyAdvancedフロントエンドノードAdvanced

トッププログラマーの成長ガイドをクリックし、公開番号に注意してください

返信1、高度なノード交換グループに参加する

Node.jsが2015年と16年に始まったばかりのとき、以前の雇用主の就職の面接でNode.jsサービスのホットアップデートを実装する方法も尋ねられたことを覚えています。

実際、初期にPhp-fpm / Fast-cgiから転送されたNoderは、サーバーを再起動せずにビジネスロジックコードを更新するこの展開スキームを間違いなく気に入っています。その利点も非常に明白です。

  • サービスを再起動する必要がないということは、特に多数の長いリンクホールドがあるアプリケーションの場合、ユーザー接続が中断されないことを意味します

  • ファイル更新の読み込みキャッシュは、ミリ秒単位でアプリケーションの更新を完了することができる非常に高速なプロセスです

一般的なメモリリーク(リソースリーク)など、ホットアップデートには多くの副作用もあります。この記事では、ダウンロード数が比較的多い2つの人気のあるホットアップデート補助モジュールであるclear-moduleとdecacheを使用して、ホットアップデートがアプリケーションに与える影響について説明します。 。どのような問題が発生しますか。

熱交換の原理

ホットアップデートについて説明する前に、まずNode.jsのモジュールメカニズムの概要を理解する必要があります。これにより、後で発生する問題をより深く理解できるようになります。

Node.js自体によって実装されたモジュール読み込みメカニズムを次の図に示します。

113d11d909ffd496d17759332f9dd259.png
簡単に言うと、親モジュールAがサブモジュールBを導入する手順は次のとおりです。

  • サブモジュールBキャッシュが存在するかどうかを確認します

  • 存在しない場合は、Bをコンパイルして解析します

    • Bモジュールキャッシュをに追加しますrequire.cache(ここで、キーはモジュールBのフルパスです)

    • Bモジュール参照を親モジュールAのchildren配列に追加します

  • 存在する場合は、親モジュールAの配列にBが存在するかどうかを判断し、存在しchildrenない場合は、Bモジュールの参照を追加します。

実際、この時点で、メモリリークなしでホットアップデートを実現するには、ホットアップデートするモジュールの次の参照リンクを切断する必要があることがすでにわかります。

698c19e1b1382cb76ba2b653d2cc4310.png
このように、requireサブモジュールBに再度移動すると、モジュールBの内容がディスクから再度読み取られ、コンパイルされてメモリにインポートされるため、ホットアップデートの機能が実現されます。

実際、最初のセクションで説明したモジュールclear-moduledecache2つのパッケージは、このアイデアに従って実装されています。もちろん、サブモジュール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

ヒープスナップショットを取得した後の分析:アレイ内で繰り返される多数のホットアップデートモジュールコンパイル結果がメモリリークにつながる
3ebdec069b9bee624088333b8510a17b.png
ことは明らかであり、情報をさらに詳しく調べると、それがエントリであることがわかりますModule@39215childrenupdate_mod.jsModule@39215
f13e8534897fd53a273972285c40d733.png
index.js

実装のソースコードを読んだ後decache、リークの理由は、ホットアップデートの実装の原則に関するセクションで説明した3つの参照をすべて削除する必要があるためですが、残念ながら、最も基本的な参照リンクのみdecacheが切断されています。require.cache
42a4a3201afbaf5d5194050dd84da6e8.png

これまでのところ、decache最も基本的なホットアップデートメモリの問題が解決されていないため、94wの月間ダウンロードボリュームはブラインドされており、参照用にホットアップデートソリューションを直接除外できます。

参照する:

  • デキャッシュ問題のソースコードの実際の場所:https://github.com/dwyl/decache/blob/main/decache.js#L35

クリアモジュール

次に、月間ダウンロード量が19wであるか見てみましょうclear-module

前のセクションのテストコードは最も基本的なモジュールのホットチェンジシナリオを表しており、clear-moduleAPIの使用法は基本的に同じであるため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、サブモジュールへの親モジュールの参照もクリアされていることがわかります。
acb5a3522ede13a1b8e27e84f070ef6f.png

したがって、この例では、熱によってプロセスメモリリーク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

分析のためにヒープスナップショットを取得し続けます。

d88bfad5f7ccc86feb3d54df2add1b5c.png
今回はModule@37543childrenアレイの下に多数のホットモジュールが繰り返され、メモリリークが発生します。詳細upload_mod.jsを見てみましょう。Module@37543

f5b3bba48a2201a377f8e0ec5409bc41.png
clear-moduleホットアップデートされたサブモジュールへの親モジュールの参照がクリーンアップされた(この例ではindex.js、親モジュールです)が、utils.js非常に多くの古い参照がまだ保持されているのは不思議ではありませんか?

実際、これは、Node.jsのモジュール実装メカニズムでは、サブモジュールと親モジュールが実際には多対多の関係にあり、モジュールキャッシングメカニズムのため、サブモジュールは次の場合にのみ実行されるためです。が初めてロードされます。コンストラクターの初期化:

ab9720429893aaf1776620cdac8304fa.png
これはclear-module、ホットアップデートモジュールへの親モジュールの古い参照のいわゆる削除は、ホットアップデートモジュールに対応する親モジュールが初めて導入されたときのみであり、この場合index.jsindex.js対応するchildrenアレイがクリーンであることを意味します。

ホットアップデートモジュールがutils.js親モジュールとして導入されると、最新バージョンのホットアップデートモジュールのキャッシュが読み取られ、children参照が更新されます。

b2f826d171cd6a08bdca733ef2d6bf91.png
キャッシュされたオブジェクト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つにすぎず、、、、などのさまざまなシステムリソース操作はsocketfd古いモジュール参照のみをクリアするシナリオでは自動的にリサイクルできません。

質問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.jsv2utils.js

モジュールの複数のバージョンのこのような状況は、オンライン障害の特定を困難にするだけでなく、ある程度のメモリリークを引き起こします。

ホットアップデートシナリオに適しています

シーン以外の問題について話すのはフーリガンです。ホットアップデートには非常に多くの問題がありますが、モジュールのホットアップデートには確かにいくつかの使用シナリオがあります。オンラインとオフラインの2つの側面から説明します。

オフラインのシナリオでは、マイナーなメモリとリソースリークの問題が開発効率に影響を与える可能性があるため、ホットアップデートは開発モードでのフレームワークの単一モジュールのロードとアンロードに非常に適しています。

オンラインシナリオの場合、ホットアップデートは役に立たないわけではありません。たとえば、親と子が1対1のまとまりのあるロジックモジュールに依存し、リソース属性を作成しないことは明らかです。適切なコード編成を通じてホットプラグを実行して、シームレスを実現できます。アップデートのリリース。目的。

結局、一般的に、アプリケーションを汚染するリスクと不慣れによるホットアップデートの利点のために、私は個人的にオンライン実稼働環境でホットアップデートテクノロジーを使用することに反対しています;そしてESMモジュールが後でロードされる場合仕様に明確に適合し、エンジンによって実装できるオフロードメカニズムでは、ホットアップデートが実際に広く安全に使用されるのに適切な時期である可能性があります。

いくつかの要約

過去数年間にAliNodeを保守する過程で、ホットアップデートによって引き起こされる多くのメモリリークに対処してきました。この記事を書く機会を利用して、以前のケースを確認しました。

現在、ホットアップデートを実現しているモジュールは、実際には「黒魔術」の範疇にあると考えられます。「黒魔術」と比較すると、「黒魔術」は両刃の剣です。使用する前に注意が必要です。自分を傷つけるために。

- 終わり -

Node 社群


我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:

1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客 www.inode.club 让我们一起成长

点赞和在看就是最大的支持

おすすめ

転載: blog.csdn.net/xgangzai/article/details/123861140