写点什么

一次一个微优化,改进 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:054901
用户头像

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

关注

评论

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

JavaScript数据类型

源字节1号

软件开发 前端开发 后端开发 小程序开发

“可严可仁”的考勤系统,让数字化不漏掉人性化

明道云

解构HE2E中的Kubernetes技术应用

华为云开发者联盟

Docker Kubernetes DevOps HE2E CCE部署

教你用 ECharts 轻松做一个Flappy Bird小游戏

华为云开发者联盟

图表 eCharts 图表库 Flappy Bird 小游戏

程序员转型产品经理:懂技术或许是把双刃剑!

博文视点Broadview

智能运维应用之道,告别企业数字化转型危机

云智慧AIOps社区

大数据 监控 数字化转型 智能运维 自动化运维

网站开发进阶(五十玖)css实现背景透明,文字不透明

No Silver Bullet

CSS 5月月更

C语言-strlen和sizeof强化习题练习- I

芒果酱

c++ C语言 5月月更

为什么前端不能没有监控系统?

杨成功

大前端 构架 5月月更

集简云 x Authing,助力网校打通用户身份管理屏障

Authing

低代码 单点登录 业务流程优化 小鹅通

uni-app技术分享| uni-app转小程序-实时消息

anyRTC开发者

小程序 uni-app 音视频 实时消息 呼叫邀请

队列同步器AQS

急需上岸的小谢

5月月更

ironSource 推出 Luna Views,通过定制化数据面板呈现多渠道广告效果

极客天地

C语言_字符串与指针的练习

DS小龙哥

5月月更

下个十年高性能 JSON 库来了:fastjson2!

王磊

Java

作业帮在线业务 Kubernetes Serverless 虚拟节点大规模应用实践

阿里巴巴云原生

阿里云 云原生 客户案例 作业帮 Kubernetes Serverless

Docker下的OpenResty三部曲之一:极速体验

程序员欣宸

Docker 5月月更 openrestry

PingCAP 宣布 TiDB Cloud 正式商用,助力全球企业在云上构建新一代云原生应用

极客天地

李东山——如何让OpenHarmony支持低功耗蓝牙芯片GR551x

OpenHarmony开发者

OpenHarmony 低功耗蓝牙芯片

浅述容器和容器镜像的区别

汪子熙

Docker 容器 容器镜像 虚拟化技术 5月月更

招商蛇口重塑客户经营新思路,推动多业态融合升级

科技热闻

如何使用 Authing 单点登录,集成 Discourse 论坛?

Authing

低代码 单点登录 Idaas 应用集成方案 Discourse

Go 学习笔记——函数篇一

为自己带盐

Go 5月月更

快慢缓急总相宜|ONES 人物

万事ONES

Java遇上SPL:架构优势和开发效率,一个不放过

华为云开发者联盟

Java stream 应用架构 SPL 结构化数据处理

WorkPlus统一门户:企业信息互通,实现业务协作

BeeWorks

Electron 插件开发实践

网易云信

c++ Electron

网站开发进阶(五十五)CSS padding、margin 属性

No Silver Bullet

5月月更 padding magin

SaaS到底是什么?如何做?

小炮

SaaS

一文,教你打造员工生命周期解决方案

Authing

单点登录 零信任 数据泄露 B2E 元气森林

2022年记一次慢查询优化指南,MySQL 优化学习第9天

梦想橡皮擦

5月月更

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