关于前端:Node-Weekly-417你需要了解的Nodejs内存限制

45次阅读

共计 4978 个字符,预计需要花费 13 分钟才能阅读完成。

🥳 欢送有趣味的小伙伴,一起做点有意义的事!本文译者:oil-oil

我发动了一个 周刊翻译打算,仓库地址,拜访地址

当初还很缺气味相投的小伙伴,纯属个人兴趣,当然对于晋升英语和前端技能也会有帮忙,要求:英语不要差的离谱、github 纯熟应用、有恒心、虚心、对本人做的事负责。

想参加的小伙伴,能够 wx 私信,也能够给仓库发 issue 留言,我博客也有具体的集体联系方式:daodaolee.cn
在本篇文章中,我将摸索一下 Node 中的堆内存调配,而后试试看把内存进步到硬件能接受的极限。而后咱们将找到一些实用的办法来监控 Node 的过程以调试内存相干问题。

OK,筹备实现就发车!

大家能够在仓库拉一下相干代码 clone the code from my GitHub.

V8 垃圾回收简介

首先,简略介绍一下 V8 垃圾回收器。内存的存储调配形式是堆(heap),堆被分为几个世代(generational)区域。
对象在它的生命周期中随着年龄的变动,它所属的世代也有所不同。

世代中分为年轻一代和老一代,而年老的一代还分为了新生代和两头代。随着对象在垃圾回收中幸存下来,它们也会退出老一代。

世代假说的根本准则是大多数对象都是年老的。V8 垃圾回收器基于这一点,只晋升在垃圾回收中幸存下来的对象。随着对象被复制到相邻区域,它们最终会进入老一代。

在 Node 中内存耗费次要分为三个方面:

  • 代码 - 代码执行时所在的地位
  • 调用栈 - 用于寄存具备原始类型(数字,字符串或布尔值)的函数和局部变量
  • 堆内存

堆内存是咱们明天的次要关注点。
当初您对垃圾回收器有了更多的理解,是时候在堆上调配一些内存了!

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 也能够,如下图:

在限度为 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 有一个神奇的仪表板,用于监控堆增长的垃圾收集统计信息。

上图显示申请在 14:25 左右进行了 7 分钟,容许垃圾回收以缩小内存压力。当对象在旧的空间中停留太久并导致内存透露时,仪表板也会裸露进去。

总结:解决 Node.js 内存限度和透露

在这篇文章中,咱们首先理解了 V8 垃圾回收器的作用,而后再探讨堆内存是否存在限度以及如何扩大内存调配限度。

最初,咱们应用了一些潜在的工具来亲密关注 Node.js 中的内存透露。咱们看到内存调配的监控能够通过应用一些粗略的工具办法来实现,比方 memoryUsage 一些调试办法。在这里,剖析依然是手动实现的。

另一种抉择是应用 AppSignal 等业余工具,它提供监控、警报和丑陋的可视化来实时诊断内存问题。

心愿你喜爱这篇对于内存限度和诊断内存透露的疾速介绍。

相干链接

原文链接

翻译打算原文

正文完
 0