>  기사  >  웹 프론트엔드  >  Node의 힙 메모리 할당을 살펴보고 메모리 제한에 대해 이야기해 보세요!

Node의 힙 메모리 할당을 살펴보고 메모리 제한에 대해 이야기해 보세요!

青灯夜游
青灯夜游앞으로
2022-01-11 19:21:114226검색

이 기사에서는 Node의 힙 메모리 할당을 살펴보고 Node.js의 메모리 제한에 대해 심층적으로 이해할 수 있습니다. 도움이 되기를 바랍니다.

Node의 힙 메모리 할당을 살펴보고 메모리 제한에 대해 이야기해 보세요!

이 기사에서는 Node의 힙 메모리 할당을 살펴본 다음 하드웨어가 감당할 수 있는 한도까지 메모리를 늘려 보겠습니다. 그런 다음 Node의 프로세스를 모니터링하여 메모리 관련 문제를 디버깅하는 몇 가지 실용적인 방법을 찾아보겠습니다.

좋아요, 준비하고 출발하세요!

웨어 하우스에서 관련 코드를 가져오고 내 GitHub에서 코드를 복제할 수 있습니다:

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

V8 가비지 수집 소개

먼저 우선 간략한 소개 V8 가비지 컬렉터를 확인해 보세요. 메모리의 저장 할당 방법은 힙이며, 힙은 여러 세대 영역으로 나누어집니다. 객체는 수명 주기 동안 연령이 변하며, 객체가 속한 세대도 변합니다.

세대는 청년세대와 기성세대로 나뉘고, 청년세대도 신세대와 중년세대로 나뉜다. 개체는 가비지 수집 후에도 살아남으면서 이전 세대에 합류하게 됩니다.

Node의 힙 메모리 할당을 살펴보고 메모리 제한에 대해 이야기해 보세요!

세대 가설의 기본 원리는 대부분의 대상이 젊다는 것입니다. V8 가비지 수집기는 가비지 수집에서 살아남은 개체만 승격하여 이를 기반으로 합니다. 객체가 인접한 지역으로 복사되면 결국 Old Generation에 속하게 됩니다.

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은 힙으로 이동하고 가비지 수집에서 살아남을 수 있습니다. 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

힙 메모리에 제한이 있나요?

이제 용감한 테스트를 위해 Node 프로세스를 한계까지 밀어넣고 힙 메모리가 부족한 위치를 확인합니다.

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

위 코드에서는 가비지 수집을 위해 40ms 간격으로 약 10MB를 할당했습니다. 충분한 시간이 제공됩니다. 살아남은 개체를 이전 세대로 승격합니다. process.memoryUsage는 힙 사용률에 대한 대략적인 지표를 재활용하는 도구입니다. 힙 할당이 증가함에 따라 heapUsed 필드는 힙 크기를 기록합니다. 이 필드는 RAM의 바이트 수를 기록하며 MB로 변환될 수 있습니다.

결과는 다를 수 있습니다. 32GB RAM이 장착된 Windows 10 노트북에서는 다음과 같은 결과를 얻을 수 있습니다.

node index.js --max-old-space-size=8000

여기에서 가비지 수집기는 최후의 수단으로 메모리 압축을 시도한 후 최종적으로 포기하고 "힙 부족" 예외를 발생시킵니다. 이 과정이 4.1GB 한도에 도달해 서비스가 중단될 것임을 깨닫는 데 26.6초가 걸렸다. 위 결과의 원인 중 일부는 아직 알려지지 않았습니다. V8 가비지 수집기는 원래 엄격한 메모리 제약이 있는 32비트 브라우저 프로세스에서 실행되었습니다. 이러한 결과는 메모리 제한이 레거시 코드에서 상속되었을 수 있음을 시사합니다.

작성 당시 위 코드는 최신 LTS Node 버전에서 실행 중이며 64비트 실행 파일을 사용하고 있습니다. 이론적으로 64비트 프로세스는 4GB 이상의 공간을 할당할 수 있어야 하며 16TB의 주소 공간까지 쉽게 확장할 수 있어야 합니다.

메모리 할당 제한 확장

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로 설정합니다. 이 작업을 수행할 때는 주의하세요. 내 노트북에는 32GB의 공간이 있습니다. RAM에서 실제로 사용 가능한 공간의 양으로 설정하는 것이 좋습니다. 물리적 메모리가 소진되면 프로세스는 가상 메모리를 통해 디스크 공간을 차지하기 시작합니다. 제한을 너무 높게 설정하면 컴퓨터를 바꿔야 할 새로운 이유가 생깁니다. 여기서는 컴퓨터의 흡연을 방지하려고 합니다~

8GB 제한으로 코드를 다시 실행해 보겠습니다.

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, () => {}); // 请求-确认
🎜이번에는 힙 크기가 거의 8GB까지, 하지만 그렇지는 않습니다. 너무 많은 메모리를 할당하기 때문에 Node 프로세스에 약간의 오버헤드가 있는 것 같습니다. 이번에는 프로세스를 완료하는 데 45.7초가 걸렸습니다. 🎜🎜프로덕션 환경에서는 메모리를 모두 사용하는 데 1분도 채 걸리지 않을 수 있습니다. 이것이 메모리 소비를 모니터링하고 통찰력을 얻는 것이 도움이 되는 한 가지 이유입니다. 메모리 소비는 시간이 지남에 따라 천천히 증가하며 문제가 있음을 알기까지 며칠이 걸릴 수 있습니다. 프로세스가 계속 충돌하고 로그에 "힙 부족" 예외가 나타나면 코드에 메모리 누수가 있을 수 있습니다. 🎜🎜프로세스는 더 많은 데이터를 처리하므로 더 많은 메모리를 차지할 수도 있습니다. 리소스 소비가 계속 증가한다면 이제 이 모놀리스를 마이크로서비스로 분할해야 할 때일 수 있습니다. 이렇게 하면 개별 프로세스에 대한 메모리 부담이 줄어들고 노드를 수평으로 확장할 수 있습니다. 🎜

如何跟踪 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으로 문의하시기 바랍니다. 삭제