处理Node.js中内存泄漏的实用指南

内存泄漏类似于寄生在应用程序上的实体。它们首先悄悄地渗透到系统中,而不会造成任何伤害。但是,如果泄漏证明足够严重,则会使应用程序遭受灾难。例如-强烈放慢速度或简单地“杀死”它。 这篇文章的作者(我们今天将要翻译的译本)建议谈论JavaScript中的内存泄漏。特别是,我们将讨论JavaScript中的内存管理,如何识别实际应用程序中的内存泄漏以及如何处理内存泄漏。





什么是内存泄漏?


从广义上讲,内存泄漏是分配给该应用程序不再需要的应用程序的一块内存,但是无法将其返回给操作系统以供将来使用。换句话说,它是由应用程序捕获的存储块,无意在将来使用此存储。

内存管理


内存管理是一种用于将系统内存分配给需要它的应用程序的机制,也是一种用于将不必要的内存返回给操作系统的机制。内存管理有很多方法。使用哪种方法取决于所使用的编程语言。以下是几种常见的内存管理方法的概述:

  • . . . , . C C++. , , malloc free, .
  • . , , , . , , , . , , , , . . — JavaScript, , JVM (Java, Scala, Kotlin), Golang, Python, Ruby .
  • 应用内存所有权的概念。使用这种方法,每个变量都应具有自己的所有者。一旦所有者超出范围,变量中的值就会被破坏,从而释放内存。这个想法被用在Rust中。

还有其他用于不同编程语言的内存管理方法。例如,C ++ 11使用RAII习惯用法,而Swift使用ARC机制但是谈论它已经超出了本文的范围。为了比较上面的内存管理方法,以了解它们的优缺点,我们需要另外一篇文章。

JavaScript是一种Web程序员无法想象的语言,它使用垃圾回收的思想。因此,我们将更多地讨论这种机制的工作原理。

JavaScript垃圾回收


如前所述,JavaScript是一种使用垃圾回收概念的语言。在JS程序运行期间,会定期启动一种称为垃圾收集器的机制。他发现可以从应用程序代码访问分配的内存的哪些部分。也就是说,引用了哪些变量。如果垃圾收集器发现不再可以从应用程序代码访问某个内存,它将释放该内存。可以使用两种主要算法来实现上述方法。第一种是所谓的标记和扫描算法。它在JavaScript中使用。第二个是引用计数。它用于Python和PHP。


标记和扫描

算法的标记(标记)和扫描(清理)阶段在实施标记算法时,window首先创建一个由全局环境变量表示的根节点列表(这是浏览器中的一个对象),然后将结果树从根爬到标记有所有元素的叶节点在途中遇到物体。释放堆中未被标签对象占用的内存。

Node.js应用程序中的内存泄漏


迄今为止,我们已经分析了与内存泄漏和垃圾回收相关的足够的理论概念。所以-我们准备看一下它们在实际应用程序中的外观。在本节中,我们将编写一个存在内存泄漏的Node.js服务器。我们将尝试使用各种工具来识别此泄漏,然后将其消除。

with熟悉存在内存泄漏的代码


为了演示,我编写了一个具有内存泄漏路由的Express服务器。我们将调试该服务器。

const express = require('express')

const app = express();
const port = 3000;

const leaks = [];

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

有一个数组leaks超出了API请求处理代码的范围。结果,每次执行相应的代码时,都会将新元素简单地添加到数组中。永远不会清除该数组。由于退出请求处理程序后,指向该数组的链接不会消失,因此垃圾收集器永远不会释放其使用的内存。

▍通话内存泄漏


这里我们来最有趣的。已经写了很多文章,介绍了使用炮兵之node --inspect类的请求填充服务器后,如何使用它来调试服务器内存泄漏。但是这种方法有一个重要的缺点。假设您有一个具有数千个端点的API服务器。他们每个人都有很多参数,将要调用的特定代码取决于其功能。结果,在实际情况下,如果开发人员不知道内存泄漏在哪里,他将不得不使用所有可能的参数组合来多次访问每个API,以填充内存。对于我来说,这样做并不容易。但是,通过使用类似的方法可以简化此问题的解决方案goreplay-一种允许您记录和“播放”实际流量的系统。

为了解决我们的问题,我们将在生产中进行调试。也就是说,我们将允许我们的服务器在实际使用期间使内存溢出(因为它会收到各种API请求)。在发现分配给它的内存量可疑增加之后,我们将进行调试。

▍堆转储


