首頁  >  文章  >  web前端  >  探索一下Node中的堆記憶體分配,聊聊記憶體限制!

探索一下Node中的堆記憶體分配,聊聊記憶體限制!

青灯夜游
青灯夜游轉載
2022-01-11 19:21:114210瀏覽

這篇文章帶大家去探索一下Node中的堆記憶體分配,深入了解Node.js中的記憶體限制,希望對大家有幫助!

探索一下Node中的堆記憶體分配,聊聊記憶體限制!

在本篇文章中,我將探索Node中的堆記憶體分配,然後試試看把記憶體提高到硬體能承受的極限。然後我們將找到一些實用的方法來監控 Node 的進程以偵錯記憶體相關問題。

OK,準備完成就發車!

大家可以在倉庫裡拉一下相關程式碼clone the code from my GitHub:

https://github.com/beautifulcoder/node-memory-limitations

V8 垃圾回收簡介

首先,簡單介紹一下V8垃圾回收器。記憶體的儲存分配方式是堆(heap),堆被分成幾個世代(generational)區域。 物件在它的生命週期中隨著年齡的變化,它所屬的世代也有所不同。

世代中分為年輕一代和老一代,而年輕的一代也分為了新生代和中間代。隨著物件在垃圾回收中倖存下來,它們也會加入老一代。

探索一下Node中的堆記憶體分配,聊聊記憶體限制!

世代假說的基本原則是大多數物件都是年輕的。 V8 垃圾回收器基於這一點,只提昇在垃圾回收中倖存下來的物件。隨著物件被複製到相鄰區域,它們最終會進入舊一代。

Nodejs中記憶體消耗主要分為三個面向:

  • #程式碼-程式碼執行時所在的位置
  • ##呼叫堆疊-用於存放具有原始類型(數字,字串或布林值)的函數和局部變數
  • 堆記憶體
堆記憶體是我們今天的主要關注點。 現在您對垃圾回收器有了更多的了解,是時候在堆上分配一些記憶體了!

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將進入堆中並且可能在垃圾回收中倖存下來。

堆記憶體有限制嗎?

現在進行勇敢測試-將Node 行程推到極限看看在哪個地方會耗盡堆記憶體:

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 記憶體的 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.1GB 的限制,需要 26.6 秒才能意識到要把服務掛掉了。

導致以上結果的原因有些還未知。 V8 垃圾回收器最初運行在具有嚴格記憶體限制的 32 位元瀏覽器進程中。這些結果表明記憶體限制可能已經從遺留程式碼中繼承下來。

在撰寫本文時,上述程式碼在最新的 LTS Node 版本下運行,並且使用的是 64 位元可執行檔。從理論上講,一個 64 位元進程應該能夠分配超過 4GB 的空間,並且可以輕鬆地成長到 16 TB 的位址空間。

擴大記憶體分配限制
node index.js --max-old-space-size=8000

這將最大限制設為 8GB。這樣做時要小心。我的筆記型電腦有 32GB的空間。我建議將其設定為 RAM 中實際可用的空間。一旦實體記憶體耗盡,進程就會開始透過虛擬記憶體佔用磁碟空間。如果您將限制設定得太高,你就get了換電腦的新理由,這裡咱們盡量避免電腦冒煙了哈~

我們再用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,但沒完全達到。我懷疑是Node 進程中有一些開銷用於分配這麼多記憶體。這次進程結束需要 45.7 秒。

在生產環境中,記憶體全部用完可能不會少於一分鐘。這就是監控和洞察記憶體消耗有幫助的原因之一。記憶體消耗會隨著時間的推移緩慢增長,並且可能需要幾天才能知道存在問題。如果進程不斷崩潰且日誌中出現「堆記憶體不足」異常,則程式碼中可能存在記憶體洩漏。

進程也可能會佔用更多內存,因為它正在處理更多資料。如果資源消耗繼續成長,可能是時候將這個單體分解為微服務了。這將減少單一進程的記憶體壓力,並允許節點水平擴展。

如何跟踪 Node.js 内存泄漏

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也可以,如下图:

探索一下Node中的堆記憶體分配,聊聊記憶體限制!

在限制为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.js 内存泄漏

尽管上面的代码在生产环境中不可行,但我们已经看到了如何去调试内存泄漏。因此,作为替代方案,可以将 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 有一个神奇的仪表板,用于监控堆增长的垃圾收集统计信息

探索一下Node中的堆記憶體分配,聊聊記憶體限制!

上图显示请求在 14:25 左右停止了 7 分钟,允许垃圾回收以减少内存压力。当对象在旧的空间中停留太久并导致内存泄漏时,仪表板也会暴露出来。

总结:解决 Node.js 内存限制和泄漏

在这篇文章中,我们首先了解了 V8 垃圾回收器的作用,然后再探讨堆内存是否存在限制以及如何扩展内存分配限制。

最后,我们使用了一些潜在的工具来密切关注 Node.js 中的内存泄漏。我们看到内存分配的监控可以通过使用一些粗略的工具方法来实现,比如memoryUsage一些调试方法。在这里,分析仍然是手动实现的。

另一种选择是使用 AppSignal 等专业工具,它提供监控、警报和漂亮的可视化来实时诊断内存问题。

希望你喜欢这篇关于内存限制和诊断内存泄漏的快速介绍。

更多node相关知识,请访问:nodejs 教程!!

以上是探索一下Node中的堆記憶體分配,聊聊記憶體限制!的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除