调试Node.js应用程序中的内存泄漏Node.js是一个基于Chrome的V8JavaScript引擎构建的平台,用于轻松构建快速且可扩展的Web应用程序。谷歌的V8——Node.js背后的JavaScript引擎,具有令人难以置信的性能,Node.js在许多用例中运行良好的原因有很多,但你总是受到堆大小的限制。当您需要在Node.js应用程序中处理更多请求时,您有两个选择:垂直扩展或水平扩展。水平扩展意味着您必须运行更多并发的应用程序实例。如果做得好,您最终可以满足更多的请求。垂直扩展意味着您必须增加应用程序的内存使用和性能,或者增加应用程序实例可用的资源。Node.js内存泄漏调试ArsenalMEMWATCH如果您搜索“如何查找node.js中的泄漏”,您可能会找到的第一个工具是memwatch。原始包早已被弃用,不再维护。但是,您可以在GitHub的存储库分支列表中轻松找到它的更新版本。这个模块很有用,因为当它看到堆增长超过5个连续的垃圾收集时,它可以发出泄漏事件。HEAPDUMP是一个很棒的工具,它允许Node.js开发人员拍摄堆快照并稍后使用Chrome开发人员工具检查它们。NODE-INSPECTOR甚至是heapdump更有用的替代品,因为它允许您附加到正在运行的应用程序,进行堆转储,甚至可以即时调试和重新编译它。使用“node-inspector”进行旋转不幸的是,您将无法连接到在Heroku上运行的生产应用程序,因为它不允许向正在运行的进程发送信号。然而,Heroku并不是唯一的托管平台。为了实际体验节点检查器,我们将使用restify编写一个简单的Node.js应用程序,并将一些内存泄漏源放入其中。这里的所有实验都是使用针对V8v3.28.71.19编译的Node.jsv0.12.7执行的。varrestify=require('restify');varserver=restify.createServer();vartasks=[];server.pre(function(req,res,next){tasks.push(function(){returnreq.headers;});//从session中同步获取用户,可能是jwttokenreq.user={id:1,username:'LeakyMaster',};returnnext();});server.get('/',function(req,res,next){res.send('Hi'+req.user.username);returnnext();});server.listen(3000,function(){console.log('%slisteningat%s',server.name,server.url);});这里的应用很简单,漏洞很明显。数组任务会在应用程序的整个生命周期内增长,导致它变慢并最终崩溃。问题是我们不仅泄露了闭包,还泄露了整个请求对象。V8中的GC使用了stop-the-world策略,所以这意味着内存中的对象越多,垃圾收集所需的时间就越长。在下面的日志中,你可以清楚地看到垃圾回收在应用程序生命周期开始时平均需要20ms,但是在几十万个请求之后大约需要230ms。由于GC,尝试访问我们应用程序的人现在必须等待230毫秒。您还可以看到每隔几秒调用一次GC,这意味着每隔几秒用户就会在访问我们的应用程序时遇到问题。延迟会越来越大,直到应用程序崩溃。当使用--trace_gc标志启动Node.js应用程序时,将打印这些日志行:node--trace_gcapp.js让我们假设我们已经使用这个标志启动了我们的Node.js应用程序。在将应用程序与节点检查器连接之前,我们需要向正在运行的进程发送SIGUSR1信号。如果您在集群中运行Node.js,请确保您已连接到其中一个从属进程。kill-SIGUSR1$pid#将$pid替换为实际的进程ID通过这样做,我们将Node.js应用程序(准确地说是V8)置于调试模式。在此模式下,应用程序使用V8调试协议自动打开端口5858。我们下一步是运行node-inspector,它将连接到正在运行的应用程序的调试界面,并在端口8080上打开另一个Web界面。$node-inspectorNodeInspectorv0.12.2访问http://127.0.0.1:8080/?ws=127...开始调试。如果应用程序正在生产中运行并且您有防火墙,我们可以通过隧道连接远程端口8080连接到本地主机:ssh-L8080:localhost:8080admin@example.com您现在可以打开Chrome网络浏览器并拥有对附加到远程生产应用程序的Chrome开发工具。让我们找到一个漏洞!正如我们从C/C++应用程序中了解到的那样,V8中的内存泄漏并不是真正的内存泄漏。在JavaScript中,变量不会变成void,它们只是被“遗忘”了。我们的目标是找到开发人员忘记的这些变量。在Chrome开发者工具中,我们可以访问多个分析器。我们对记录堆分配特别感兴趣,它会随着时间的推移运行并拍摄多个堆快照。这使我们可以清楚地看到哪些对象正在泄漏。要开始记录堆分配,让我们使用ApacheBenchmark在我们的主页上模拟50个并发用户。ab-c50-n1000000-khttp://example.com/在拍摄新快照之前,V8执行标记清除垃圾收集,因此我们可以确定快照中没有旧垃圾。FixedtheLeakontheFly在3分钟内收集堆分配快照后,我们得到了这样的结果:我们可以清楚地看到堆中有一些巨大的数组,以及大量的IncomingMessage、ReadableState、ServerResponse和Domain对象。让我们尝试分析泄漏的来源。在图表上选择从20秒到40秒的HeapDiff后,我们将仅看到从您启动分析器起20秒后添加的对象。这样您就可以排除所有正常数据。注意系统中每种类型的对象数量,我们将过滤器从20秒延长到1分钟。我们可以看到已经相当大的阵列继续增长。在“(array)”下面我们可以看到有很多等距的对象“(objectproperties)”。这些对象是我们内存泄漏的根源。我们还可以看到“(closure)”对象也在快速增长。查看字符串也可能很方便。字符串列表下有很多“HiLeakyMaster”短语。这些或许也能给我们一些线索。在我们的例子中,我们知道字符串“HiLeakyMaster”只能在“GET/”路由下组装。如果你打开保留路径,你会看到这个字符串以某种方式通过req被引用,然后上下文被创建并添加到一些巨大的闭包数组中。所以在这一点上我们知道我们有一些巨大的闭包。让我们在“Sources”选项卡下命名我们所有的闭包。当我们完成代码编辑后,我们可以按CTRL+S保存并重新编译代码!现在让我们记录另一个堆分配快照,看看哪些闭包正在占用内存。显然SomeKindOfClojure()是我们的目标。现在我们可以看到SomeKindOfClojure()闭包被添加到全局空间中的一些名为tasks的数组中。很容易看出这个数组是无用的。我们可以把它注释掉。但是我们如何释放已经占用的内存呢?很简单,我们只需为任务分配一个空数组,它将在下一次请求时被覆盖,内存将在下一次GC事件后被释放。V8堆分为几个不同的空间:新空间:这个空间比较小,大小从1MB到8MB不等。大多数对象都分配在这里。旧指针空间:具有可能具有指向其他对象的指针的对象。如果对象在新空间中存活的时间足够长,它将被提升到旧指针空间。旧数据空间:仅包含原始数据,例如字符串、装箱数字和未装箱的双精度数组。在新空间中在GC中存活时间足够长的对象也将移至此处。largeobjectspace:在这个空间中创建太大而无法容纳在其他空间中的对象。每个对象在内存中都有自己的mmap区代码空间:包含JIT编译器生成的汇编代码。单元格空间、属性单元格空间、地图空间:该空间包含单元格、属性单元格和地图。这用于简化垃圾收集。每个空间由页面组成。页是操作系统使用mmap分配的内存区域。除大对象空间中的页面外,每个页面的大小始终为1MB。V8有两种内置的垃圾收集机制:Scavenge、Mark-Sweep和Mark-Compact。Scavenge是一种非常快速的垃圾回收技术,适用于NewSpace中的对象。Scavenge是切尼算法的一个实现。思路很简单,NewSpace被分成两个相等的半空间:To-Space和From-Space。ScavengeGC在To-Space已满时发生。它只是交换To和From空间并将所有活动对象复制到To-Space或将它们提升到旧空间之一,如果它们在两次清除中幸存下来,则将它们从空间中完全删除。清理非常快,但它们有维护双倍大小堆和不断复制内存中对象的开销。使用clear的原因是因为大多数对象都是年轻的。Mark-Sweep和Mark-Compact是V8中使用的另一种垃圾收集器。另一个名字是完整的垃圾收集器。它标记所有活节点,然后清除所有死节点并对内存进行碎片整理。GC性能和调试技巧虽然高性能对于Web应用程序来说可能不是什么大问题,但您仍然希望不惜一切代价避免泄漏。在fullGC的标记阶段,应用程序实际上被暂停,直到垃圾收集完成。这意味着堆中的对象越多,执行GC所需的时间就越长,用户需要等待的时间也就越长。始终为闭包和函数命名当所有闭包和函数都有名称时,检查堆栈跟踪和堆就容易多了。db.query('GIVETHEMALL',functionGiveThemAllAName(error,data){...})避免在热函数中使用大对象理想情况下,您希望避免在热函数中使用大对象,以便所有数据都适合新空间.所有受CPU和内存限制的操作都应在后台执行。还要避免触发热函数的去优化,优化的热函数比未优化的热函数使用更少的内存。避免IC在热门函数中的多态性内联缓存(InlineCaches)用于通过缓存对象属性以访问obj.key或一些简单函数来加速某些代码块的执行。函数x(a,b){返回a+b;}x(1,2);//monomorphicx(1,“string”);//多态,级别2x(3.14,1);//多态,级别3当x(a,b)第一次运行时,V8创建一个单态IC。当你第二次调用x时,V8会擦除旧的IC并创建一个支持整数和字符串操作数类型的新多态IC。当您第三次调用该IC时,V8会重复相同的过程并创建另一个级别为3的多态IC。但是,有一个限制。在IC级别5(可以使用--max_inlining_levels标志更改)之后,函数变为超态并且不再被认为是可优化的。直观上,单态函数运行速度最快,占用内存少,这是可以理解的。不要将大文件添加到内存中这是显而易见且众所周知的。如果你有一个大文件要处理,比如一个大的CSV文件,逐行读取它并以小块的形式处理它,而不是将整个文件加载到内存中。在极少数情况下,单行csv将大于1mb,因此您可以将其放入新空间。不要阻塞主服务器线程如果您有一些需要花费一些时间来处理的热门API,例如图像调整API,请将其移至单独的线程或将其转换为后台作业。CPU密集型操作会阻塞主线程,迫使所有其他客户端等待并继续发送请求。未处理的请求数据堆积在内存中,迫使FullGC需要更长的时间才能完成。不要创建不必要的数据我曾经有过关于restify的奇怪经历。如果您向无效的URL发送数十万个请求,应用程序内存将迅速增长到数百兆字节,直到几秒钟后发生FullGC,此时一切都会恢复正常。事实证明,对于每个无效的URL,restify都会生成一个带有长堆栈跟踪的新错误对象。这会强制将新创建的对象分配到大对象空间而不是新空间。访问这些数据在开发过程中非常有帮助,但在生产中显然不需要。所以规则很简单——除非你真的需要,否则不要生成数据。总结了解V8的垃圾收集和代码优化器的工作原理是提高应用程序性能的关键。V8将JavaScript编译为原生程序集,在某些情况下,编写良好的代码可以实现与GCC编译的应用程序相当的性能。更多Jerry原创文章在这里:《王子熙》:
