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

多个提高Node.js应用吞吐量的小优化技巧介绍

时间:2023-04-03 22:33:46 Node.js

介绍几个提升Node.js应用吞吐量的小优化技巧Web前端入门及工程实践。一些小的优化技术来提高Node.js应用程序的吞吐量。Introduction内容提醒尽量使用聚合IO操作,尽量减少批量写入的系统调用次数。需要考虑发布的开销以清除应用程序中的不同计时器。CPU分析器可以为您提供一些有用的信息,但它不会提供整个过程的完整反馈。谨慎使用ECMAScript高级语法,特别是如果你没有使用最新的JavaScript引擎或像Babel这样的转译器。深入了解依赖关系树的组成并对您使用的依赖关系执行适当的性能测量。当我们想要优化包含IO功能的应用程序的性能时,我们需要了解应用程序消耗的CPU周期以及阻碍应用程序并行化的CPU周期。执行的要素是众所周知的。这篇文章就是分享我在ApacheCassandra项目中升级DataStaxNode.js驱动时的一些思考以及导致应用吞吐量下降的关键因素。背景Node.js使用的标准JavaScript引擎V8将JavaScript代码编译成机器代码,然后将其作为本机代码运行。V8引擎使用以下三个组件来确保低启动时间和最佳性能同时:一个通用编译器,可以快速将JavaScript代码编译成机器代码。一种运行时分析器,可自动跟踪应用程序中的代码执行时间并决定应优化哪些代码模块。一个优化编译器,可以自动优化分析器标记为要优化的代码;如果操作被认为是过度优化,编译器还可以自动执行反优化操作。虽然一个优化的编译器可以保证最好的性能,但它并不能优化所有的代码,尤其是那些不适合编码模式的代码。可以参考GoogleChromeDevTools团队的建议,了解V8拒绝优化哪些代码模式。典型的例子包括:包含try-catch语句的函数使用arguments对象重新分配函数参数,虽然优化编译器可以显着提高代码允许的速度,但对于典型的IO密集型应用程序,大多数性能优化仍然依赖于指令重排和避免高-占用调用增加每秒操作次数;这也将是我们下一章讨论部分所需要的。基准为了更好地发现那些最能使用户受益的优化技术,我们需要模拟真实世界的用户场景,根据常见任务执行的工作量定义测试基准。首先,我们需要测试API入口点的吞吐量和延迟;另外,如果想获取更多信息,还可以选择对内部调用方法进行性能评估。推荐使用process.hrtime()获取实时解析执行时间。虽然可能会给项目开发带来一些不便,但我还是建议尽早在开发周期中引入性能度量。可以选择先从一些方法调用开始进行吞吐量测试,然后慢慢增加延迟分布等相对复杂的测试。CPU分析目前,可供我们使用的CPU分析器有很多,其中Node.js本身提供的开箱即用的CPU分析器已经可以应对大部分的使用场景。内置的Node.jsprofiler源自V8的内置profiler,可以固定频率采样堆栈信息;您可以在运行node命令时使用--prof参数来创建V8标记文件。然后,您可以使用--prof-process参数聚合分析结果并将其转换为更易读的文本:$node--prof-processisolate-0xnnnnnnnnnnnn-v8.log>processed.txt在编辑器中打开处理后的记录文件,你可以看到整个记录被分成了几个部分。首先,让我们看一下摘要部分。格式如下:[Summary]:tickstotalnonlibname2010941.2%45.7%JavaScript2354848.3%53.5%C++8051.7%1.8%GC47749.8%Sharedlibraries3560.7%Unaccounted以上数值代表采样频率分别在JavaScript/C++代码和垃圾收集器中,会随着分析代码的不同而不同。然后您可以根据需要单独查看特定的小节(例如[JavaScript]、[C++]、...)以获取特定的采样信息。另外,分析文件中还有一个非常有用的部分叫做【Bottomup(heavy)profile】,它以树状结构展示了一个函数的调用者,其基本格式如下:22332%LazyCompile:*function1lib/file1.js:223:2022199%LazyCompile:~function2lib/file2.js:70:57221100%LazyCompile:*function3/lib/file3.js:58:74上面的百分比代表了这一层调用者的比例目标函数的所有调用者,函数前的星号表示函数已优化,波浪号表示函数未优化。在上面的例子中,function199%的调用是由function2发起的,function3占function2调用的100%。CPU分析结果和火焰图是分析堆栈使用情况和CPU时间消耗的非常有用的工具。但是需要注意的是,这些分析结果并不代表一切,大量的异步IO操作会让分析变得不那么容易。系统调用Node.js使用Libuv提供的平台无关接口来实现非阻塞IO。应用程序中的所有IO操作(套接字、文件系统...)都将转换为系统调用。调度这些系统调用会耗费大量的时间,所以我们需要尽可能聚合IO操作,通过批量写入的方式尽量减少系统调用的次数。具体来说,我们应该把Socket或者文件流放入缓冲区,一次性处理,而不是每个操作都单独处理。您可以使用写队列来管理所有的写操作。写队列的常见实现逻辑如下:当我们需要执行写操作时,在某个处理窗口内:将缓冲区添加到待写列表中,将所有缓冲区连接起来,一次性写入目标管道.可以根据缓冲区总长度或者第一个元素进入队列的时间来定义窗口大小,但是在定义窗口大小时,我们需要权衡单个写操作的延迟和整体写操作的延迟,我们不能偏袒另一个。您还需要同时考虑可以聚合的最大写入操作数和单个写入请求的开销。您可以确定写入队列的上限(以千字节为单位)。我们的经验发现,大约8KB是一个很好的临界点;当然,这个值肯定会根据你应用的具体场景而有所不同。可以参考我们这个写队列的完整实现。综上所述,当我们使用批量写入时,系统调用的次数大大减少,最终提高了应用程序的整体吞吐量。Node.js定时器Node.js中的定时器与窗口中的定时器具有相同的API,可以轻松实现简单的调度操作;它在整个生态系统中有着广泛的应用,所以我们的应用可能会充斥着大量的延迟调用。与其他基于哈希的循环调度程序类似,Node.js使用哈希表和链表来维护计时器实例。但是,与其他循环调度程序不同,Node.js不维护固定长度的哈希表,而是根据触发时间对计时器进行索引。在添加新的定时器实例时,如果Node.js发现已经存在相同的键值(触发事件相同的定时器),则会以O(1)的复杂度完成添加操作。如果密钥尚不存在,则会创建一个新桶并将计时器添加到该桶中。重要的是要记住,我们应该尽可能地重用现有的定时器存储桶,避免删除整个桶然后创建一个新桶的耗时操作。例如,如果您使用滑动延迟,则应在使用clearTimeout()将其删除之前使用setTimeout()创建一个新计时器。在我们对心跳包的处理中,在去掉最后一个定时器之前,我们会先确定一个复杂度为O(1)的空闲定时器。Ecmascript语言特性当我们关注整体性能保证时,我们需要避免使用Ecmascript中的一些高级语言特性,例如:Function.prototype.bind()、Object.defineProperty()和Object.defineProperties()。我们可以在JavaScript引擎实现描述或问题中找到这些特性的性能缺陷,例如V85.3中Promise性能的改进和V85.4中Function.prototype.bind性能的改进。此外,你还需要谨慎使用ES2015或ESNext中的新语言特性,它们比ECMAScript5中的语法慢得多。六速项目网站跟踪了这些语言特性在不同JavaScript引擎上的性能。如果你还没有找到一些特性的性能评估,你也可以自己做一些测试。V8团队也一直致力于提高新语言特性的性能,最终使它们与底层实现保持一致。我们可以在性能规划中及时了解他们的ES2015性能优化工作进展,他们会收集用户对改进点的建议,并发布新的设计文档来说明他们的解决方案。也可以在本博客及时了解V8的实现进度,但考虑到V8的改进可能需要很长时间才能合并到Node.js的LTS版本中:按照LTS计划,只会合并当Node.js大版本迭代到最新的V8版本时。您可能需要等待6-12个月才能发现新的V8引擎已合并到Node.js运行环境中,而当前新发布的Node.js版本只会包含V8引擎中的一些修复。依靠Node.js运行时为我们提供了完整的IO操作库,但是ECMAScript语法标准只提供了少数内置的数据类型,很多时候我们不得不依赖第三方库来完成一些基本的工作。没有人能保证这些第三方库能准确高效地工作,甚至那些流行的明星模块也可能出现问题。Node.js生态系统如此繁荣以至于许多依赖模块可能只包含少数几个您可以轻松实现的方法。我们需要在重新发明轮子的成本和依赖带来的性能不可控之间做一个权衡。我们团队尽量避免引入新的依赖,对所有的依赖都比较保守。但是,我们欢迎像bluebird这样发布可靠性能评估的库。我们使用async来处理项目中的异步操作,在代码库中广泛使用async.series()、async.waterfall()和async.whilst()。确实,我们很难说这样连接起来的多级异步处理库是造成性能损失的罪魁祸首。幸运的是,许多其他开发人员已经找到了其中的问题。我们也可以选择像neo-async这样的替代库,它的效率要高得多,并且有公开的性能评估结果。总结本文提到的一些优化技术属于常识性的,而另一些则涉及到Node.js生态和JavaScript核心引擎的实现细节和工作原理。在我们开发的客户端驱动程序中,通过引入这些优化,我们实现了两倍的吞吐量提升。考虑到我们的Node.js应用是以单线程方式运行的,我们应用CPU占用的时间片和指令的先后顺序会极大地影响整体的吞吐量和高并行度。关于作者JorgeBay是ApacheCassandra项目中Node.js和C#客户端驱动程序的核心工程师,也是DataStax的DSE。他喜欢解决问题和提供服务器端解决方案。Jorge拥有超过15年的专业软件开发经验。他为ApacheCassandra实现的Node.js客户端驱动程序也是官方DataStax驱动程序的基础。