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

多个提升Node.js应用吞吐量的小优化技巧介绍翻译自 InfoQ 英文站的 node-micro-optimizations-javascript 一文,从属于笔者的Web 前端入门与工程实践javascript

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

内容提点

  • 尽量地使用聚合IO操做,以批量写的方式来最小化系统调用的次数。html

  • 须要将发布的开销考虑进内,清除应用中不一样的定时器。前端

  • CPU分析器可以给你提升一些有用信息,可是并不能完整地反馈整个流程。java

  • 谨慎使用ECMAScript高级语法,特别是你还未使用最新的JavaScript引擎或者相似于Babel这样的转换器的时候。node

  • 要洞察你的依赖树的组成而且对你使用的依赖进行适当的性能评测linux

当咱们但愿去优化某个包含了IO功能的应用性能时,咱们须要对于应用耗费的CPU周期以及那些妨碍到应用并行化执行的因素了如指掌。本文则是分享我在提高Apache Cassandra项目中的DataStax Node.js 驱动时的一些思考与总结出的致使应用吞吐量降级的关键因素。git

背景

Node.js使用的标准JavaScript引擎V8会将JavaScript代码编译为机器码而后以本地代码的方式运行。V8引擎使用了以下三个组件来同时保证较低的启动时间与最佳性能表现:github

  • 可以快速将JavaScript代码编译为机器码的通用编译器。web

  • 可以自动追踪应用中代码执行时间而且决定应该优化哪些代码模块的运行时分析器。性能优化

  • 可以自动优化被分析器标注的待优化代码的优化编译器;而且若是操做被认为是过优化,该编译器还能自动地进行逆优化操做。

尽管优化编译器可以保证最佳的性能表现,可是它并不会对全部的代码进行优化,特别是那些不合适的代码编写模式。你能够参考来自Google Chrome DevTools团队的建议来了解哪些代码模式是V8拒绝优化的,典型的包括:

  • 包含try-catch语句的函数

  • 使用arguments对象对函数参数进行从新赋值

虽然优化编译器可以显著提高代码容许速度,可是对于典型的IO密集型的应用,大部分的性能优化仍是依赖于指令重排以及避免高占用的调用来提升每秒的操做执行数目;这也会是咱们在接下来的章节中须要讨论的部分。

测试基准

为了可以更好地发现那些能够惠及最多用户的优化技巧,咱们须要模拟真实用户场景,根据经常使用任务执行的工做量来定义测试基准。首先咱们须要测试API入口点的吞吐量与时延;除此以外若是但愿获取更多的信息,你也能够选择对于内部调用方法进行性能评测。推荐使用process.hrtime()来获取实时解析与执行时长。虽然可能会对项目开发形成些许不便,但我仍是建议尽量早地在开发周期中引入性能评测。能够选择先从一些方法调用进行吞吐量测试,而后再慢慢地增长譬如时延分布这些相对复杂的测试。

CPU 分析

目前有多种CPU分析器可供咱们使用,其中Node.js自己提供的开箱即用的CPU分析器已经能应付大部分的使用场景。内建的Node.js分析器源于V8内置的分析器,它可以以固定地频率对栈信息进行采样;你能够在运行node命令时使用--prof参数来建立V8标记文件。而后你能够对分析结果进行聚合转化处理,经过使用--prof-process参数将其转化为可读性更好的文本:

$ node --prof-process isolate-0xnnnnnnnnnnnn-v8.log > processed.txt

在编辑器中打开通过处理的记录文件,你能够看到整个记录被划分为了部分,首先咱们来看下Summary部分,其格式以下所示:

[Summary]:

  ticks   total   nonlib  name

  20109   41.2%   45.7%  JavaScript

  23548   48.3%   53.5%  C++

    805    1.7%    1.8%  GC

   4774    9.8%          Shared libraries

    356    0.7%          Unaccounted

上面的值分别表明了在JavaScript/C++代码以及垃圾收集器中的采样频次,其会随着分析代码的不一样而变化。而后你能够根据须要分别查看具体的子部分(譬如[JavaScript], [C++], ...)来了解具体的采样信息。除此以外,分析文件中还包含一个叫作[Bottom up (heavy) profile]的很是有用的部分,它以树形结构展现了买个函数的调用者,其基本格式以下:

223  32%      LazyCompile: *function1 lib/file1.js:223:20

221  99%        LazyCompile: ~function2 lib/file2.js:70:57

221  100%         LazyCompile: *function3 /lib/file3.js:58:74

上面的百分比表明该层调用者占目标函数全部调用者数目的比重,而函数以前的星号意味着该函数是通过优化处理的,而波浪号表明该函数是未通过优化的。在上面的例子中,function199%的调用是由function2发起的,而function3占据了function2100%的调用占比。CPU 分析结果与火焰图是很是有用的分析栈占用与CPU耗时的工具。不过须要注意的是,这些分析结果并不意味着所有,大量的异步IO操做会让分析变得不那么容易。

系统调用

Node.js利用Libuv提供的平台无关的接口来实现非阻塞型IO,应用程序中全部的IO操做(sockets, 文件系统, ...)都会被转化为系统调用。而调度这些系统调用会耗费大量的时间,所以咱们须要尽量地聚合IO操做,以批量写的方式来最小化系统调用的次数。具体而言,咱们应该将Socket或者文件流放入到缓冲中而后一次性处理而不是对每一个操做进行单独处理。你可使用写队列来管理你的全部写操做,经常使用的写队列的实现逻辑以下:

  • 当咱们须要进行写操做而且在某个处理窗口期内:

    • 将该缓冲区添加到待写列表中

  • 链接全部的缓冲区而且一次性的写入到目标管道中。

你能够基于总的缓冲区长度或者第一个元素进入队列的时间来定义窗口尺寸,不过在定义窗口尺寸时咱们须要权衡考虑单个写操做的时延与总体写操做的时延,不能厚此薄彼。你也须要同时考虑可以聚合的写操做的最大数目以及单个写请求的开销。你可能会以千字节为单位决定一个写队列的上限,咱们的经验发现8千字节左右是个不错的临界点;固然根据你应用的具体场景这个值确定会有变化,你能够参考咱们的这个写队列的完整实现。总结而言,当咱们采用了批量写以后系统调用的数目大大下降了,最终提高了应用的总体吞吐量。

Node.js 定时器

Node.js中的定时器与window中的定时器具备相同的API,能够很方便地实现简单的调度操做;在整个生态系统中有很普遍的应用,所以咱们的应用中可能充斥着大量的延时调用。相似于其余基于散列的轮转调度器,Node.js使用散列表与链表来维护定时器实例。不过有别于其余的轮转调度器,Node.js并无维持固定长度的散列表,而是根据触发时间对定时器创建索引。添加新的定时器实例时,若是Node.js发现已经存在了相同的键值(有相同触发事件的定时器),那么会以O(1)复杂度完成添加操做。若是还不存在该键值,则会建立新的桶而后将定时器添加到该桶中。须要铭记于心的是,咱们应该尽量地重用已存在的定时器存放桶,避免移除整个桶而后再建立一个新的这种耗时的操做。举例而言,若是你使用滑动延时,那么应该在使用clearTimeout()移除定时器以前使用setTimeout()建立新的定时器。咱们对于心跳包的处理中在移除上一个定时器以前会先肯定下以O(1)复杂度调度空闲的定时器。

Ecmascript 语言特性

当咱们着眼于总体的性能保障时,咱们须要避免使用部分Ecmascript中的高级语言特性,典型的譬如:Function.prototype.bind(), Object.defineProperty() 以及 Object.defineProperties()。咱们能够在JavaScript引擎的实现描述或者问题中发现这些特性的性能缺陷所在,譬如Improvement in Promise performance in V8 5.3 以及 Function.prototype.bind performance in V8 5.4。另外你也须要谨慎使用ES2015或者ESNext中的新的语言特性,它们相较于ECMAScript 5中的语法会慢不少。six-speed 项目网站就追踪了这些语言特性在不一样的JavaScript引擎上的性能表现,若是你还没有发现某些特性的性能评测你也能够本身进行一些测试。V8 团队也一直致力于提升新的语言特性的性能表现,最终使其与底层实现保持一致。咱们能够在性能规划中随时了解他们对于ES2015性能优化的工做进展,这里他们会收集使用者对于提高点的建议而且发布新的设计文档来阐述他们的解决方案。你也能够在这个博客随时了解V8的实现进展,不过考虑到V8的提高可能须要较长的时间才能合并入LTS版本的Node.js: 根据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的时间片与指令的排布顺序会大大影响总体的吞吐量与高平行的实现程度。

关于做者

Jorge Bay是Apache Cassandra项目中Node.js以及C#客户端驱动的核心工程师,同时仍是DataStax的DSE。他乐于解决问题与提供服务端解决方案,Jorge拥有超过15年的专业软件开发经验,他为Apache Cassandra实现的Node.js客户端驱动一样也是DataStax官方驱动的基础

相关文章
相关标签/搜索