乐趣区

关于node.js:Nodejs-应用的内存泄漏问题的检测方法

Debugging Memory Leaks in Node.js Applications

Node.js 是一个基于 Chrome 的 V8 JavaScript 引擎构建的平台,用于轻松构建疾速且可扩大的网络应用程序。

Google 的 V8 ——Node.js 背地的 JavaScript 引擎, 它的性能令人难以置信,并且 Node.js 在许多用例中运行良好的起因有很多,但您总是受到堆大小的限度。当您须要在 Node.js 应用程序中解决更多申请时,您有两种抉择:垂直扩大或者程度扩大。程度扩大意味着您必须运行更多并发应用程序实例。如果做得好,您最终可能满足更多申请。垂直扩大意味着您必须进步应用程序的内存应用和性能或减少应用程序实例可用的资源。

Node.js Memory Leak Debugging Arsenal

MEMWATCH

如果您搜寻“如何在 node.js 中查找透露”,您可能会找到的第一个工具是 memwatch。原来的包早就废除了,不再保护。然而,您能够在 GitHub 的存储库分叉列表中轻松找到它的更新版本。这个模块很有用,因为它能够在看到堆增长超过 5 次连续垃圾收集时收回透露事件。

HEAPDUMP

很棒的工具,它容许 Node.js 开发人员拍摄堆快照并在当前应用 Chrome 开发人员工具查看它们。

NODE-INSPECTOR

甚至是 heapdump 的更有用的代替计划,因为它容许您连贯到正在运行的应用程序,进行堆转储,甚至能够即时调试和从新编译它。

Taking“node-inspector”for a Spin

可怜的是,您将无奈连贯到在 Heroku 上运行的生产应用程序,因为它不容许将信号发送到正在运行的过程。然而,Heroku 并不是惟一的托管平台。

为了体验 node-inspector 的实际操作,咱们将应用 restify 编写一个简略的 Node.js 应用程序,并在其中搁置一些内存透露源。这里所有的试验都是用 Node.js v0.12.7 进行的,它是针对 V8 v3.28.71.19 编译的。

var restify = require('restify');

var server = restify.createServer();

var tasks = [];

server.pre(function(req, res, next) {tasks.push(function() {return req.headers;});

  // Synchronously get user from session, maybe jwt token
  req.user = {
    id: 1,
    username: 'Leaky Master',
  };

  return next();});

server.get('/', function(req, res, next) {res.send('Hi' + req.user.username);
  return next();});

server.listen(3000, function() {console.log('%s listening at %s', server.name, server.url);
});

这里的利用很简略,有很显著的泄露。阵列工作会随着应用程序生命周期的增长而增长,导致它变慢并最终解体。问题是咱们不仅透露了闭包,还透露了整个申请对象。

V8 中的 GC 应用 stop-the-world 策略,因而这意味着内存中的对象越多,收集垃圾所需的工夫就越长。在上面的日志中,您能够分明地看到,在应用程序生命周期开始时,收集垃圾均匀须要 20 毫秒,但几十万个申请之后须要大概 230 毫秒。因为 GC,试图拜访咱们应用程序的人当初必须期待 230 毫秒。您还能够看到每隔几秒就会调用一次 GC,这意味着每隔几秒用户就会在拜访咱们的应用程序时遇到问题。提早会越来越大,直到应用程序解体。

当应用 –trace_gc 标记启动 Node.js 应用程序时,会打印这些日志行:

node –trace_gc app.js

让咱们假如咱们曾经应用这个标记启动了咱们的 Node.js 应用程序。在将应用程序与节点查看器连贯之前,咱们须要将 SIGUSR1 信号发送给正在运行的过程。如果您在集群中运行 Node.js,请确保您连贯到隶属过程之一。

kill -SIGUSR1 $pid # Replace $pid with the actual process ID

通过这样做,咱们使 Node.js 应用程序(精确地说是 V8)进入调试模式。在此模式下,应用程序会应用 V8 调试协定主动关上端口 5858。

咱们的下一步是运行 node-inspector,它将连贯到正在运行的应用程序的调试界面,并在端口 8080 上关上另一个 Web 界面。

$ node-inspector
Node Inspector v0.12.2
Visit http://127.0.0.1:8080/?ws=127… to start debugging.

