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

Node应用内存泄漏分析方法与实践

时间:2023-04-03 20:27:37 Node.js

本文发表于北斗同构GitHub,转载请注明出处。前言菜鸟物流市场是菜鸟旗下的一条业务线。可以简单理解为物流领域的淘宝,是为匹配物流需求方和物流提供方而搭建的平台。其中,搜索页、详情页、买家中心等页面均基于北斗同构框架开发。随着node、react同构等技术的应用越来越广泛,内存泄漏问题时有发生,应该引起足够的重视。最近在做菜鸟物流市场的技术支持,中奖了。我整理了实践过程中的心得体会,供大家参考。先介绍几个基本名词:SSR:server-siderendering,简单来说就是在服务器端渲染页面,直接返回给浏览器,提高显示性能同构:在SSR的基础上,应用可以在服务端渲染也可以在浏览器中渲染,两端运行一套代码。北斗(北斗):基于eggjs的React同构框架,开源地址内存泄漏:指程序中已经动态分配的堆内存没有释放或由于某种原因无法释放,通常是由于代码处逻辑不合理造成的应用层。OOM:OutOfMemory,简单的说就是内存耗尽,无法再分配内存。内存泄漏是OOM最常见的原因。OOM的直接后果就是进程崩溃。RSS:ResidentSetSize实际使用物理内存(包括共享库占用的内存)案例分析回到菜鸟物流市场,发现问题所在。打开alinode查看慢日志,果然有很多慢日志记录。分析验证&排查分析主要有以下现象:详情页有时打开很快,有时需要4-5秒才能打开,重启后明显好转。响应速度很快,机器负载采样:CPU占用很低,内存占用高达53.5%。根据当时的现象,做了简单的分析,制定了具体的动作:响应很慢-->1)可能是HSF界面慢2)可能是渲染慢-->动作:记录日志分别时快时慢-->不同机器当前状态可能不一样,导致响应速度差异很大-->动作:对比每台机器的负载情况,重启后速度很快-->可能发生某个事件导致性能不佳,重点排查内存泄漏-->action:通过alinodeheapsnapshots分析低CPU和高内存消耗-->很有可能是内存泄漏-->action:analyzealinodeheapsnapshots从上面推断,内存泄漏的可能性很大,但是还是需要实际数据来验证,所以按照制定的action进行数据收集和验证。随着时间的推移,1694进程的hsf调用时间一直很稳定,但是服务端的渲染时间逐渐飙升到3700多毫秒,然后在某个临界值后下降到50毫秒左右。可能是进程因为某个事件崩溃了(猜测是内存泄漏导致OOM),然后北斗框架会自动重启进程,恢复良好的状态。打开沙箱,查看进程生命周期。果然1694进程挂了,然后又重启了一个29649进程。从上图中也可以看到RSS(实际使用的物理内存)高达1880.93MB,基本可以确定是内存泄漏。查看内存使用曲线,内存呈锯齿状。先一路飙升,到零点后瞬间下降,以此类推。与我们的推断完全一致,这是一条典型的内存泄漏曲线。最终结论:访问速度慢的原因是内存泄漏消耗了太多资源。检查定位内存泄漏后,还需要进一步检查是什么代码导致了内存泄漏。这时候我们就需要用到排错神器——alinode。先创建一个堆快照:打开分析页面的对象簇视图,可以看到里面有大量的Window对象。经过查找,采样的Window对象多达390个。通过GCRoot展开,发现挂载了无数个定时器。分析代码,我发现了两个定时器设置。看代码逻辑,服务器端根本不会释放定时器。componentWillMount(){let_this=this;window.handler=window.setInterval(function(){if(typeofAMap){_this.renderMap('',AMap);window.clearInterval(window.handler);}},300);}注释掉后,预发布验证没有window相关的内存泄露。附言。后来验证发现,除了定时器的问题,还有另外两个内存泄漏问题,这里不再赘述,贴出其中一个内存泄漏(高德地图)的代码,供读者参考。componentWillMount(){this.createAmapScript();}createAmapScript(){letscript=document.createElement('script'),body=document.getElementsByTagName('body')[0];script.type='文本/javascript';script.src='https://webapi.amap.com/maps?v=1.3&key=59699a8cfee7c52f58390357cbdbf27d';body.appendChild(脚本);}解决问题从上面两段代码可以看出,定时器不需要在服务端执行,高德地图本身不支持服务端渲染,所以两者都可以在客户端渲染.根据react的特点,componentDidMount生命周期函数不会在server端执行,所以只需将上面的代码从componentWillMount移到componentDidMount即可。具体修复如下:通过loadtest和本地压测验证:单个进程同样以10QPS进行压测。从对比可以看出修复前RT时间一直在上升,修复后RT一直稳定在200毫秒左右。再看网上的数据,内存使用率一直很稳定,没有飙升的现象。到目前为止,收工吧。Methodology看完案例,是时候系统总结方法论了。现象从刚才的案例可以看出,内存泄漏最典型的现象就是内存使用率会随着时间的推移逐渐升高。即使没有流量,内存使用率也不会下降。对于一个健康的应用,当流量增加时,内存使用量会增加,当流量下降后,内存使用量会恢复到原来的水平。内存泄漏的原因通常包括以下因素。缓存队列消费不及时。范围未发布。本文案例属于范畴。该解决方案未发布。本地loadtest压测,观察应用是否健康。抓取v8堆内存快照,通过chrome开发者工具配置文件导入快照进行分析。通过alimonitor、eagleeye等监控平台在线监控应用健康度。如果有异常,使用alinodeheapsnapshots排查问题。如果异常难以重现,可以在预发布或隔离的线上机器上进行压力测试。压力测试可以有效放大问题在压力测试过程中,最主要的建议是通过alinode堆快照排查问题:开发阶段压力测试,开发阶段压力测试,开发阶段压力测试。重要的事情说三遍。古语有云:上医治前病,中医治本病,下医治已病。都说最有本事的医生,不是擅长治病的,而是会防病的。让问题在开发阶段就暴露出来,而不是等着在线告警来抢救。避免在构造函数中绑定事件。不支持SSR的组件建议在componentDidMount的生命周期中放入componentDidMount。同样,createElement、appendChild等DOM原生操作也应该放在componentDidMount中。详见同构注释