为了了解什么是堆转储,我们首先需要找出堆概念的含义。如果您尽可能简单地描述此概念,那么事实证明堆是分配内存的所有内容所在的地方。所有这些都在堆上,直到垃圾收集器从其中删除了所有不必要的东西。堆转储只是堆当前状态的快照。转储包含所有内部变量和程序员声明的变量。它代表接收到转储时在堆上分配的所有内存。

结果,如果我们能够以某种方式将刚开始的服务器的堆转储与已经运行了很长时间并且内存溢出的服务器堆的转储进行比较,那么我们就可以识别出应用程序不需要但可被垃圾收集器删除的可疑对象。

在继续对话之前,让我们谈谈如何创建堆转储。为了解决这个问题,我们将使用npm包heapdump,它允许您以编程方式获取服务器堆的转储。

安装软件包:

npm i heapdump

我们将对服务器代码进行一些更改,以允许我们使用此程序包:

const express = require('express');
const heapdump = require("heapdump");

const app = express();
const port = 3000;

const leaks = [];

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

app.get('/heapdump', (req, res) => {
  heapdump.writeSnapshot(`heapDump-${Date.now()}.heapsnapshot`, (err, filename) => {
    console.log("Heap dump of a bloated server written to", filename);

    res.status(200).send({msg: "successfully took a heap dump"})
  });
});

app.listen(port, () => {
  heapdump.writeSnapshot(`heapDumpAtServerStart.heapsnapshot`, (err, filename) => {
    console.log("Heap dump of a fresh server written to", filename);
  });
});

在这里,我们使用此软件包来转储新启动的服务器。我们还创建了一个API,/heapdump旨在在访问堆时创建一个堆。当我们意识到服务器开始消耗过多内存时,我们将使用此API。

如果您的服务器在Kubernetes集群中运行,那么如果不付出额外的努力,您将无法转向其服务器正在运行的那个pod占用了太多内存。为此,您可以使用端口转发此外,由于将无法访问下载转储文件所需的文件系统,因此最好将这些文件上传到外部云存储(如S3)。

▍内存泄漏检测


现在,服务器已部署。他已经工作了几天。它接收到很多请求(在我们的例子中,只有相同类型的请求),并且我们注意到服务器消耗的内存量的增加。可以使用Express Status MonitorClinicPrometheus等监视工具检测内存泄漏。之后,我们调用API来转储堆。此转储将包含垃圾收集器无法删除的所有对象。

这是查询创建转储的样子:

curl --location --request GET 'http://localhost:3000/heapdump'

创建堆转储时,将强制运行垃圾收集器。因此,我们不必担心将来可能被垃圾收集器删除但仍在堆上的那些对象。那就是-关于在工作时不会发生内存泄漏的对象。

在我们都可以使用两个转储(一个新启动的服务器的转储和已经工作了一段时间的服务器的转储)之后,我们可以开始比较它们。

获取内存转储是一项阻塞操作,需要大量内存才能完成。因此,必须谨慎执行。您可以在此处阅读有关此操作过程中可能遇到的问题的更多信息

启动Chrome,然后按键。F12这将导致开发人员工具的发现。在这里,您需要转到选项卡Memory并加载两个内存快照。


下载内存的Chrome开发者工具的内存选项卡上转储

下载这两个快照之后,您需要更改perspectiveComparison并单击,未来一段时间工作的服务器的内存的快照。


开始比较快照

在这里,我们可以分析该列Constructor并查找垃圾收集器无法删除的对象。这些对象中的大多数将由节点使用的内部链接表示。在这里使用一个技巧很有用,其中包括按field对列表进行排序Alloc. Size这将快速找到使用最多内存的对象。如果先扩展block(array),然后再扩展-(object elements),您将看到一个leaks包含大量对象的数组,这些对象无法使用垃圾回收器删除。


对可疑数组的分析

此技术将使我们能够进入数组,leaks并了解使用错误的操作会导致内存泄漏。

▍修复内存泄漏


现在我们知道“罪魁祸首”是一个数组leaks,我们可以分析代码并发现问题在于数组是在请求处理程序之外声明的。结果,事实证明从不删除指向它的链接。解决此问题非常简单-只需将数组的声明传输到处理程序即可:

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  const leaks = [];

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

为了验证所采取措施的有效性,重复上述步骤并再次比较堆映像就足够了。

摘要


内存泄漏以不同的语言发生。特别是-使用垃圾回收机制的垃圾回收站。例如,在JavaScript中。通常,修复泄漏并不困难-真正的困难只有在您搜索它时才会出现。

在本文中,您熟悉了内存管理的基础知识以及如何用不同的语言组织内存管理。在这里,我们再现了内存泄漏的真实情况,并描述了一种故障排除方法。

亲爱的读者们!您是否在Web项目中遇到内存泄漏?


All Articles