如果应用程序在生产环境中运行并且您有防火墙,咱们能够通过隧道将近程端口 8080 连贯到本地主机:

ssh -L 8080:localhost:8080 admin@example.com

当初,您能够关上 Chrome 网络浏览器并齐全拜访附加到近程生产应用程序的 Chrome 开发工具。

Let’s Find a Leak!

V8 中的内存透露并不是咱们从 C/C++ 应用程序中晓得的真正的内存透露。在 JavaScript 中,变量不会成为 void,它们只会被“忘记”。咱们的指标是找到这些被开发人员忘记的变量。

在 Chrome 开发者工具中,咱们能够拜访多个分析器。咱们对记录堆调配特地感兴趣,它会随着工夫的推移运行并拍摄多个堆快照。这让咱们能够分明地看到哪些对象正在透露。

开始记录堆调配,让咱们应用 Apache Benchmark 在咱们的主页上模仿 50 个并发用户。

ab -c 50 -n 1000000 -k http://example.com/

在拍摄新快照之前,V8 会执行标记 - 革除垃圾收集,所以咱们必定晓得快照中没有旧垃圾。

Fixing the Leak on the Fly

在 3 分钟内收集堆调配快照后,咱们最终失去如下后果:

咱们能够分明地看到,堆中有一些微小的数组,还有很多 IncomingMessage、ReadableState、ServerResponse 和 Domain 对象。让咱们尝试剖析透露的起源。

在图表上从 20 秒到 40 秒抉择堆差别后,咱们只会看到从您启动分析器时起 20 秒后增加的对象。这样您就能够排除所有失常数据。

记下零碎中每种类型的对象有多少,咱们将过滤器从 20 秒扩大到 1 分钟。咱们能够看到,曾经相当宏大的阵列还在一直增长。在“(array)”下咱们能够看到有很多等距的对象“(object properties)”。这些对象是咱们内存透露的源头。

咱们也能够看到“(闭包)”对象也在快速增长。

查看字符串也可能很不便。在字符串列表下有很多“Hi Leaky Master”短语。这些也可能给咱们一些线索。

在咱们的例子中,咱们晓得字符串“Hi Leaky Master”只能在“GET /”路由下组装。

如果您关上保留器门路,您将看到此字符串以某种形式通过 req 援用,而后创立了上下文并将所有这些增加到一些微小的闭包数组中。

所以在这一点上咱们晓得咱们有某种微小的闭包数组。让咱们在“源”选项卡下实时为所有闭包命名。

实现代码编辑后,咱们能够按 CTRL+S 来保留和从新编译代码!

当初让咱们记录另一个堆调配快照,看看哪些闭包正在占用内存。

很显著 SomeKindOfClojure() 是咱们的 target。当初咱们能够看到 SomeKindOfClojure() 闭包被增加到全局空间中一些名为工作的数组中。

很容易看出这个数组是没有用的。咱们能够正文掉。然而咱们如何开释曾经占用的内存呢?很简略,咱们只需为任务分配一个空数组,下一次申请时它将被笼罩并在下一次 GC 事件后开释内存。

V8 堆分为几个不同的空间:

  • new space:这个空间比拟小,大小在 1MB 到 8MB 之间。大多数对象都在这里调配。
  • old pointer space:具备可能具备指向其余对象的指针的对象。如果对象在新空间中存活的工夫足够长,它就会被晋升到旧指针空间。
  • old data space:仅蕴含原始数据,如字符串、装箱数字和未装箱双精度数组。在新空间中在 GC 中存活足够长时间的对象也被挪动到这里。
  • large object space:在此空间中创立太大而无奈放入其余空间的对象。每个对象在内存中都有本人的 mmap 区域
  • code space:蕴含由 JIT 编译器生成的汇编代码。
  • Cell space, property cell space, map space:该空间蕴含单元格、属性单元格和地图。这用于简化垃圾收集。

每个空间由页面组成。页面是从操作系统应用 mmap 调配的内存区域。除了大对象空间中的页面外,每个页面的大小始终为 1MB。

V8 有两个内置的垃圾收集机制:Scavenge、Mark-Sweep 和 Mark-Compact。

