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

Node.js 应用故障排查手册 —— 冗余配置传递引发的内存溢出

时间:2023-04-04 00:51:16 Node.js

Node.js应用故障排查手册——压力测试中冗余配置传输性能调优导致的内存溢出。与CPU相关的问题相比,Node.js应用中因使用不当导致的内存问题是重灾区,而且这些问题经常发生在生产环境中,本地压测很难重现。事实上,这部分内存问题也成为很多Node.js开发者不敢将Node.js技术栈深入应用到后端的一大障碍。本节将通过一个开发者容易忽视的生产内存溢出案例,展示如何使用性能平台实现在线Node.js应用中发现、分析、定位问题代码和修复内存泄漏的过程。激励大家。本书首发于Github,仓库地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云栖社区同步更新。最小化复现代码因为内存问题相对于高CPU的问题比较特殊,我们直接描述问题排查可能不如问题代码的组合直观,所以这里先给出最小化复现代码,之后大家运行一下,结合下面的分析过程应该更有收获。该示例基于Egg.js:如下:'usestrict';constController=require('egg').Controller;constDEFAULT_OPTIONS={logger:console};classSomeClient{constructor(options){this.options=选项;}asyncfetchSomething(){returnthis.options.key;}}constclients={};functiongetClient(options){if(!clients[options.key]){clients[options.key]=newSomeClient(Object.assign({},DEFAULT_OPTIONS,options));}returnclients[options.key];}classMemoryControllerextendsController{asyncindex(){const{ctx}=this;constoptions={ctx,key:Math.random().toString(16).slice(2)};constdata=awaitgetClient(options).fetchSomething();ctx.body=数据;}}module.exports=MemoryController;然后在app/router.js中添加一个Post请求路由:router.post('/memory',controller.memory.index);这里也给出了导致问题的Post请求Demo,如下:'usestrict';常量fs=要求('fs');consthttp=require('http');constpostData=JSON.stringify({//这里的body.txt可以容纳一个大2M左右的字符串数据:fs.readFileSync('./body.txt').toString()});functionpost(){constreq=http.request({method:'POST',host:'localhost',port:'7001',path:'/memory',headers:{'Content-Type':'application/json','Content-Length':Buffer.byteLength(postData)}});req.write(postData);请求结束();req.on('error',function(err){console.log(12333,err);});}setInterval(post,1000);最后,启动最小化复现的Demo服务器后,我们运行Post请求的客户端,1s发起一个Post请求,在平台控制台可以看到堆内存一直在增加。如果我们按照本书工具一章的Node.js性能平台使用指南-配置合适的告警来配置Node.js进程,如果出现堆内存告警,那么你会收到平台的短信/邮件提醒。while排查进程收到性能平台的进程内存告警后,我们登录控制台,进入应用首页,在告警对应的实例上查找问题进程。然后参考《Node.js性能平台用户指南-内存泄漏》工具章节中的方法抓取堆快照,点击Analyze按钮查看AliNode自定义分解结果:顶部信息含义默认报告页面已经提到了就是这样,这里不再赘述,这里重点说一下可疑信息:表示18个对象占用了96.38%的堆空间,显然这是我们需要进一步检查的点.我们可以点击对象名查看这18个system/Context对象的详细信息:这里我们进入以这18个system/Context为根节点开始的dominator树视图,所以展开后可以看到每个对象的详细信息实际内存使用情况。上图中,问题明显集中在第一个对象上。我们继续展开查看:很明显,451个SomeClient实例实际上正在吃光这里的堆空间。面对这样的问题,我们需要从两个方面来考虑,判断这是否真的是内存异常的问题:在当前Node.js应用的正常逻辑下,单个进程是否需要451个SomeClient实例?如果真的需要那么多SomeClient实例,每个实例占用1.98MB空间吗?合理第一次判断,在对应的实际生产问题中,再次确认代码逻辑后,我们的应用确实需要那么多的Client实例。显然,此时的调查重点是集中在每个实例的1.98MB空间上。对于占用是否合理,如果进一步判断合理,说明Node.js默认的单进程堆限制1.4G不适用该场景,需要通过启动旗帜。基于上面的判断需求,我们继续点击这些SomeClient实例进行查看:这里可以清楚的看到SomeClient本身的大小只有1.97MB,但是下面options属性对应的Object@428973对象却占了一个1.98M,进一步展开这个可疑的Object@428973对象,我们可以看到其ctx属性对应的Object@428919对象是SomeClient实例占用这么大对象空间的根本原因!我们可以点击其他的SomeClient实例,可以看到每个实例都是一样的。这时候我们就需要结合代码判断options.ctx属性挂载在SomeClient实例上是否合理。点击本题Object的地址:输入该Object的关系图:Search显示的视图与Dom结果图不同。它实际上展示的是从堆缓存中解析出来的原始对象关系图,所以sideinformation肯定是会存在的,除了name和objectName之外,更容易让我们判断对象在代码中的位置。但是在这个例子中,仅仅依靠从Object@428973开始的原始内存关系图,我们是看不到明确的代码位置的。毕竟Object.ctx和Object.key都是相当常见的JavaScript代码关系,所以我们继续点击Retainer视图:得到如下信息:这里的Retainer信息和ChromeDevtools中的Retainer含义相同,代表节点在堆内存中的原始父引用关系。如本文内存问题的案例,仅怀疑如果点本身及其展开不能可靠定位问题代码,展开该对象的Retainer视图,可以看到其父节点链接可以轻松定位到问题代码.这里我们可以很容易的通过Retainer视图中问题对象的父引用链接,在代码中找到创建这个对象的代码:key]=newSomeClient(Object.assign({},DEFAULT_OPTIONS,options));}returnclients[options.key];}结合SomeClient的使用可以看出,用于初始化的options参数其实只是用到了key属性,其余都是多余的配置信息,不需要传入。代码修复与确认知道原因后,修改起来就比较简单了。单独生成一个SomeClient使用的options参数,只从传入的options参数中取出需要的数据,保证没有冗余信息:默认选项);如果(!clients[options.key]){clients[options.key]=newSomeClient(someClientOptions);}returnclients[options.key];}重新发布运行后,堆内存可以降到只有几十兆。至此,Node.js应用内存异常的问题已经完美解决。最后,本节还全面展示了如何使用Node.js性能平台排查定位在线应用内存泄漏问题。其实严格来说,这个问题并不是真正的内存泄漏。当开发者为了省事直接全额赋值的时候,我们在写代码的时候或多或少都会遇到。这个问题给我们带来的启示是:我们在编写公共组件模块时,永远不要相信用户的传统。输入参数。任何时候,只要我们需要用到的参数保留下来并传递下去,这样就可以避免很多问题。本文作者:易君阅读原文本文为云栖社区原创内容,未经允许不得转载。