当前位置: 首页 > 后端技术 > Node.js

Node.jsApplicationTroubleshootingPlaybook-OpeningChromedevtools

时间:2023-04-03 16:43:01 Node.js

WedgeCorrectly在之前的预赛中,我们概述了如何获取一般错误日志、系统/进程指标,以及对这些角度进行核心转储以解决问题。这就引出了下一个问题:我们知道进程的CPU/Memory很高,或者我们已经获取了进程崩溃后的coredump,如何分析定位到具体的JavaScript代码段。其实Chrome自带的Devtools对于上述JavaScript代码的CPU/Memory问题都有很好的原生分析展示。本节将为您介绍一些实用的功能和指标(基于Chromev72,不同版本之间有使用差异。)。本书首发于Github,仓库地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云栖社区同步更新。CPU飙升问题一、导出JS代码运行状态当我们发现当前Node.jsApplication转储一段时间JavaScript代码的运行状态,以便分析CPU高的原因。幸运的是,V8引擎实现了一个CPUProfiler,可以帮助我们导出一段时间内JS代码的运行状态。目前有很多成熟的模块或者工具可以帮助我们完成这样的操作。v8-profiler是一款老牌的Node.js应用性能分析工具。可以方便的帮助开发者导出JS代码的运行状态。我们可以在项目目录下执行如下命令来安装这个模块:npminstallv8-profiler--save然后在代码中可以获取5s大陆JS代码运行状态如下:'usestrict';constv8Profiler=require('v8-profiler');consttitle='test';v8Profiler.startProfiling(title,true);setTimeout(()=>{constprofiler=v8Profiler.stopProfiling(title);profiler.delete();console.log(profiler);},5000);然后我们可以看到v8-profiler模块帮我导出了代码运行状态其实就是一个很大的JSON对象。我们可以将这个JSON对象序列化为一个字符串,并将其存储在一个文件中:test.cpuprofile。注意这里文件名的后缀必须是.cpuprofile,否则Chromedevtools无法识别。注意:v8-profiler目前处于年久失修状态。在Node.js8和Node.js10上无法正确编译安装,如果想在8个或10个项目中使用,可以试试v8-profiler-next。二。分析CPUProfile借助v8-profiler获取我们Node.js应用一段时间的JS代码运行状态后,我们可以将其导入到Chromedevtools中进行分析展示。在Chrome72中,从我们的Dump中分析CPUProfile的方法与之前有所不同,默认工具栏中不会显示CPUProfile分析页面。我们需要点击工具栏右侧的More按钮,然后选择Moretools->JavaScriptProfiler进入CPU分析页面,如下图:选择JavaScriptProfiler后,点击页面上的Load按钮出现,然后加载刚才保存的test.cpuprofile文件,可以看到Chromedevtools的分析结果:这里默认的view是Heavyview。在这个视图中,Devtools会按照对你的应用的影响程度从高到低列出这些函数。点击展开可以看到列出了这些函数的完整路径,方便大家查看代码中对应的位置。这里再介绍两个比较重要的指标,方便大家更有针对性的排查:SelfTime:这个函数本身代码段的执行时间(不包括任何调用)TotalTime:这个函数包括它调用的其他函数的总执行时间是像上面的截图例子,ejs模块应该在线缓存,所以ejs模块的compile方法不应该出现在列表中。这显然是一个非常可疑的性能损失点,我们需要对其进行扩展找出原因。除了Heavyview,Devtools其实还为我们提供了一个火焰图,可以在更多的维度上展示。点击左上角切换:火焰图是按照我们的CPU采样时间轴显示的,这样我们更容易看到采样期间我们Node.js应用的JS代码执行行为。这里新增两个指标解释一下含义:Aggregatedselftime:CPU采样周期内聚合后函数自身代码段的总执行时间(不包括其他调用)CPU采样周期包括它调用的其他函数的总执行时间。综上所述,借助Chromedevtools以及导出当前Node.js应用模块中Javascript代码运行状态的能力,我们已经能够比较完整的排查定位到对应Node.js的CPU占用情况应用服务异常时进程很高。在生产实践中,这部分JS代码的性能分析经常用在新项目上线前的性能压力测试中。有兴趣的同学可以深入研究。内存泄漏问题一、判断是否存在内存泄漏根据笔者的经验,内存泄漏问题是Node.js在线运行时出现的各类问题中重灾区。尤其是三方库本身的bug或者开发者使用不当造成的内存泄露,会让很多Node.js开发者感到束手无策。本节首先向读者介绍一下,在什么情况下我们的应用程序很可能会发生内存泄漏?其实判断我们线上的Node.js应用是否存在内存泄漏很简单:借助各自公司的一些系统和进程监控工具,如果我们发现Node.js的总内存使用曲线应用在长期增长率不下降,堆内存按照趋势突破堆限制的70%,那么基本上应用很可能发生泄漏。当然,没有绝对的。如果应用的访问量(QPS)也在增长,那么内存曲线只增不减是正常的。如果真的是因为QPS%的不断增长导致堆内存超过了堆限制的70%甚至90%,此时就需要考虑扩容服务器来缓解内存问题了。二。导出JS堆内存快照如果确认Node.js应用存在内存泄漏问题,那么我们需要通过一些方法导出JS内存快照(堆快照)进行分析,就像上面的CPU问题一样。V8引擎还提供了一个内部接口,可以直接导出分配在V8堆上的JS对象,供开发者分析。这里我们使用heapdump模块。首先,执行以下命令安装:npminstallheapdump--save然后可以在代码中使用该模块,如下所示:'usesytrict';constheapdump=require('heapdump');heapdump.writeSnapshot('./test'+'.heapsnapshot');这样我们就在当前目录下创建了一个堆快照文件:test.heapsnapshot,用文本编辑工具打开这个文件,可以看到还是一个很大的JSON结构,这里的堆快照文件的后缀一定是.heapsnapshot,否则Chromedevtools将无法识别它。三、分析堆快照在Chromedevtools的工具栏选择Memory进入分析页面,如下图:然后点击页面的Load按钮,选择我们刚刚生成的test.heapsnapshot文件,就可以看到分析了结果。如下图所示:默认视图其实是一个Summary视图。这里的Constructor和我们写JS代码时的构造函数没什么区别。它们都引用这个构造函数创建的对象。新版Chromedevtools依然在构造函数后添加*number信息,表示该构造函数创建的实例数。其实在堆快照的分析视图中,有两个非常重要的概念需要大家去理解。否则很有可能你拿到堆快照再看分析结果的时候会一头雾水。分别是ShallowSize和RetainedSize,需要更好的理解这两个概念,我们需要先理解支配树。首先我们看简化堆快照描述的内存关系图如下:1这里是根节点,也就是GC根,那么对于对象5,如果我们要回收对象5(也就是unreachablefromtheGCroot),仅仅移除对象4或对象3对对象5的引用是不够的,因为显然根节点1可以分别从对象3或对象4遍历到对象5。所以,我们只能通过移除对象2来回收对象5,所以在上图中,对象5的直接支配者是对象2。顺着这个思路,我们可以将上面简化的堆内存关系图通过一定的方式转化为支配树算法:对象1和对象8的支配关系描述如下:对象1支配对象2,对象2支配对象3、4、5对象4支配对象6对象5支配对象7对象6支配对象8好了,这里我们可以开始解释什么是ShallowSize和RetainedSize。其实一个对象的ShallowSize就是对象本身创建的时候,在V8堆上面分配的大小,结合上面的例子,就是对象1到8本身的大小。对象的RetainedSize是V8堆在FullGC之后,如果从堆中移除对象,可以释放的空间大小。同样结合上面的例子,支配树的叶子节点Object3、Object7、Object8没有直接的支配对象,所以它们的RetainedSize等于它们的ShallowSize。剩下的非叶子节点可以一个一个展开。为了描述方便,SZ_5代表对象5的ShallowSize,RZ_5代表对象5的RetainedSize。那么可以得到如下结果:RetainedSizeofobject3:RZ_3=SZ_3object7RetainedSizeofRZ_7=SZ_7对象8保留大小:RZ_8=SZ_8对象6保留大小:RZ_6=SZ_6+RZ_8=SZ_6+SZ_8对象5保留大小:RZ_5=SZ_5+RZ_7=SZ_5+SZ_7对象4大小保留:RZ_4=SZ_4+RZ_6=SZ_4+SZ_6+SZ_8对象2的保留大小:RZ_2=SZ_2+RZ_3+RZ_4+RZ_5=SZ_2+SZ_3+SZ_4+SZ_5+SZ_6+SZ_7+SZ_8GCroot1的保留大小:RZ_1=SZ_1+RZ_2=SZ_1+SZ_2+RZ_3+RZ_4+RZ_5=SZ_2+SZ_3+SZ_4+SZ_5+SZ_6+SZ_7+SZ_8这里可以发现GC根的RetainedSize等于从这个根开始的所有可达对象的ShallowSizeheap并且,这符合我们的理解和预期。毕竟,如果从堆中移除GC根,则应清除从该根开始的所有对象。明白这一点,回到我们一开始看到的默认概览视图。通常,可能泄漏的对象往往具有特别大的RetainedSize。我们可以在窗口中根据RetainedSize对占用堆空间的进行排序。大部分对象都被检查过:如果可疑对象被确认,Chromedevtools会自动展开它来为你定位代码段。我们以NativeModule构造函数生成的对象vm为例:这里的上半部分是顺序例如NativeModule实例@45655的exports属性指向对象@45589,filename属性指向一个字符串"vm.js";后半部分是反向引用关系:NativeModule实例@13021的_cache属性指向Object实例@41103,Object实例@41103的vm属性指向NativeModule实例@45655。如果你对这部分图感到困惑,可以仔细看看上面的例子,因为如果你发现一个可疑的泄漏对象,你可以看到这个对象下的属性和值以及它的父引用关系chain结合上图。大多数情况下,我们然后就可以定位到生成可疑对象的JS代码段。事实上,除了默认的Summary视图,Chromedevtools还提供了Containment和Statistics视图。让我们再看看Containment视图。可以通过选择堆快照分析页面的左上角进行切换,如下图所示:该视图实际上是堆快照分析,因此相对于Summary视图,直接查找疑似泄漏对象相对困难从这个角度来看。最后,Chromedevtools实际上是一个非常强大的工具。本节仅介绍CPUProfile和heapsnapshot分析能力的介绍以及常用视图的使用指南。如果您在Node.js应用程序中遇到诸如高CPU或内存泄漏等问题,您必须能够知道会发生什么而不是恐慌。本文作者:易君阅读原文,为云栖社区原创内容,未经允许不得转载。