写点什么

一次一个微优化,改进 Node.js 应用的吞吐量

  • 2017-02-16
  • 本文字数:4098 字

    阅读完需:约 13 分钟

本文要点

  • 借助分组或批量写,尽量最小化系统调用数量。
  • 考虑应用中各种定时器的发布和清除开销。
  • CPU 性能分析器能提供有用的信息,但是不会告诉问题的原委。
  • 慎用 ECMAScript 高级特性,尤其是在未使用最新版 JavaScript 引擎或源码到源码的编译器时。
  • 控制 QDF 依赖树,并对依赖做基准测试。

为了改进涉及 IO 操作的 Node.js 应用的性能,你应了解 CPU 周期的使用情况,更为重要的是知道妨碍应用高度并行的症结所在。

我在关注改进 Apache Cassandra 的 DataStax Node.js 驱动的整体性能时,对此问题有了一些洞悉,并以此文分享出来,力图总结可导致应用吞吐量降级的最为重要的症结。

背景知识

Node.js 使用的 JavaScript 引擎 V8 将 JavaScript 编译成机器码,并以原生代码运行。为尽量达到低启动时间和峰值性能,V8 引擎使用了三个组件:

  1. 通用编译器,尽可能地快速地将 JavaScript 编译为机器码。
  2. 运行时性能分析器,追踪各部分代码运行所耗费的时间,识别其中值得优化的代码。
  3. 优化编译器,尽量优化被性能分析器识别的代码。它支持对优化器所做的过于乐观的假设去优化(deopt)。

通常优化编译器能达到最好的性能,但是并未选取全部 JavaScript 代码做优化,即存在被优化编译器拒绝优化的代码模式。

对于那些不能被 V8 优化但是或许有变通方案的代码模式,你可以使用来自于 Google Chrome DevTools 团队的解决方案作为工作指南找出他们。下面列出部分例子:

  • 具有 try-catch 语句的函数。
  • 在使用arguments域时,重赋值参数。

尽管优化编译器显著地加快了代码运行,但是正如我们将在下文中的,为在 IO 密集的应用中每秒能完成更多操作,大多数性能改进解决方法关注的是如何重排序指令以及使用更低代价的调用。

基准测试

为找到那些影响用户数量最大的可优化部分,重要的是对基准测试的定义。基准测试使用的工作负荷具有常用的执行路径,模拟了真实世界中的使用情况。

基准测试首先测定 API 入口点的吞吐量和延迟。你也可对单独内部方法的性能做基准测试,以得到更为详细的信息。使用process.hrtime()可实时地获取高精度的时间信息,得到程序执行的时间长度。

你应尽量创建有限但切实可行的基准测试。从方法吞吐量测定这样的小问题开始,然后添加更多更全面的信息,例如延迟分布等。

CPU 性能分析

现有多种 CPU 性能分析工具,Node.js 也提供了一种开箱可用的工具,在很多情况下该工具足以适用。内建的Node.js 性能分析工具使用 V8 引擎内的性能分析器,在程序执行中对程序栈做周期性地采样。使用–prof 标志运行 node 将会生成 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

每行中的百分比值显示了调用者占父调用总量的比例。函数名前面的星号表示函数优化后所用时间,波浪号表示未优化的函数。

在本例中,根据性能分析采样,99% 的 function1 调用来自 function2,而 100% 的 function2 调用来自 function3。

对于了解大部分时间中堆栈中的内容以及消耗 CPU 时间的方法,CPU 性能分析会话和结构框图是非常有用的工具,使用他们便于找到易于优化的目标。同时你应该明白他们并未提供全面的信息。例如,异步IO 操作会提高应用中的并行,但同时也会使导致性能降低的问题难以识别。

系统调用

Node.js 可使用 libuv 提供的独立于平台的 API 去执行非阻塞 IO 操作。Node.js 应用的 IO 操作(socket、文件系统等)最终都将转化为系统调用。

这些系统调用的调度需付出相当高的代价。应尽量使用分组或批量写去最小化系统调用量。

在使用 socket 或文件系统时,不应每次发布一个写操作,而应随时缓存并清空数据。

你可以使用写队列去处理并分组写操作。写队列实现的逻辑应类似于:

  • 具有处于窗口尺寸内的待写条目
  • 将缓存推入到“待写队列”中
  • 连接列表中所有的缓存并依次写入。

