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

Node.js应用程序故障排除手册——使用CPU分析和调整吞吐量

时间:2023-04-04 00:28:39 Node.js

Wedge在我们要启动一个新的Node.js应用程序之前,尤其是第一个Node.对于在线吞吐性能,你肯定会想做性能压力测试,以估计在当前集群规模下它能承受多少流量。本案其实就是在这样一个场景中。我们要推出Node.js技术栈,前后端分离。那么,除了后端服务的响应QPS,单纯使用Node.js做模板渲染还能做什么呢?这样的表现,是大家非常关心的事情。本书首发于Github,仓库地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云栖社区同步更新。性能压测所体现的优化流程集群的整体能力,其实可以通过单机的吞吐量来衡量。所以本次性能压测使用4核8G内存的服务器进行压测,页面使用流行的ejs进行服务端渲染,进程数使用PM2根据数量启动4个业务子进程运行的核心。1.开始压力测试完成这些准备后,使用阿里云提供的PTS性能压力测试工具进行压力测试。此时单机ejs模板渲染的QPS在200左右,此时可以看到四个子进程的CPU基本都在100%,也就是CPU负载已经到了瓶颈,但是200左右的QPS显然让系统整体的渲染效果很不理想。2、模板缓存整体QPS无法提升,CPU达到系统瓶颈。因此,按照工具篇第二部分的方法,我们抓取了平台压力测试时的3分钟CPUProfile,结果如下图所示:这里是一个很奇怪的地方,因为我们已经在压测环境中开启了模板缓存,按道理来说,ejs.compile函数对应的模板编译不会再出现了。仔细对比项目中的渲染逻辑代码,发现这部分使用了一个比较不常见的模块koa-view。项目开发者想当然的认为ejs模块传入了cache:true,但实际上该模块并没有对ejs模板有更好的支持,所以在实际压测中模板缓存没有生效,而模板编译动作本质上是字符串处理,恰恰是一个CPU密集型操作,导致QPS达不到预期。了解原因后,首先我们将koa-view换成比较好用的koa-ejs模块,并根据koa-ejs文档正确开启缓存:render(app,{root:path.join(__dirname,'view'),viewExt:'html',缓存:true});再次压测后,单机下的QPS提升到了600左右,虽然性能提升了3倍左右,但还是达不到预期的目标。3.包含编译为了继续优化并进一步提升服务器的渲染性能,我们在压力测试的时候继续抓取3分钟的CPUProfile进行查看:如您所见,虽然我们已经确认koa-使用了ejs模块,也正确开启了缓存,但是在压力测试的过程中,CPUProfile中竟然有对ejs的编译动作!这里继续展开compile,发现在includeFile的时候引入了,继续回到项目本身,观察压测的页面模板,确实是用ejs注入的include方法导入其他模板:<%-include("../xxx")%>对比ejs的源码,注入的include函数调用链确实是include->includeFile->handleCache->compile,与获取到的CPUProfile显示的内容一致来自压力测试。那么下图红框中的replace部分也是在编译过程中生成的。此时开始怀疑是koa-ejs模块没有正确的将缓存参数传递给实际负责渲染的ejs模块,导致出现这个问题,于是继续阅读koa-ejs的缓存设置,下面是简化后的逻辑(koa-ejs@4.1.1版本):constcache=Object.create(null);asyncfunctionrender(view,options){view+=settings.viewExt;constviewPath=path.join(settings.root,view);//如果有缓存,则直接使用解析缓存模板得到的函数进行渲染}//调用ejs进行第一次渲染而不缓存.compile编译consttpl=awaitfs.readFile(viewPath,'utf8');constfn=ejs.compile(tpl,{filename:viewPath,_with:settings._with,compileDebug:settings.debug&&settings.compileDebug,debug:settings.debug,delimiter:settings.delimiter});//缓存ejs.compile得到的模板解析函数if(settings.cache){cache[viewPath]=fn;}returnfn.call(options.scope,options);}很明显,koa-ejs模板的模板缓存完全是自己实现的,调用时传入的option参数中并没有传入用户设置的缓存参数ejs.compile方法,但是使用了ejs模块提供的缓存能力。而项目直接使用模板中ejs模块注入的include方法进行模板间调用。结果是只缓存了主模板,主模板使用include调用其他模板,还是会重新编译解析,导致在压测下,还是有很多重复的模板编译动作,这防止QPS增加。又找到了问题的根源。为了验证是否是koa-ejs模块本身的bug,我们在项目中稍微改动了它的渲染逻辑:constfn=ejs.compile(tpl,{filename:viewPath,_with:settings._with,compileDebug:settings.debug&&settings.compileDebug,debug:settings.debug,delimiter:settings.delimiter,//将用户设置的缓存参数传递给ejs,并使用其提供的缓存能力cache:settings.cache});然后打包并进行压力测试。此时单机QPS从600提升到4000左右,基本达到上线前的性能预期。为了确认压测下是否还有模板编译动作,我们在Node.js性能平台上继续抓取压测时3分钟的CPUProfile:可以看到经过上述优化后的koa-ejs模板,ejs。与最初的性能相比,性能提高了约20倍。本文中的koa-ejs模块缓存问题在4.1.2(含)版本后已经修复。有关详细信息,请参阅缓存包含文件。如果使用koa-ejs版本>=4.1.2,可以放心使用。最后,CPUProfile本质上以可读的方式向开发者反映了JavaScript代码在运行时的执行频率。除了在上线进程负载高时用于定位问题代码外,在我们上线前进行性能压力测试。而相应的性能调优也能提供很大的帮助。这里需要注意的是:只有当进程的CPU负载很高的时候,获取到的CPUProfile才能真正给我们反馈问题。这个案例从实际生产中我们也可以看出,使用Node.js正确和错误开发应用程序前后运行效率相差20倍。Node.js作为服务端技术栈的今天,它所能提供的性能是毋庸置疑的。大多数情况下,执行效率低下是我们自己的业务代码或者第三方库本身的bug导致的。Node.js性能平台可以帮助我们更方便地找到这些bug。本文作者:易君阅读原文,为云栖社区原创内容,未经允许不得转载。