Scavenge 是一种十分疾速的垃圾收集技术,能够解决 New Space 中的对象。Scavenge 是切尼算法的实现。这个想法很简略,New Space 被分成两个相等的半空间:To-Space 和 From-Space。当 To-Space 已满时,会产生 Scavenge GC。它只是替换 To 和 From 空间并将所有流动对象复制到 To-Space 或将它们晋升到旧空间之一,如果它们在两次革除中幸存下来,而后从空间中齐全删除。清理速度十分快,然而它们具备放弃双倍大小的堆和一直在内存中复制对象的开销。应用革除的起因是因为大多数对象都很年老。

Mark-Sweep 和 Mark-Compact 是 V8 中应用的另一种类型的垃圾收集器。另一个名称是 full garbage collector. 它标记所有流动节点,而后革除所有死节点并整顿内存碎片。

GC Performance and Debugging Tips

尽管对于 Web 应用程序来说,高性能可能不是什么大问题,但您依然心愿不惜一切代价防止透露。在 full GC 的标记阶段,应用程序实际上会暂停,直到垃圾收集实现。这意味着堆中的对象越多,执行 GC 所需的工夫就越长,用户期待的工夫也就越长。

ALWAYS GIVE NAMES TO CLOSURES AND FUNCTIONS

当所有闭包和函数都有名称时,查看堆栈跟踪和堆会容易得多。

db.query('GIVE THEM ALL', function GiveThemAllAName(error, data) {...})

AVOID LARGE OBJECTS IN HOT FUNCTIONS

现实状况下,您心愿防止在 hot function 外部应用大对象,以便所有数据都适宜新空间。所有 CPU 和内存绑定操作都应在后盾执行。还要防止 hot function 的去优化触发器,优化的 hot function 比未优化的 hot function 应用更少的内存。

AVOID POLYMORPHISM FOR IC’S IN HOT FUNCTIONS

内联缓存 (Inline Caches) 用于通过缓存对象属性拜访 obj.key 或某些简略函数来减速某些代码块的执行。

function x(a, b) {return a + b;}

x(1, 2); // monomorphic
x(1,“string”); // polymorphic, level 2
x(3.14, 1); // polymorphic, level 3

当 x(a,b) 第一次运行时,V8 创立了一个单态 IC。当您第二次调用 x 时,V8 会擦除旧 IC 并创立一个新的多态 IC,该 IC 反对整数和字符串两种类型的操作数。当您第三次调用 IC 时,V8 反复雷同的过程并创立另一个级别为 3 的多态 IC。

然而,有一个限度。在 IC 级别达到 5(能够应用 –max_inlining_levels 标记更改)后,该函数变得超态,不再被认为是可优化的。

直观上能够了解,单态函数运行速度最快,内存占用也更小。

DON’T ADD LARGE FILES TO MEMORY

这是不言而喻的,也是家喻户晓的。如果您有大文件要解决,例如一个大 CSV 文件,请逐行读取并以小块解决,而不是将整个文件加载到内存中。在极少数状况下,单行 csv 会大于 1mb,因而您能够将其放入新空间。

DO NOT BLOCK MAIN SERVER THREAD

如果您有一些须要一些工夫来解决的热门 API,例如调整图像大小的 API,请将其移至独自的线程或将其转换为后台作业。CPU 密集型操作会阻塞主线程,迫使所有其余客户期待并持续发送申请。未解决的申请数据会沉积在内存中,从而迫使 full GC 须要更长的工夫能力实现。

DO NOT CREATE UNNECESSARY DATA

我已经对 restify 有过奇怪的经验。如果您向有效 URL 发送数十万个申请,那么应用程序内存将迅速增长到数百兆字节,直到几秒钟后齐全 GC 启动,此时所有都会恢复正常。事实证明,对于每个有效的 URL,restify 会生成一个新的谬误对象,其中蕴含长堆栈跟踪。这迫使新创建的对象在大对象空间而不是新空间中调配。

在开发过程中拜访这些数据可能十分有帮忙,但在生产中显然不须要。因而规定很简略——除非您的确须要,否则不要生成数据。

总结

理解 V8 的垃圾收集和代码优化器的工作原理是进步应用程序性能的要害。V8 将 JavaScript 编译为原生程序集,在某些状况下,编写良好的代码能够取得与 GCC 编译的应用程序相当的性能。

更多 Jerry 的原创文章,尽在:” 汪子熙 ”:

退出移动版