窗口大小可以根据缓存的总长度定义,或是根据自第一个条目进入队列后所过去的时间。定义窗口大小是在单一写延迟和平均写延迟之间取得权衡。也应考虑需组织在一起的写请求的总量,以及产生每次写请求的代价。

你通常会按写入内容的大小顺序将内容写入到缓存中。我们发现 8KB 大小的缓存是合适的,但是你可能并不这么认为。你可以去查看我们在客户驱动中的实现,了解完整的写队列实现

降低系统调用量使得分组或批量写转化为更高的吞吐量。

Node.js 定时器

Node.js 定时器十分有用,它的 API 和 Web API 中的 window 对象的计时器的 API 一样,易于调度和去调度,并已广泛用于整个生态系统。

鉴于此,应用在任何时刻都可能会出现大量的超时调度。

类似于其他的哈希轮盘定时器(Hased Wheel Timer),Node.js 使用哈希表和链表维护定时器实例。但是不同于其他的轮盘定时器,Node.js 并没有使用固定长度哈希表,而是以持续时间作为各个定时器列表的键值。

当列表中存在一个键值时(即存在持续时间相同的定时器),定时器以O(1) 代价的操作附加到桶上。

当该键值在列表中不存在时,Node.js 新创建一个桶,并将定时器附加到该桶上。

因此必须要确认已有的桶被重新使用,尽量避免移除整个桶并创建新桶。例如,如果你正在使用滑动延迟,应在移除旧的超时( cleartimeout())前就创建新的超时(setTimeout())。

对于我们而言,我们将调度空闲的超时(心跳)先于移除前期超时实现,这确保了O(1) 代价的空闲超时调度和去调度操作。

Ecmascript 特性

如果你关注的是性能问题,应该慎用一些 Ecmascript 高层特性,其中包括: Function.prototype.bind() Object.defineProperty() Object.defineProperties()等。

这些特性的性能不好,主要由 JavaScript 引擎中的实现细节所导致。其中的一些问题已得到解决,例如:在V8 5.3 引擎中Promise 性能的改进在V8 5.4 引擎中Function.prototype.bind 的性能

应对ES2015 和ESNext 中的新语言特性格外谨慎。与ECMAScript 5 中的相应特性相比,这些新语言特性明显要慢。 six-speed 项目网站跟踪记录了他们在不同 JavaScript 引擎上性能的进度。此外,在不能从现有基准测试中找到结论性结果时,你可以对各种方法做微基准测试。

V8 团队正在致力于改进新语言特性的性能,最终要达到与原生特性相同性能。他们通过一份性能规划协调针对ES2015 及以后版本引入的特性的优化工作,V8 团队通过该计划收集需要改进的地方以及提议的应对这些问题的设计文档。

你可以通过指定博客跟踪V8 实现的进展,但是考虑到这些改进还需相当长的时间才能进入到Node.js 的长期支持(LTS,Long-term Support)版本中(根据 LTS 规划,进入 Node.js 主版本的 V8 版本通常是在该版本被从主分支裁剪前确定),为使用包括新的 V8 主版本或小版本的 Node.js 运行时,你将不得不再等待 6 到 12 个月的时间。

新的 Node.js 主版本将只以补丁形式更新V8 引擎

依赖

Node.js 运行时提供了完整的 IO 操作函数库,但是由于 ECMAScript 规范中提供了非常少的内建类型,有时你不得不依赖于外部软件包去执行其他基本任务。

即便是那些广为使用的模块,也不能保证发布的软件包会以有效方式并正确地工作。Node.js 的生态系统非常庞大,通常这些第三方模块只包括了很少能让你自己实现的方法。

对于去重做轮子还是去控制性能对依赖的影响,你应该在两者间权衡。

任何情况下都应避免添加新的依赖。不要相信你的依赖,就是这样。此原则的例外是如果所依赖的项目自身发布了可靠的基准测试,就像 bluebird 程序库那样。

就我们而言, async 对请求延迟有影响。我们在代码块中广泛地使用了 async.series() async.waterfall() async.whilst()。由于控制流程序库是一个横切关注点,这使得难以识别导致性能问题的坏份子。由于 async 是最为广泛使用的模块之一,由 async 导致的性能问题得到了广泛关注。async 具有简化的替代实现,例如 neo-async 。neo-async 的运行性能得到了显著的改进,也发布了基准测试。

总结

虽然这里给出的一些优化技术对于其它的技术也是通用的,但是其中的部分技术是特定于 Node.js 生态系统及 JavaScript 引擎和核心库的工作方式的。

