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

2022-01-11 22:00:27
本篇文章帶大家探索一下中的堆記憶體分配,深入瞭解一下Node.js中的記憶體限制,希望對大家有所幫助!

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

OK,準備完成就發車!

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

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

V8 垃圾回收簡介

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

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

1.png

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

在中記憶體消耗主要分為三個方面:

  • 程式碼-程式碼執行時所在的位置
  • 呼叫棧-用於存放具有原始型別(數位,字串或布林值)的函數和區域性變數
  • 堆記憶體

堆記憶體是我們今天的主要關注點。 現在您對垃圾回收器有了更多的瞭解,是時候在堆上分配一些記憶體了!

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

2.png

在限制為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 有一個神奇的儀表板,用於監控堆增長的垃圾收集統計資訊

3.png

上圖顯示請求在 14:25 左右停止了 7 分鐘,允許垃圾回收以減少記憶體壓力。當物件在舊的空間中停留太久並導致記憶體漏失時,儀表板也會暴露出來。

總結:解決 Node.js 記憶體限制和洩漏

在這篇文章中,我們首先了解了 V8 垃圾回收器的作用,然後再探討堆記憶體是否存在限制以及如何擴充套件記憶體分配限制。

最後,我們使用了一些潛在的工具來密切關注 Node.js 中的記憶體漏失。我們看到記憶體分配的監控可以通過使用一些粗略的工具方法來實現,比如memoryUsage一些偵錯方法。在這裡,分析仍然是手動實現的。

另一種選擇是使用 AppSignal 等專業工具,它提供監控、警報和漂亮的視覺化來實時診斷記憶體問題。

希望你喜歡這篇關於記憶體限制和診斷記憶體漏失的快速介紹。

更多node相關知識,請存取:!!

以上就是探索一下Node中的堆記憶體分配,聊聊記憶體限制!的詳細內容,更多請關注TW511.COM其它相關文章!