この記事では、Node でのヒープ メモリ割り当てを調査し、Node.js のメモリ制限について詳しく説明します。お役に立てば幸いです。
この記事では、Node でのヒープ メモリの割り当てについて調査し、ハードウェアが耐えられる限界までメモリを増やしてみます。次に、ノードのプロセスを監視してメモリ関連の問題をデバッグするためのいくつかの実用的な方法を見つけます。
よし、準備が整ったら出発だ!
ウェアハウス内の関連コードを取得し、GitHub からコードのクローンを作成できます:
https://github.com/Beautifulcoder/node-memory-limitations
まず、V8 ガベージ コレクションについて簡単に紹介します。メモリの記憶割り当て方法はヒープであり、複数の世代領域に分割されます。 オブジェクトはライフサイクルの中で年齢が変化し、オブジェクトが属する世代も変化します。
世代は若い世代と古い世代に分けられ、若い世代も新世代と中間世代に分けられます。オブジェクトはガベージ コレクションに生き残ると、古い世代にも加わります。
世代仮説の基本原則は、ほとんどの被験者が若いということです。 V8 ガベージ コレクターは、ガベージ コレクション後に生き残ったオブジェクトのみをプロモートすることによってこれに基づいて構築されます。オブジェクトは隣接する領域にコピーされるため、最終的には古い世代になります。
Nodejs では、メモリ消費は主に 3 つの側面に分けられます。
ヒープ メモリが今日の主な焦点です。 ガベージ コレクターについてもう少し理解できたので、次はヒープにメモリを割り当てます。
function allocateMemory(size) { // Simulate allocation of bytes const numbers = size / 8; const arr = []; arr.length = numbers; for (let i = 0; i < numbers; i++) { arr[i] = i; } return arr; }
呼び出しスタックでは、関数呼び出しが終了するとローカル変数が破棄されます。基になる型 number
はヒープ メモリには入らず、呼び出しスタックに割り当てられます。ただし、オブジェクト arr はヒープに移動し、ガベージ コレクションで生き残る可能性があります。
さて、勇敢なテストです - ノード プロセスを限界まで押し上げて、どこでヒープ メモリが不足するかを確認してください:
const memoryLeakAllocations = []; const field = "heapUsed"; const allocationStep = 10000 * 1024; // 10MB const TIME_INTERVAL_IN_MSEC = 40; setInterval(() => { const allocation = allocateMemory(allocationStep); memoryLeakAllocations.push(allocation); const mu = process.memoryUsage(); // # bytes / KB / MB / GB const gbNow = mu[field] / 1024 / 1024 / 1024; const gbRounded = Math.round(gbNow * 100) / 100; console.log(`Heap allocated ${gbRounded} GB`); }, TIME_INTERVAL_IN_MSEC);
上記のコードでは、40 で実行しています。ミリ秒 約 10 MB が一定の間隔で割り当てられ、存続するオブジェクトを古い世代に昇格させるためのガベージ コレクションに十分な時間を提供します。 process.memoryUsage
は、ヒープ使用率に関する大まかなメトリクスをリサイクルするためのツールです。ヒープ割り当てが増加すると、heapused フィールドにヒープのサイズが記録されます。このフィールドは RAM 内のバイト数を記録し、MB に変換できます。
結果は異なる場合があります。 32GB RAM を搭載した Windows 10 ラップトップでは、次の結果が得られます。
Heap allocated 4 GB Heap allocated 4.01 GB <--- Last few GCs ---> [18820:000001A45B4680A0] 26146 ms: Mark-sweep (reduce) 4103.7 (4107.3) -> 4103.7 (4108.3) MB, 1196.5 / 0.0 ms (average mu = 0.112, current mu = 0.000) last resort GC in old space requested <--- JS stacktrace ---> FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
ここでは、ガベージ コレクターは最後の手段としてメモリを圧縮しようとしますが、最終的には諦めて「ヒープ メモリ不足」をスローします。 " 例外。このプロセスは 4.1 GB の制限に達し、サービスがハングアップすることを認識するまでに 26.6 秒かかりました。
上記の結果に至った理由の一部はまだ不明です。 V8 ガベージ コレクターは当初、厳しいメモリ制約のある 32 ビット ブラウザ プロセスで実行されていました。これらの結果は、メモリ制限が従来のコードから引き継がれた可能性があることを示唆しています。
執筆時点では、上記のコードは最新の LTS ノード バージョンで実行されており、64 ビットの実行可能ファイルを使用しています。理論的には、64 ビット プロセスは 4 GB を超えるスペースを割り当て、16 TB のアドレス スペースまで簡単に拡張できるはずです。
node index.js --max-old-space-size=8000
これにより、最大制限が 8GB に設定されます。これを行うときは注意してください。私のラップトップには 32GB のスペースがあります。 RAM で実際に利用可能なスペースの量に設定することをお勧めします。物理メモリが使い果たされると、プロセスは仮想メモリを通じてディスク領域を占有し始めます。制限を高く設定しすぎると、コンピューターを変更する新たな理由が生まれます。ここでは、コンピューターが煙を発するのを回避しようとします~
8GB 制限を使用してコードを再度実行してみましょう:
Heap allocated 7.8 GB Heap allocated 7.81 GB <--- Last few GCs ---> [16976:000001ACB8FEB330] 45701 ms: Mark-sweep (reduce) 8000.2 (8005.3) -> 8000.2 (8006.3) MB, 1468.4 / 0.0 ms (average mu = 0.211, current mu = 0.000) last resort GC in old space requested <--- JS stacktrace ---> FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
今回のヒープ サイズはほぼ 8GB ですが、そこまでではありません。非常に多くのメモリを割り当てるため、ノード プロセスにオーバーヘッドがあると思われます。今回はプロセスが完了するまでに 45.7 秒かかりました。
実稼働環境では、メモリが不足するまでに 1 分もかからない場合があります。これが、メモリ消費を監視して洞察を得ることが役立つ理由の 1 つです。メモリ消費量は時間の経過とともに徐々に増加し、問題があることに気づくまでに数日かかる場合があります。プロセスがクラッシュし続け、「ヒープ不足」例外がログに表示される場合は、コードにメモリ リークがある可能性があります。
プロセスはより多くのデータを処理するため、より多くのメモリを使用する可能性もあります。リソース消費が増加し続ける場合は、このモノリスをマイクロサービスに分割する時期が来るかもしれません。これにより、個々のプロセスのメモリ負荷が軽減され、ノードが水平方向にスケールできるようになります。
process.memoryUsage 的 heapUsed 字段还是有点用的,调试内存泄漏的一个方法是将内存指标放在另一个工具中以进行进一步处理。由于此实现并不复杂,因此主要解析下如何亲自实现。
const path = require("path"); const fs = require("fs"); const os = require("os"); const start = Date.now(); const LOG_FILE = path.join(__dirname, "memory-usage.csv"); fs.writeFile(LOG_FILE, "Time Alive (secs),Memory GB" + os.EOL, () => {}); // 请求-确认
为了避免将堆分配指标放在内存中,我们选择将结果写入 CSV 文件以方便数据消耗。这里使用了 writeFile 带有回调的异步函数。回调为空以写入文件并继续,无需任何进一步处理。 要获取渐进式内存指标,请将其添加到 console.log:
const elapsedTimeInSecs = (Date.now() - start) / 1000; const timeRounded = Math.round(elapsedTimeInSecs * 100) / 100; s.appendFile(LOG_FILE, timeRounded + "," + gbRounded + os.EOL, () => {}); // 请求-确认
上面这段代码可以用来调试内存泄漏的情况下,堆内存随着时间变化而增长。你可以使用一些分析工具来解析原生csv数据以实现一个比较漂亮的可视化。
如果你只是赶着看看数据的情况,直接用excel也可以,如下图:
在限制为4.1GB的情况下,你可以看到内存的使用率在短时间内呈线性增长。内存的消耗在持续的增长并没有变得平缓,这个说明了某个地方存在内存泄漏。在我们调试这类问题的时候,我们要寻找在分配在老世代结束时的那部分代码。
对象如果再在垃圾回收时幸存下来,就可能会一直存在,直到进程终止。
使用这段内存泄漏检测代码更具复用性的一种方法是将其包装在自己的时间间隔内(因为它不必存在于主循环中)。
setInterval(() => { const mu = process.memoryUsage(); // # bytes / KB / MB / GB const gbNow = mu[field] / 1024 / 1024 / 1024; const gbRounded = Math.round(gbNow * 100) / 100; const elapsedTimeInSecs = (Date.now() - start) / 1000; const timeRounded = Math.round(elapsedTimeInSecs * 100) / 100; fs.appendFile(LOG_FILE, timeRounded + "," + gbRounded + os.EOL, () => {}); // fire-and-forget }, TIME_INTERVAL_IN_MSEC);
要注意上面这些方法并不能直接在生产环境中使用,仅仅只是告诉你如何在本地环境调试内存泄漏。在实际实现时还包括了自动显示、警报和轮换日志,这样服务器才不会耗尽磁盘空间。
尽管上面的代码在生产环境中不可行,但我们已经看到了如何去调试内存泄漏。因此,作为替代方案,可以将 Node 进程包裹在 PM2 之类 的 守护进程 中。
当内存消耗达到限制时设置重启策略:
pm2 start index.js --max-memory-restart 8G
单位可以是 K(千字节)、M(兆字节)和 G(千兆字节)。进程重启大约需要 30 秒,因此通过负载均衡器配置多个节点以避免中断。
另一个漂亮的工具是跨平台的原生模块node-memwatch,它在检测到运行代码中的内存泄漏时触发一个事件。
const memwatch = require("memwatch"); memwatch.on("leak", function (info) { // event emitted console.log(info.reason); });复制代码
事件通过leak触发,并且它的回调对象中有一个reason会随着连续垃圾回收的堆增长而增长。
使用 AppSignal 的 Magic Dashboard 诊断内存限制
AppSignal 有一个神奇的仪表板,用于监控堆增长的垃圾收集统计信息。
上图显示请求在 14:25 左右停止了 7 分钟,允许垃圾回收以减少内存压力。当对象在旧的空间中停留太久并导致内存泄漏时,仪表板也会暴露出来。
在这篇文章中,我们首先了解了 V8 垃圾回收器的作用,然后再探讨堆内存是否存在限制以及如何扩展内存分配限制。
最后,我们使用了一些潜在的工具来密切关注 Node.js 中的内存泄漏。我们看到内存分配的监控可以通过使用一些粗略的工具方法来实现,比如memoryUsage一些调试方法。在这里,分析仍然是手动实现的。
另一种选择是使用 AppSignal 等专业工具,它提供监控、警报和漂亮的可视化来实时诊断内存问题。
希望你喜欢这篇关于内存限制和诊断内存泄漏的快速介绍。
更多node相关知识,请访问:nodejs 教程!!
以上がNode でのヒープ メモリ割り当てを調査し、メモリ制限について話します。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。