对于我们的客户驱动,其中已经应用了这些优化技术。根据我们的基准测试结果,这些优化技术导致了吞吐量增加了两倍以上。

考虑到我们的代码在 Node.js 上是单线程运行的,优化程度还取决于应用消耗 CPU 周期的方式和指令的顺序。我们可以通过支持高度并行改进整体的吞吐量。

关于作者

Jorge Bay是 DataStax 公司负责 Apache Cassandra 和 DSE 的 Node.js 和 C#客户驱动的首席工程师。他在本职工作之余,也享受解决问题和构建服务器端解决方案的乐趣。Jorge 具有超过 15 年的专业软件开发经验,他实现了 Apache Cassandra 的社区版 Node.js 驱动,该驱动也是 DataStax 官方驱动的基础。

查看英文原文: Improve Your Node.js App Throughput One Micro-optimization at a Time


感谢王纯超对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017-02-16 16:054630
用户头像

发布了 227 篇内容, 共 74.2 次阅读, 收获喜欢 28 次。

关注

评论

发布
暂无评论
发现更多内容

B站挂了登上全网热搜!技术人员为你还原前因后果

雨果

Kyligence 正式支持 Amazon EMR Serverless,构建高效低成本云上数据分析

Kyligence

Cloud Kyligence Amazon EMR

SAP Fiori Launchpad 应用的两个实用技巧分享

汪子熙

JavaScript SAP SAP UI5 ui5 7月月更

4 个 JavaScript 最基础的问题 —— Eric Elliott

掘金安东尼

JavaScript 面试 前端 7月月更

python小知识-rethinking python 生成器

AIWeker

Python python小知识 7月月更

TPC藏宝计划质押系统开发(Dapp)

薇電13242772558

智能合约 dapp

2022年浙江省等保备案流程指南

行云管家

等保 等保备案

如何快速有效的定位应用抖动问题?| 龙蜥技术

OpenAnolis小助手

Linux 系统 龙蜥技术 SysAK 抖动

不会吧!钉钉都下载了,你还不知道可以这样玩?

Jianmu

钉钉 持续集成 自动化运维 建木CI 通知

全新出品!阿里P5工程师~P8架构师晋升路线揭秘

程序员小毕

Java 程序员 面试 架构师 学习路线

Lite Actor:方舟Actor并发模型的轻量级优化

HarmonyOS开发者

HarmonyOS

TiKV & TiFlash 加速复杂业务查询

TiDB 社区干货传送门

Okaleido或杀出NFT重围,你看好它吗?

鳄鱼视界

音视频开发进阶|第四讲:音频自动增益控制 AGC

ZEGO即构

音视频开发 AGC

nacos注册中心之服务注册

急需上岸的小谢

7月月更

MySQL数据库优化

五分钟学大数据

MySQL 7月月更

声网传输层协议 AUT 的总结与展望丨Dev for Dev 专栏

声网

传输协议 Dev for Dev

uni-app进阶之内嵌应用【day14】

恒山其若陋兮

7月月更

还在羡慕其它平台有跨店满减,其实你也可以!

CRMEB

边无际 Shifu IoT 开源开发框架 助力物联网应用开发加速十倍

亚马逊云科技 (Amazon Web Services)

开源 Kubernetes 物联网 应用开发

异想天开 | 假如用中文写代码,是一种什么体验?

雨果

程序员 开发者

龙蜥开发者说:社区首个支持 LoongArch架构的操作系统构建之路 | 第 9 期

OpenAnolis小助手

开源 操作系统 龙蜥开发者说 龙芯 LoongArch 架构

查找——B-树

乔乔

7月月更

向量化执行引擎框架 Gluten 宣布正式开源,并亮相 Spark 技术峰会

Kyligence

spark Gluten

jdbc自带MySQL连接池实践

FunTester

Google上网神器Ghelper

源字节1号

软件开发 小程序开发

想进大厂拿高薪?掌握Redis的Sentinel哨兵原理将是至关重要的突破口

了不起的程序猿

Java java程序员 Redis 数据结构

《黑客与画家》作者:18 个会杀死初创公司的错误

雨果

创业者

模块七作业 - 王者荣耀商城异地多活架构设计

Elvis FAN

云计算和大数据的关系以及区别详细讲解

行云管家

云计算 大大数据

JAVA编程规范之控制语句

源字节1号

后端开发

一次一个微优化,改进Node.js应用的吞吐量_JavaScript_Jorge Bay_InfoQ精选文章