优化node.js应用程序

给定的:旧的http node.js应用程序并增加了其负担。

问题的标准解决方案:退出服务器,从0开始重写所有内容,优化已编写的内容。

让我们尝试进行优化,找出如何发现和改善应用程序的弱点。 也许无需触摸一行代码即可加速:)

欢迎所有有兴趣的猫下!

首先,让我们决定一种性能测试技术。 我们将对1秒钟内服务的请求数量感兴趣:rps。

我们将在工作程序的模式1(1个进程)中运行该应用程序,以评估旧代码的性能以及经过优化的代码-绝对性能并不重要,比较性能很重要。

在具有许多不同路由的典型应用程序中,首先查找负载最大的请求是合乎逻辑的,而处理请求的时间则大部分。 诸如request-log-analizer之类的实用程序或许多类似的实用程序将使您能够从日志中提取此信息。

另一方面,您可以获取真实的请求列表,并将其全部列出(例如,使用yandex-tank)-我们可以获得可靠的负载配置文件。

但是,进行多次代码优化迭代,使用更简单,更快速的工具和一种特定类型的请求(在优化了一个请求后,研究下一个请求等)会更加方便。 我的选择是wrk 。 而且,在我的情况下,路由的数量并不多-逐一检查所有内容并不困难。

应该立即指出,在阻止查询,数据库期望等方面。 应用程序已经过优化,一切都取决于cpu:在测试期间,工作人员消耗100%的cpu。

出售的服务器使用node.js版本6-让我们开始吧:

请求/秒: 1210

我们在第8个节点上尝试:
请求/秒: 2308
第十注:
请求/秒: 2590

区别是显而易见的。 这里的关键作用是更新v8版本-过去有很多优化不佳的v8代码。 并且为了不处理在node.js v8中消失的风车,最好立即进行升级,然后再进行代码优化。

我们转向实际寻找瓶颈的方法:在我看来,最好的工具是Flamegraph。 随着0x项目的到来,获得一个Flamegraph变得非常简单-运行0x而不是节点:0x -o yourscript.js,进行测试,停止脚本,并在浏览器中查看结果。

在优化之前,经过测试的代码的火焰图看起来像这样:


在过滤器下方,保留应用程序,部门-仅包含应用程序和第三方模块的代码。

条带越宽,执行此功能(包括嵌套调用)所花费的时间就越多。

我们将处理最大的中央部分。

首先,我们重点介绍未优化的功能。 我在应用程序中发现了其中一些。

此外,最重要的功能是优化的典型候选者。 其余功能按相对均匀的步骤排列-每个功能仅占延迟的一小部分,没有明显的领导者。

然后,可以使用一种简单的动作算法:优化最广泛的功能,从一个功能移到另一个功能。 但是我选择了另一种方法:从入口点到应用程序(http.createServer中的请求处理程序)进行优化。 在所研究的函数的最后,我没有调用以下函数,而是使用虚拟响应完成了请求处理,并研究了该特定函数的性能。 优化后,虚拟答案会沿着调用堆栈进一步移动到下一个函数,依此类推。

这种方法带来的便利结果是:您可以在理想条件下看到rps(只有一个启动函数,rps接近hellow world node.js应用程序的最大rps),并且响应桩进一步深入应用程序中,观察所研究函数对性能下降的贡献。 rps-啊。

因此,我们只剩下start函数,我们得到:

请求/秒: 16176



通过连接核心v8过滤器,您可以看到几乎所有正在研究的功能都包括发送答案,记录日志以及其他性能欠佳的内容-我们可以进一步进行。

我们传递给以下函数:

请求/秒: 16111
一切都没有改变-进一步下降:
请求/秒: 13330


我们的客户! 可以看出,涉及到的getByUrl函数占据了start函数的重要部分-与rps沉降很好地相关。

我们仔细查看其中发生的情况(开启核心,v8):

许多事情正在发生……我们抽出代码,优化:

for (var i in this.data) { if (this[i]._options.regexp_obj.test(url)) return this[i]; } return null; 

变成

 let result = null; for (let i=0; i<this.length && !result; i++) { if (this[i]._options.regexp_obj.test(url)) result = this[i]; } 

在这种情况下,简单的for比..in快得多

获取请求/秒: 16015



在视觉上,函数“放气”并仅占启动函数的一小部分。
在有关该功能的详细信息中,所有内容也都大大简化了:

我们继续下一个功能。

请求/秒: 13316



该函数具有很多数组函数,尽管在最新版本的node.js中有明显的加速,但它们仍然比简单的循环慢:change [] .map和filter。 定期获得

请求/秒: 15067



如此反复,对于每个后续功能。

一些更有用的优化:对于具有动态更改的键集的哈希,新的Map()可以比常规{}快40%;

Math.round(el * 100)/ 100比toFixed(2)快2倍。

在用于核心和v8函数的Flamegraph中,您可以看到晦涩的条目和相当明显的StringPrototypeSplit或v8 :: internal :: Runtime_StringToNumber,并且,如果这是代码执行的重要组成部分,请尝试进行优化,例如,简单地重写不执行这些代码的代码操作。

例如,用多个indexOf和substring调用替换split可以使性能提高一倍。

一个单独的大型而复杂的主题是jit优化,或者说是非优化功能。
如果此类功能的比例很大,则有必要对其进行处理。

对节点--trace_file_names --trace_opt_verbose --trace-deopt --trace_opt的输出进行周到的研究可以为您提供帮助。

例如,表格的行

反优化(DEOPT软):开始0x2bcf38b2d079 <JS函数getTime ...二进制操作的类型反馈不足导致该行

返回val> = 10? val:'0'+ val;

替代

return(val> = 10?'':'0')+ val;

纠正了情况。

有关旧版v8引擎的很多信息,其中包括各种原因以及解决功能优化问题的方法:

github.com/P0lip/v8-deoptimize-reasons-列表,
www.netguru.co/blog/tracing-patterns-hinder-performance-典型原因分析,
www.html5rocks.com/en/tutorials/speed/v8-关于v8的优化,我认为对于当前的v8引擎也是如此。

但是许多问题与新v8不再相关。

无论如何,经过所有优化后,我设法获得了Requests / sec: 9971 ,即 由于过渡到最新版本的node.js,它将加速约2倍,由于代码优化,它将再加速4倍。

我希望这种经验对其他人有用。

Source: https://habr.com/ru/post/zh-CN414541/


All Articles